Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to add custom features to robot preview #907

Merged
merged 4 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Writerside/topics/Robot-Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ True Max Drive Speed
>
> It is very important that the True Max Drive Speed is measured for your actual robot. This value is not simply a "max
> velocity" limit on the robot. It encodes information about how much motor torque can actually be used to accelerate
> the
> robot.
> the robot.
>
> This can be easily measured by driving the robot in a straight line as fast as possible on a charged battery, and
> measuring the robot's maximum velocity.
Expand All @@ -107,4 +106,9 @@ Drive Current Limit
## Module Offsets

The locations of each swerve module relative to the center of the robot, in meters. These should be the same offsets
used to create your kinematics in code. Only available for swerve drive robots.
used to create your kinematics in code. Only available for swerve drive robots.

## Robot Features

Robot features are shapes that can be added to the robot in the GUI. These can be used to represent intakes, shooting
trajectories, etc. Current features include: rectangle, circle, and lines.
57 changes: 22 additions & 35 deletions lib/commands/command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,43 +25,30 @@ abstract class Command {

static Command? fromJson(Map<String, dynamic> json) {
String? type = json['type'];

if (type == 'wait') {
return WaitCommand.fromDataJson(json['data'] ?? {});
} else if (type == 'named') {
return NamedCommand.fromDataJson(json['data'] ?? {});
} else if (type == 'path') {
return PathCommand.fromDataJson(json['data'] ?? {});
} else if (type == 'sequential') {
return SequentialCommandGroup.fromDataJson(json['data'] ?? {});
} else if (type == 'parallel') {
return ParallelCommandGroup.fromDataJson(json['data'] ?? {});
} else if (type == 'race') {
return RaceCommandGroup.fromDataJson(json['data'] ?? {});
} else if (type == 'deadline') {
return DeadlineCommandGroup.fromDataJson(json['data'] ?? {});
}

return null;
Map<String, dynamic> data = json['data'] ?? {};

return switch (type) {
'wait' => WaitCommand.fromDataJson(data),
'named' => NamedCommand.fromDataJson(data),
'path' => PathCommand.fromDataJson(data),
'sequential' => SequentialCommandGroup.fromDataJson(data),
'parallel' => ParallelCommandGroup.fromDataJson(data),
'race' => RaceCommandGroup.fromDataJson(data),
'deadline' => DeadlineCommandGroup.fromDataJson(data),
_ => null,
};
}

static Command? fromType(String type, {List<Command>? commands}) {
if (type == 'named') {
return NamedCommand();
} else if (type == 'wait') {
return WaitCommand();
} else if (type == 'path') {
return PathCommand();
} else if (type == 'sequential') {
return SequentialCommandGroup(commands: commands ?? []);
} else if (type == 'parallel') {
return ParallelCommandGroup(commands: commands ?? []);
} else if (type == 'race') {
return RaceCommandGroup(commands: commands ?? []);
} else if (type == 'deadline') {
return DeadlineCommandGroup(commands: commands ?? []);
}

return null;
return switch (type) {
'named' => NamedCommand(),
'wait' => WaitCommand(),
'path' => PathCommand(),
'sequential' => SequentialCommandGroup(commands: commands ?? []),
'parallel' => ParallelCommandGroup(commands: commands ?? []),
'race' => RaceCommandGroup(commands: commands ?? []),
'deadline' => DeadlineCommandGroup(commands: commands ?? []),
_ => null,
};
}
}
10 changes: 10 additions & 0 deletions lib/pages/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,8 @@
json, PrefsKeys.defaultMaxAngVel, Defaults.defaultMaxAngVel);
_setPrefDoubleFromJSON(
json, PrefsKeys.defaultMaxAngAccel, Defaults.defaultMaxAngAccel);
_setPrefDoubleFromJSON(

Check warning on line 551 in lib/pages/home_page.dart

View check run for this annotation

Codecov / codecov/patch

lib/pages/home_page.dart#L551

Added line #L551 was not covered by tests
json, PrefsKeys.defaultNominalVoltage, Defaults.defaultNominalVoltage);
_setPrefDoubleFromJSON(json, PrefsKeys.robotMass, Defaults.robotMass);
_setPrefDoubleFromJSON(json, PrefsKeys.robotMOI, Defaults.robotMOI);
_setPrefDoubleFromJSON(
Expand All @@ -574,6 +576,8 @@
json, PrefsKeys.bumperOffsetX, Defaults.bumperOffsetX);
_setPrefDoubleFromJSON(
json, PrefsKeys.bumperOffsetY, Defaults.bumperOffsetY);
widget.prefs.setString(PrefsKeys.robotFeatures,
json[PrefsKeys.robotFeatures] ?? Defaults.robotFeatures);

Check warning on line 580 in lib/pages/home_page.dart

View check run for this annotation

Codecov / codecov/patch

lib/pages/home_page.dart#L579-L580

Added lines #L579 - L580 were not covered by tests
}

void _setPrefDoubleFromJSON(
Expand Down Expand Up @@ -616,6 +620,9 @@
PrefsKeys.defaultMaxAngAccel:
widget.prefs.getDouble(PrefsKeys.defaultMaxAngAccel) ??
Defaults.defaultMaxAccel,
PrefsKeys.defaultNominalVoltage:
widget.prefs.getDouble(PrefsKeys.defaultNominalVoltage) ??

Check warning on line 624 in lib/pages/home_page.dart

View check run for this annotation

Codecov / codecov/patch

lib/pages/home_page.dart#L624

Added line #L624 was not covered by tests
Defaults.defaultNominalVoltage,
PrefsKeys.robotMass:
widget.prefs.getDouble(PrefsKeys.robotMass) ?? Defaults.robotMass,
PrefsKeys.robotMOI:
Expand Down Expand Up @@ -660,6 +667,9 @@
PrefsKeys.bumperOffsetY:
widget.prefs.getDouble(PrefsKeys.bumperOffsetY) ??
Defaults.bumperOffsetY,
PrefsKeys.robotFeatures:
widget.prefs.getStringList(PrefsKeys.robotFeatures) ??

Check warning on line 671 in lib/pages/home_page.dart

View check run for this annotation

Codecov / codecov/patch

lib/pages/home_page.dart#L671

Added line #L671 was not covered by tests
Defaults.robotFeatures,
};

settingsFile.writeAsString(encoder.convert(settings)).then((_) {
Expand Down
22 changes: 20 additions & 2 deletions lib/pages/telemetry_page.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'dart:collection';
import 'dart:convert';
import 'dart:math';

import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:pathplanner/robot_features/feature.dart';
import 'package:pathplanner/services/pplib_telemetry.dart';
import 'package:pathplanner/util/path_painter_util.dart';
import 'package:pathplanner/util/prefs.dart';
Expand Down Expand Up @@ -39,6 +41,7 @@
late final Size _robotSize;
late final Translation2d _bumperOffset;
late bool _useSim;
final List<Feature> _robotFeatures = [];

bool _gotCurrentPose = false;
bool _gotTargetPose = false;
Expand All @@ -62,6 +65,16 @@
_useSim = widget.prefs.getBool(PrefsKeys.telemetryUseSim) ??
Defaults.telemetryUseSim;

for (String featureJson
in widget.prefs.getStringList(PrefsKeys.robotFeatures) ??
Defaults.robotFeatures) {

Check warning on line 70 in lib/pages/telemetry_page.dart

View check run for this annotation

Codecov / codecov/patch

lib/pages/telemetry_page.dart#L70

Added line #L70 was not covered by tests
try {
_robotFeatures.add(Feature.fromJson(jsonDecode(featureJson))!);

Check warning on line 72 in lib/pages/telemetry_page.dart

View check run for this annotation

Codecov / codecov/patch

lib/pages/telemetry_page.dart#L72

Added line #L72 was not covered by tests
} catch (_) {
// Ignore and skip loading this feature
}
}

widget.telemetry.connectionStatusStream().listen((connected) {
if (mounted) {
setState(() {
Expand Down Expand Up @@ -232,6 +245,7 @@
targetPose: _targetPose,
currentPath: _currentPath,
colorScheme: Theme.of(context).colorScheme,
robotFeatures: _robotFeatures,
),
),
),
Expand Down Expand Up @@ -577,6 +591,7 @@
final Pose2d? targetPose;
final List<Pose2d>? currentPath;
final ColorScheme colorScheme;
final List<Feature> robotFeatures;

static double scale = 1;

Expand All @@ -588,6 +603,7 @@
this.targetPose,
this.currentPath,
required this.colorScheme,
required this.robotFeatures,
});

@override
Expand Down Expand Up @@ -623,7 +639,8 @@
scale,
canvas,
Colors.grey[600]!.withOpacity(0.75),
colorScheme.surfaceContainer);
colorScheme.surfaceContainer,
robotFeatures);
}

if (currentPose != null) {
Expand All @@ -635,7 +652,8 @@
scale,
canvas,
colorScheme.primary,
colorScheme.surfaceContainer);
colorScheme.surfaceContainer,
robotFeatures);
}
}

Expand Down
63 changes: 63 additions & 0 deletions lib/robot_features/circle_feature.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:pathplanner/robot_features/feature.dart';
import 'package:pathplanner/util/wpimath/geometry.dart';

class CircleFeature extends Feature {
Translation2d center;
num radius;
num strokeWidth;
bool filled;

CircleFeature({
this.center = const Translation2d(),
this.radius = 0.1,
this.strokeWidth = 0.02,
this.filled = false,
required super.name,
}) : super(type: 'circle');

CircleFeature.fromDataJson(Map<String, dynamic> dataJson, String name)
: this(
center: Translation2d.fromJson(dataJson['center']),
radius: dataJson['radius'],
strokeWidth: dataJson['strokeWidth'],
filled: dataJson['filled'],
name: name,
);

@override
Map<String, dynamic> dataToJson() {
return {
'center': center.toJson(),
'radius': radius,
'strokeWidth': strokeWidth,
'filled': filled,
};
}

@override
void draw(Canvas canvas, double pixelsPerMeter, Color color) {
Paint paint = Paint()
..style = filled ? PaintingStyle.fill : PaintingStyle.stroke
..strokeWidth = strokeWidth * pixelsPerMeter
..color = color;

Offset centerPixels =
Offset(center.x * pixelsPerMeter, -center.y * pixelsPerMeter);
double radiusPixels = radius * pixelsPerMeter;

canvas.drawCircle(centerPixels, radiusPixels, paint);
}

@override
int get hashCode => Object.hash(type, center, radius, strokeWidth, filled);

@override
bool operator ==(Object other) {
return other is CircleFeature &&
other.center == center &&
other.radius == radius &&
other.strokeWidth == strokeWidth &&
other.filled == filled;
}
}
41 changes: 41 additions & 0 deletions lib/robot_features/feature.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:pathplanner/robot_features/circle_feature.dart';
import 'package:pathplanner/robot_features/line_feature.dart';
import 'package:pathplanner/robot_features/rounded_rect_feature.dart';

abstract class Feature {
String name;
final String type;

Feature({required this.name, required this.type});

/// Draw the feature on the canvas with the given pixels per meter ratio
/// The canvas should already be transformed to the center of the robot
/// and its rotation. This just needs to draw relative to the robot.
void draw(Canvas canvas, double pixelsPerMeter, Color color);

Map<String, dynamic> dataToJson();

@nonVirtual
Map<String, dynamic> toJson() {
return {
'name': name,
'type': type,
'data': dataToJson(),
};
}

static Feature? fromJson(Map<String, dynamic> json) {
String name = json['name'];
String type = json['type'];
Map<String, dynamic> data = json['data'] ?? {};

return switch (type) {
'rounded_rect' => RoundedRectFeature.fromDataJson(data, name),
'circle' => CircleFeature.fromDataJson(data, name),
'line' => LineFeature.fromDataJson(data, name),
_ => null,
};
}
}
58 changes: 58 additions & 0 deletions lib/robot_features/line_feature.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:pathplanner/robot_features/feature.dart';
import 'package:pathplanner/util/wpimath/geometry.dart';

class LineFeature extends Feature {
Translation2d start;
Translation2d end;
num strokeWidth;

LineFeature({
this.start = const Translation2d(),
this.end = const Translation2d(0.2, 0.0),
this.strokeWidth = 0.02,
required super.name,
}) : super(type: 'line');

LineFeature.fromDataJson(Map<String, dynamic> dataJson, String name)
: this(
start: Translation2d.fromJson(dataJson['start']),
end: Translation2d.fromJson(dataJson['end']),
strokeWidth: dataJson['strokeWidth'],
name: name,
);

@override
Map<String, dynamic> dataToJson() {
return {
'start': start.toJson(),
'end': end.toJson(),
'strokeWidth': strokeWidth,
};
}

@override
void draw(Canvas canvas, double pixelsPerMeter, Color color) {
Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth * pixelsPerMeter
..color = color;

Offset startPixels =
Offset(start.x * pixelsPerMeter, -start.y * pixelsPerMeter);
Offset endPixels = Offset(end.x * pixelsPerMeter, -end.y * pixelsPerMeter);

canvas.drawLine(startPixels, endPixels, paint);
}

@override
int get hashCode => Object.hash(type, start, end, strokeWidth);

@override
bool operator ==(Object other) {
return other is LineFeature &&
other.start == start &&
other.end == end &&
other.strokeWidth == strokeWidth;
}
}
Loading
Loading