From 389b3f5ecd777708ccfa20b1d957308657345e22 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 4 Nov 2024 19:36:12 -0500 Subject: [PATCH 001/110] Initial test of diagonal movement --- bin/task.dart | 6 +- lib/src/interfaces/a_star.dart | 91 +++++++++++++++++++++++++++++-- lib/src/interfaces/drive.dart | 46 +++++++++++++++- lib/src/interfaces/gps_utils.dart | 12 ++++ lib/src/rover/drive.dart | 6 ++ lib/src/rover/drive/sensor.dart | 12 ++++ lib/src/rover/drive/timed.dart | 12 ++++ lib/src/rover/orchestrator.dart | 10 ++-- lib/src/rover/sensorless.dart | 12 ++++ lib/src/simulator/drive.dart | 19 +++++++ pubspec.lock | 16 ++++-- test/path_test.dart | 12 ++-- 12 files changed, 229 insertions(+), 25 deletions(-) diff --git a/bin/task.dart b/bin/task.dart index a267658..798289b 100644 --- a/bin/task.dart +++ b/bin/task.dart @@ -5,7 +5,6 @@ import "package:burt_network/logging.dart"; final chair = (2, 0).toGps(); final obstacles = [ - SimulatedObstacle(coordinates: (2, 0).toGps(), radius: 3), SimulatedObstacle(coordinates: (6, -1).toGps(), radius: 3), SimulatedObstacle(coordinates: (6, 1).toGps(), radius: 3), ]; @@ -17,9 +16,10 @@ void main() async { simulator.detector = DetectorSimulator(collection: simulator, obstacles: obstacles); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.orchestrator = RoverOrchestrator(collection: simulator); - simulator.drive = RoverDrive(collection: simulator, useImu: true, useGps: false); + // simulator.drive = RoverDrive(collection: simulator, useImu: true, useGps: false); simulator.gps = GpsSimulator(collection: simulator); -// simulator.drive = DriveSimulator(collection: simulator); + simulator.imu = ImuSimulator(collection: simulator); + simulator.drive = DriveSimulator(collection: simulator, shouldDelay: true); await simulator.init(); await simulator.imu.waitForValue(); // await simulator.drive.faceNorth(); diff --git a/lib/src/interfaces/a_star.dart b/lib/src/interfaces/a_star.dart index e36d4bf..3f7b9e6 100644 --- a/lib/src/interfaces/a_star.dart +++ b/lib/src/interfaces/a_star.dart @@ -1,3 +1,5 @@ +import "dart:math"; + import "package:a_star/a_star.dart"; import "package:burt_network/generated.dart"; @@ -37,10 +39,12 @@ class AutonomyAStarState extends AStarState { DriveDirection.left => "Turn left to face $direction", DriveDirection.right => "Turn right to face $direction", DriveDirection.stop => "Start/Stop at ${position.prettyPrint()}", + DriveDirection.forwardLeft => "Turn 45 degrees left to face $direction", + DriveDirection.forwardRight => "Turn 45 degrees right to face $direction", }; @override - double heuristic() => position.manhattanDistance(goal); + double heuristic() => position.distanceTo(goal); @override String hash() => "${position.prettyPrint()} ($orientation)"; @@ -54,13 +58,88 @@ class AutonomyAStarState extends AStarState { orientation: orientation, direction: direction, goal: goal, - depth: direction == DriveDirection.forward ? depth + 1 : depth + 2, + depth: (direction == DriveDirection.forward) + ? depth + 1 + : (direction == DriveDirection.forwardLeft || direction == DriveDirection.forwardRight) + ? depth + sqrt2 + : depth + 2, ); + bool drivingThroughObstacle(AutonomyAStarState state) { + final isTurn = state.direction != DriveDirection.forward; + final isQuarterTurn = state.direction == DriveDirection.forwardLeft || state.direction == DriveDirection.forwardRight; + + // Forward drive across the perpendicular axis + if (!isTurn && state.orientation.angle.abs() % 90 == 0) { + return false; + } + + // Not encountering any sort of diagonal angle + if (isTurn && isQuarterTurn && state.orientation.angle.abs() % 90 == 0) { + return false; + } + + // No diagonal movement, won't drive between obstacles + if (!isQuarterTurn && orientation.angle.abs() % 90 == 0) { + return false; + } + + DriveOrientation orientation1; + DriveOrientation orientation2; + + if (!isTurn) { + orientation1 = state.orientation.turnQuarterLeft(); + orientation2 = state.orientation.turnQuarterRight(); + } else if (isQuarterTurn) { + orientation1 = orientation; + orientation2 = (state.direction == DriveDirection.forwardLeft) + ? orientation1.turnLeft() + : orientation1.turnRight(); + } else { + orientation1 = (state.direction == DriveDirection.left) + ? orientation.turnQuarterLeft() + : orientation.turnQuarterRight(); + orientation2 = (state.direction == DriveDirection.left) + ? state.orientation.turnQuarterLeft() + : state.orientation.turnQuarterRight(); + } + + // Since the state being passed has a position of moving after the + // turn, we have to check the position of where it started + return collection.pathfinder.isObstacle( + position.goForward(orientation1), + ) || + collection.pathfinder.isObstacle( + position.goForward(orientation2), + ); + } + @override Iterable expand() => [ - copyWith(direction: DriveDirection.forward, orientation: orientation, position: position.goForward(orientation)), - copyWith(direction: DriveDirection.left, orientation: orientation.turnLeft(), position: position), - copyWith(direction: DriveDirection.right, orientation: orientation.turnRight(), position: position), - ].where((state) => !collection.pathfinder.isObstacle(state.position)); + copyWith( + direction: DriveDirection.forward, + orientation: orientation, + position: position.goForward(orientation), + ), + copyWith( + direction: DriveDirection.left, + orientation: orientation.turnLeft(), + position: position, + ), + copyWith( + direction: DriveDirection.right, + orientation: orientation.turnRight(), + position: position, + ), + copyWith( + direction: DriveDirection.forwardLeft, + orientation: orientation.turnQuarterLeft(), + position: position, + ), + copyWith( + direction: DriveDirection.forwardRight, + orientation: orientation.turnQuarterRight(), + position: position, + ), + ].where((state) => !collection.pathfinder.isObstacle(state.position) && !drivingThroughObstacle(state)); } diff --git a/lib/src/interfaces/drive.dart b/lib/src/interfaces/drive.dart index d7dd8b2..03c8d8e 100644 --- a/lib/src/interfaces/drive.dart +++ b/lib/src/interfaces/drive.dart @@ -5,14 +5,22 @@ enum DriveDirection { forward, left, right, - stop, + forwardLeft, + forwardRight, + stop; + + bool get isTurn => this != forward && this != stop; } enum DriveOrientation { north(0), west(90), south(180), - east(270); + east(270), + northEast(360 - 45), + northWest(45), + southEast(180 + 45), + southWest(180 - 45); final int angle; const DriveOrientation(this.angle); @@ -30,6 +38,10 @@ enum DriveOrientation { west => south, south => east, east => north, + northEast => northWest, + northWest => southWest, + southWest => southEast, + southEast => northEast, }; DriveOrientation turnRight() => switch (this) { @@ -37,6 +49,32 @@ enum DriveOrientation { west => north, south => west, east => south, + northEast => southEast, + southEast => southWest, + southWest => northWest, + northWest => northEast, + }; + + DriveOrientation turnQuarterLeft() => switch (this) { + north => northWest, + northWest => west, + west => southWest, + southWest => south, + south => southEast, + southEast => east, + east => northEast, + northEast => north, + }; + + DriveOrientation turnQuarterRight() => switch (this) { + north => northEast, + northEast => east, + east => southEast, + southEast => south, + south => southWest, + southWest => west, + west => northWest, + northWest => north, }; } @@ -49,6 +87,8 @@ abstract class DriveInterface extends Service { DriveDirection.forward => await goForward(), DriveDirection.left => await turnLeft(), DriveDirection.right => await turnRight(), + DriveDirection.forwardLeft => await turnQuarterLeft(), + DriveDirection.forwardRight => await turnQuarterRight(), DriveDirection.stop => await stop(), }; @@ -57,6 +97,8 @@ abstract class DriveInterface extends Service { Future goForward(); Future turnLeft(); Future turnRight(); + Future turnQuarterLeft(); + Future turnQuarterRight(); Future stop(); Future faceDirection(DriveOrientation orientation) async { diff --git a/lib/src/interfaces/gps_utils.dart b/lib/src/interfaces/gps_utils.dart index a303a6a..8a04356 100644 --- a/lib/src/interfaces/gps_utils.dart +++ b/lib/src/interfaces/gps_utils.dart @@ -21,6 +21,14 @@ extension GpsUtils on GpsCoordinates { GpsCoordinates(latitude: 1 * latitudePerMeter); static GpsCoordinates get south => GpsCoordinates(latitude: -1 * latitudePerMeter); + static GpsCoordinates get northEast => GpsCoordinates( + latitude: 1 * latitudePerMeter, longitude: -1 / metersPerLongitude); + static GpsCoordinates get northWest => GpsCoordinates( + latitude: 1 * latitudePerMeter, longitude: 1 / metersPerLongitude); + static GpsCoordinates get southEast => GpsCoordinates( + latitude: -1 * latitudePerMeter, longitude: -1 / metersPerLongitude); + static GpsCoordinates get southWest => GpsCoordinates( + latitude: -1 * latitudePerMeter, longitude: 1 / metersPerLongitude); // Taken from https://stackoverflow.com/a/39540339/9392211 // static const metersPerLatitude = 111.32 * 1000; // 111.32 km @@ -58,5 +66,9 @@ extension GpsUtils on GpsCoordinates { DriveOrientation.south => GpsUtils.south, DriveOrientation.west => GpsUtils.west, DriveOrientation.east => GpsUtils.east, + DriveOrientation.northEast => GpsUtils.northEast, + DriveOrientation.northWest => GpsUtils.northWest, + DriveOrientation.southEast => GpsUtils.southEast, + DriveOrientation.southWest => GpsUtils.southWest, }; } diff --git a/lib/src/rover/drive.dart b/lib/src/rover/drive.dart index 7cef09e..7235668 100644 --- a/lib/src/rover/drive.dart +++ b/lib/src/rover/drive.dart @@ -64,4 +64,10 @@ class RoverDrive extends DriveInterface { @override Future turnRight() => useImu ? sensorDrive.turnRight() : timedDrive.turnRight(); + + @override + Future turnQuarterLeft() => useImu ? sensorDrive.turnQuarterLeft() : timedDrive.turnQuarterLeft(); + + @override + Future turnQuarterRight() => useImu ? sensorDrive.turnQuarterRight() : timedDrive.turnQuarterRight(); } diff --git a/lib/src/rover/drive/sensor.dart b/lib/src/rover/drive/sensor.dart index 7a6ea93..7611c28 100644 --- a/lib/src/rover/drive/sensor.dart +++ b/lib/src/rover/drive/sensor.dart @@ -104,6 +104,18 @@ class SensorDrive extends DriveInterface with RoverMotors { this.orientation = this.orientation.turnRight(); } + @override + Future turnQuarterLeft() { + // TODO: implement turnQuarterLeft + throw UnimplementedError(); + } + + @override + Future turnQuarterRight() { + // TODO: implement turnQuarterRight + throw UnimplementedError(); + } + @override Future spinForAruco() async { for (var i = 0; i < 16; i++) { diff --git a/lib/src/rover/drive/timed.dart b/lib/src/rover/drive/timed.dart index 142a1ff..3921bdc 100644 --- a/lib/src/rover/drive/timed.dart +++ b/lib/src/rover/drive/timed.dart @@ -48,4 +48,16 @@ class TimedDrive extends DriveInterface with RoverMotors { await Future.delayed(turnDelay); await stop(); } + + @override + Future turnQuarterLeft() { + // TODO: implement turnQuarterLeft + throw UnimplementedError(); + } + + @override + Future turnQuarterRight() { + // TODO: implement turnQuarterRight + throw UnimplementedError(); + } } diff --git a/lib/src/rover/orchestrator.dart b/lib/src/rover/orchestrator.dart index 6a88b0d..2000093 100644 --- a/lib/src/rover/orchestrator.dart +++ b/lib/src/rover/orchestrator.dart @@ -39,11 +39,13 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.logger.info("Got GPS Task: Go to ${destination.prettyPrint()}"); collection.logger.debug("Currently at ${collection.gps.coordinates.prettyPrint()}"); collection.drive.setLedStrip(ProtoColor.RED); + collection.detector.findObstacles(); + // await collection.drive.faceNorth(); while (!collection.gps.coordinates.isNear(destination)) { // Calculate a path collection.logger.debug("Finding a path"); currentState = AutonomyState.PATHING; - await collection.drive.faceNorth(); + // await collection.drive.faceNorth(); final path = collection.pathfinder.getPath(destination); currentPath = path; // also use local variable path for promotion if (path == null) { @@ -61,13 +63,13 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.logger.debug(step.toString()); } currentState = AutonomyState.DRIVING; - var count = 0; + // var count = 0; for (final state in path) { collection.logger.debug(state.toString()); await collection.drive.goDirection(state.direction); traversed.add(state.position); - if (state.direction != DriveDirection.forward) continue; - if (count++ == 5) break; + // if (state.direction != DriveDirection.forward) continue; + // if (count++ == 5) break; final foundObstacle = collection.detector.findObstacles(); if (foundObstacle) { collection.logger.debug("Found an obstacle. Recalculating path..."); diff --git a/lib/src/rover/sensorless.dart b/lib/src/rover/sensorless.dart index 2beab09..fc07345 100644 --- a/lib/src/rover/sensorless.dart +++ b/lib/src/rover/sensorless.dart @@ -61,4 +61,16 @@ class SensorlessDrive extends DriveInterface { await simulatedDrive.turnRight(); await realDrive.turnRight(); } + + @override + Future turnQuarterLeft() { + // TODO: implement turnQuarterLeft + throw UnimplementedError(); + } + + @override + Future turnQuarterRight() { + // TODO: implement turnQuarterRight + throw UnimplementedError(); + } } diff --git a/lib/src/simulator/drive.dart b/lib/src/simulator/drive.dart index 015dc1a..b1b04d8 100644 --- a/lib/src/simulator/drive.dart +++ b/lib/src/simulator/drive.dart @@ -29,6 +29,7 @@ class DriveSimulator extends DriveInterface { @override Future faceNorth() async { collection.imu.update(OrientationUtils.north); + if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); } @override @@ -49,6 +50,24 @@ class DriveSimulator extends DriveInterface { if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); } + @override + Future turnQuarterLeft() async { + collection.logger.debug("Turning quarter left"); + final heading = collection.imu.heading; + final orientation = Orientation(z: heading + 45).clampHeading(); + collection.imu.update(orientation); + if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); + } + + @override + Future turnQuarterRight() async { + collection.logger.debug("Turning quarter right"); + final heading = collection.imu.heading; + final orientation = Orientation(z: heading - 45).clampHeading(); + collection.imu.update(orientation); + if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); + } + @override Future stop() async => collection.logger.debug("Stopping"); } diff --git a/pubspec.lock b/pubspec.lock index 0f65b3b..7366b66 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,9 +57,11 @@ packages: burt_network: dependency: "direct main" description: - path: "../burt_network" - relative: true - source: path + path: "." + ref: HEAD + resolved-ref: ceeb9602a332d612bd9392d2b865403eea76b465 + url: "https://github.com/BinghamtonRover/Dart-Networking.git" + source: git version: "2.0.0" collection: dependency: transitive @@ -240,9 +242,11 @@ packages: opencv_ffi: dependency: "direct main" description: - path: "../opencv_ffi" - relative: true - source: path + path: "." + ref: HEAD + resolved-ref: fb721ce518faa62b470c73ae58edfe825a5f9052 + url: "https://github.com/BinghamtonRover/OpenCV-FFI.git" + source: git version: "1.2.0" package_config: dependency: transitive diff --git a/test/path_test.dart b/test/path_test.dart index e5be9d4..499fe46 100644 --- a/test/path_test.dart +++ b/test/path_test.dart @@ -33,15 +33,16 @@ void main() => group("[Pathfinding]", tags: ["path"], () { var turnCount = 0; for (final step in path) { - if (step.direction == DriveDirection.left || step.direction == DriveDirection.right) { + if (step.direction.isTurn) { turnCount++; } simulator.logger.trace(step.toString()); } // start + 5 forward + 1 turn + 5 right = 12 steps + // start + quarter turn left + 7 forward = 8 steps expect(turnCount, 1); - expect(path.length, 11); + expect(path.length, 7); GpsUtils.maxErrorMeters = oldError; }); @@ -69,13 +70,16 @@ void main() => group("[Pathfinding]", tags: ["path"], () { simulator.pathfinder.recordObstacle((1, 0).toGps()); simulator.pathfinder.recordObstacle((2, 0).toGps()); final path = simulator.pathfinder.getPath(destination); - expect(path, isNotNull); if (path == null) return; + expect(path, isNotNull); + if (path == null) { + return; + } expect(path, isNotEmpty); for (final step in path) { simulator.logger.trace(step.toString()); expect(simulator.pathfinder.isObstacle(step.position), isFalse); } - expect(path.length, 10, reason: "1 Stop + 5 detour + 4 forward = 10 steps total"); + expect(path.length, 10, reason: "1 turn + 1 forward + 1 turn + 4 forward + 1 45 degree turn + 1 forward + 1 stop = 10 steps total"); await simulator.drive.followPath(path); expect(simulator.gps.isNear(destination), isTrue); await simulator.dispose(); From 3fc237c1267dff7f05b4ee9a1c8954cb33006afa Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:08:44 -0500 Subject: [PATCH 002/110] Added turning logic to sensor and timed drive --- lib/src/rover/drive/sensor.dart | 70 +++++++++++++++++++++++---------- lib/src/rover/drive/timed.dart | 16 +++++--- lib/src/rover/sensorless.dart | 12 +++--- 3 files changed, 65 insertions(+), 33 deletions(-) diff --git a/lib/src/rover/drive/sensor.dart b/lib/src/rover/drive/sensor.dart index 7611c28..b019a87 100644 --- a/lib/src/rover/drive/sensor.dart +++ b/lib/src/rover/drive/sensor.dart @@ -69,16 +69,16 @@ class SensorDrive extends DriveInterface with RoverMotors { @override Future turnLeft() async { - await collection.imu.waitForValue(); + await collection.imu.waitForValue(); - if (collection.imu.orientation == null) { - await faceNorth(); - await faceDirection(this.orientation); - } - await collection.imu.waitForValue(); + if (collection.imu.orientation == null) { + await faceNorth(); + await faceDirection(this.orientation); + } + await collection.imu.waitForValue(); final orientation = collection.imu.orientation; - final destination = orientation!.turnLeft(); // do NOT clamp! + final destination = orientation!.turnLeft(); // do NOT clamp! print("Going from ${orientation} to ${destination}"); setThrottle(maxThrottle); setSpeeds(left: -1, right: 1); @@ -89,31 +89,59 @@ class SensorDrive extends DriveInterface with RoverMotors { @override Future turnRight() async { - await collection.imu.waitForValue(); - if (collection.imu.orientation == null) { - await faceNorth(); - await faceDirection(this.orientation); - } - await collection.imu.waitForValue(); + await collection.imu.waitForValue(); + if (collection.imu.orientation == null) { + await faceNorth(); + await faceDirection(this.orientation); + } + await collection.imu.waitForValue(); final orientation = collection.imu.orientation; - final destination = orientation!.turnRight(); // do NOT clamp! + final destination = orientation!.turnRight(); // do NOT clamp! setThrottle(maxThrottle); setSpeeds(left: 1, right: -1); await waitFor(() => collection.imu.orientation == destination); await stop(); - this.orientation = this.orientation.turnRight(); + this.orientation = this.orientation.turnRight(); } @override - Future turnQuarterLeft() { - // TODO: implement turnQuarterLeft - throw UnimplementedError(); + Future turnQuarterLeft() async { + await collection.imu.waitForValue(); + + if (collection.imu.orientation == null) { + await faceNorth(); + await faceDirection(this.orientation); + } + await collection.imu.waitForValue(); + + final orientation = collection.imu.orientation; + final destination = orientation!.turnQuarterLeft(); // do NOT clamp! + print("Going from ${orientation} to ${destination}"); + setThrottle(maxThrottle); + setSpeeds(left: -1, right: 1); + await waitFor(() => collection.imu.orientation == destination); + await stop(); + this.orientation = this.orientation.turnQuarterLeft(); } @override - Future turnQuarterRight() { - // TODO: implement turnQuarterRight - throw UnimplementedError(); + Future turnQuarterRight() async { + await collection.imu.waitForValue(); + + if (collection.imu.orientation == null) { + await faceNorth(); + await faceDirection(this.orientation); + } + await collection.imu.waitForValue(); + + final orientation = collection.imu.orientation; + final destination = orientation!.turnQuarterRight(); // do NOT clamp! + print("Going from ${orientation} to ${destination}"); + setThrottle(maxThrottle); + setSpeeds(left: 1, right: -1); + await waitFor(() => collection.imu.orientation == destination); + await stop(); + this.orientation = this.orientation.turnQuarterRight(); } @override diff --git a/lib/src/rover/drive/timed.dart b/lib/src/rover/drive/timed.dart index 3921bdc..0d03a22 100644 --- a/lib/src/rover/drive/timed.dart +++ b/lib/src/rover/drive/timed.dart @@ -50,14 +50,18 @@ class TimedDrive extends DriveInterface with RoverMotors { } @override - Future turnQuarterLeft() { - // TODO: implement turnQuarterLeft - throw UnimplementedError(); + Future turnQuarterLeft() async { + setThrottle(turnThrottle); + setSpeeds(left: -1, right: 1); + await Future.delayed(turnDelay * 0.5); + await stop(); } @override - Future turnQuarterRight() { - // TODO: implement turnQuarterRight - throw UnimplementedError(); + Future turnQuarterRight() async { + setThrottle(turnThrottle); + setSpeeds(left: 1, right: -1); + await Future.delayed(turnDelay * 0.5); + await stop(); } } diff --git a/lib/src/rover/sensorless.dart b/lib/src/rover/sensorless.dart index fc07345..7a9b7b4 100644 --- a/lib/src/rover/sensorless.dart +++ b/lib/src/rover/sensorless.dart @@ -63,14 +63,14 @@ class SensorlessDrive extends DriveInterface { } @override - Future turnQuarterLeft() { - // TODO: implement turnQuarterLeft - throw UnimplementedError(); + Future turnQuarterLeft() async { + await simulatedDrive.turnQuarterLeft(); + await realDrive.turnQuarterLeft(); } @override - Future turnQuarterRight() { - // TODO: implement turnQuarterRight - throw UnimplementedError(); + Future turnQuarterRight() async { + await simulatedDrive.turnQuarterRight(); + await realDrive.turnQuarterRight(); } } From 131be910f7b88e2abb5744586b76147c06fbef5d Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:27:39 -0500 Subject: [PATCH 003/110] More docs --- lib/src/interfaces/a_star.dart | 32 +++++++++++++++++++++++++++----- lib/src/interfaces/drive.dart | 2 ++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/src/interfaces/a_star.dart b/lib/src/interfaces/a_star.dart index 3f7b9e6..b738c47 100644 --- a/lib/src/interfaces/a_star.dart +++ b/lib/src/interfaces/a_star.dart @@ -65,37 +65,59 @@ class AutonomyAStarState extends AStarState { : depth + 2, ); + /// Returns whether or not the rover will drive between or right next to an obstacle diagonally
+ ///
+ /// Case 1:
+ /// 0 X
+ /// X R
+ /// Assuming the rover is facing 0 and trying to drive forward, will return false
+ ///
+ /// Case 2:
+ /// 0 X
+ /// X R
+ /// Assuming the rover is facing north and trying to turn 45 degrees left, will return false
+ ///
+ /// Case 3:
+ /// 0 X
+ /// 0 R
+ /// If the rover is facing left but trying to turn 45 degrees right, will return false
+ ///
+ /// Case 4:
+ /// 0 X 0
+ /// 0 R 0
+ /// If the rover is facing northeast to 0 and trying to turn left, will return false bool drivingThroughObstacle(AutonomyAStarState state) { final isTurn = state.direction != DriveDirection.forward; final isQuarterTurn = state.direction == DriveDirection.forwardLeft || state.direction == DriveDirection.forwardRight; // Forward drive across the perpendicular axis - if (!isTurn && state.orientation.angle.abs() % 90 == 0) { + if (!isTurn && state.orientation.isPerpendicular) { return false; } // Not encountering any sort of diagonal angle - if (isTurn && isQuarterTurn && state.orientation.angle.abs() % 90 == 0) { + if (isTurn && isQuarterTurn && state.orientation.isPerpendicular) { return false; } // No diagonal movement, won't drive between obstacles - if (!isQuarterTurn && orientation.angle.abs() % 90 == 0) { + if (!isQuarterTurn && orientation.isPerpendicular) { return false; } DriveOrientation orientation1; DriveOrientation orientation2; + // Case 1, trying to drive while facing a 45 degree angle if (!isTurn) { orientation1 = state.orientation.turnQuarterLeft(); orientation2 = state.orientation.turnQuarterRight(); - } else if (isQuarterTurn) { + } else if (isQuarterTurn) { // Case 2 and Case 3 orientation1 = orientation; orientation2 = (state.direction == DriveDirection.forwardLeft) ? orientation1.turnLeft() : orientation1.turnRight(); - } else { + } else { // Case 4 orientation1 = (state.direction == DriveDirection.left) ? orientation.turnQuarterLeft() : orientation.turnQuarterRight(); diff --git a/lib/src/interfaces/drive.dart b/lib/src/interfaces/drive.dart index 03c8d8e..80439b7 100644 --- a/lib/src/interfaces/drive.dart +++ b/lib/src/interfaces/drive.dart @@ -33,6 +33,8 @@ enum DriveOrientation { return null; } + bool get isPerpendicular => angle.abs() % 90 == 0; + DriveOrientation turnLeft() => switch (this) { north => west, west => south, From f7e34f0c1fbde2c20fa4f3331c2c3b74db3ccde5 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 24 Nov 2024 11:26:48 -0500 Subject: [PATCH 004/110] Added network detector --- lib/src/simulator/network_detector.dart | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 lib/src/simulator/network_detector.dart diff --git a/lib/src/simulator/network_detector.dart b/lib/src/simulator/network_detector.dart new file mode 100644 index 0000000..804519f --- /dev/null +++ b/lib/src/simulator/network_detector.dart @@ -0,0 +1,47 @@ +import "package:autonomy/autonomy.dart"; +import "package:burt_network/burt_network.dart"; + +class NetworkDetector extends DetectorInterface { + final List _newObstacles = []; + bool _hasNewObstacles = false; + + NetworkDetector({required super.collection}); + + @override + bool canSeeAruco() => false; + + @override + Future dispose() async {} + + @override + bool findObstacles() { + for (final obstacle in _newObstacles) { + collection.pathfinder.recordObstacle(obstacle); + } + final newObstacles = _hasNewObstacles; + _hasNewObstacles = false; + + _newObstacles.clear(); + + return newObstacles; + } + + @override + Future init() async { + collection.server.messages.onMessage( + name: AutonomyData().messageName, + constructor: AutonomyData.fromBuffer, + callback: _onDataReceived); + return true; + } + + void _onDataReceived(AutonomyData data) { + if (data.obstacles.isNotEmpty) { + _newObstacles.addAll(data.obstacles); + _hasNewObstacles = true; + } + } + + @override + bool isOnSlope() => false; +} From e3e11736af39d4ca210e64ea4e31c3cee34c7d2b Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 24 Nov 2024 11:29:54 -0500 Subject: [PATCH 005/110] Refactored movement units --- bin/latlong.dart | 4 +-- lib/interfaces.dart | 1 + lib/src/interfaces/a_star.dart | 4 +-- lib/src/interfaces/gps_utils.dart | 49 +++++++++++++------------------ lib/src/rover/orchestrator.dart | 9 +++--- 5 files changed, 31 insertions(+), 36 deletions(-) diff --git a/bin/latlong.dart b/bin/latlong.dart index eb5ce07..2c8bbf5 100644 --- a/bin/latlong.dart +++ b/bin/latlong.dart @@ -9,14 +9,14 @@ void printInfo(String name, double latitude) { GpsInterface.currentLatitude = latitude; print("At $name:"); print(" There are ${GpsUtils.metersPerLongitude.toStringAsFixed(2)} meters per 1 degree of longitude"); - print(" Our max error in longitude would be ${GpsUtils.epsilonLongitude} degrees"); + print(" Our max error in longitude would be ${GpsUtils.epsilonLongitude.toStringAsFixed(20)} degrees"); final isWithinRange = GpsInterface.gpsError <= GpsUtils.epsilonLongitude; print(" Our GPS has ${GpsInterface.gpsError} degrees of error, so this would ${isWithinRange ? 'work' : 'not work'}"); } void main() { print("There are always ${GpsUtils.metersPerLatitude} meters in 1 degree of latitude"); - print(" So our max error in latitude is always ${GpsUtils.epsilonLatitude} degrees"); + print(" So our max error in latitude is always ${GpsUtils.epsilonLatitude.toStringAsFixed(20)} degrees"); printInfo("the equator", 0); printInfo("Binghamton", binghamtonLatitude); printInfo("Utah", utahLatitude); diff --git a/lib/interfaces.dart b/lib/interfaces.dart index bd903f0..a6ecf3b 100644 --- a/lib/interfaces.dart +++ b/lib/interfaces.dart @@ -14,3 +14,4 @@ export "src/interfaces/receiver.dart"; export "src/interfaces/reporter.dart"; export "src/interfaces/service.dart"; export "src/interfaces/orchestrator.dart"; +export "package:burt_network/src/utils.dart"; diff --git a/lib/src/interfaces/a_star.dart b/lib/src/interfaces/a_star.dart index b738c47..07bcf0a 100644 --- a/lib/src/interfaces/a_star.dart +++ b/lib/src/interfaces/a_star.dart @@ -50,7 +50,7 @@ class AutonomyAStarState extends AStarState { String hash() => "${position.prettyPrint()} ($orientation)"; @override - bool isGoal() => position.isNear(goal); + bool isGoal() => position.isNear(goal, min(GpsUtils.moveLengthMeters, GpsUtils.maxErrorMeters)); AutonomyAStarState copyWith({required DriveDirection direction, required DriveOrientation orientation, required GpsCoordinates position}) => AutonomyAStarState( collection: collection, @@ -62,7 +62,7 @@ class AutonomyAStarState extends AStarState { ? depth + 1 : (direction == DriveDirection.forwardLeft || direction == DriveDirection.forwardRight) ? depth + sqrt2 - : depth + 2, + : depth + 2 * sqrt2, ); /// Returns whether or not the rover will drive between or right next to an obstacle diagonally
diff --git a/lib/src/interfaces/gps_utils.dart b/lib/src/interfaces/gps_utils.dart index 8a04356..63b3948 100644 --- a/lib/src/interfaces/gps_utils.dart +++ b/lib/src/interfaces/gps_utils.dart @@ -4,38 +4,29 @@ import "dart:math"; import "package:autonomy/interfaces.dart"; import "package:burt_network/generated.dart"; -extension RecordToGps on (num, num) { - GpsCoordinates toGps() => GpsCoordinates(latitude: $1.toDouble() * GpsUtils.latitudePerMeter, longitude: $2.toDouble() * GpsUtils.longitudePerMeter); -} - extension GpsUtils on GpsCoordinates { static double maxErrorMeters = 1; + static double moveLengthMeters = 0.5; static double get epsilonLatitude => maxErrorMeters * latitudePerMeter; - static double get epsilonLongitude => maxErrorMeters * longitudePerMeter; + static double get epsilonLongitude => maxErrorMeters * longitudePerMeter; + + static double get movementLatitude => moveLengthMeters * latitudePerMeter; + static double get movementLongitude => moveLengthMeters * longitudePerMeter; - static GpsCoordinates get east => - GpsCoordinates(longitude: -1 / metersPerLongitude); - static GpsCoordinates get west => - GpsCoordinates(longitude: 1 / metersPerLongitude); - static GpsCoordinates get north => - GpsCoordinates(latitude: 1 * latitudePerMeter); - static GpsCoordinates get south => - GpsCoordinates(latitude: -1 * latitudePerMeter); - static GpsCoordinates get northEast => GpsCoordinates( - latitude: 1 * latitudePerMeter, longitude: -1 / metersPerLongitude); - static GpsCoordinates get northWest => GpsCoordinates( - latitude: 1 * latitudePerMeter, longitude: 1 / metersPerLongitude); - static GpsCoordinates get southEast => GpsCoordinates( - latitude: -1 * latitudePerMeter, longitude: -1 / metersPerLongitude); - static GpsCoordinates get southWest => GpsCoordinates( - latitude: -1 * latitudePerMeter, longitude: 1 / metersPerLongitude); + static GpsCoordinates get east => GpsCoordinates(longitude: -movementLongitude); + static GpsCoordinates get west => GpsCoordinates(longitude: movementLongitude); + static GpsCoordinates get north => GpsCoordinates(latitude: movementLatitude); + static GpsCoordinates get south => GpsCoordinates(latitude: -movementLatitude); + static GpsCoordinates get northEast => GpsCoordinates(latitude: movementLatitude, longitude: -movementLongitude); + static GpsCoordinates get northWest => GpsCoordinates(latitude: movementLatitude, longitude: movementLongitude); + static GpsCoordinates get southEast => GpsCoordinates(latitude: -movementLatitude, longitude: -movementLongitude); + static GpsCoordinates get southWest => GpsCoordinates(latitude: -movementLatitude, longitude: movementLongitude); // Taken from https://stackoverflow.com/a/39540339/9392211 - // static const metersPerLatitude = 111.32 * 1000; // 111.32 km - static const metersPerLatitude = 1; + static const metersPerLatitude = 111.32 * 1000; // 111.32 km + // static const metersPerLatitude = 1; static const radiansPerDegree = pi / 180; - static double get metersPerLongitude => 1; -// 40075 * cos( GpsInterface.currentLatitude * radiansPerDegree ) / 360 * 1000; + static double get metersPerLongitude => 40075 * cos(GpsInterface.currentLatitude * radiansPerDegree) / 360 * 1000.0; static double get latitudePerMeter => 1 / metersPerLatitude; static double get longitudePerMeter => 1 / metersPerLongitude; @@ -49,9 +40,11 @@ extension GpsUtils on GpsCoordinates { (latitude - other.latitude).abs() * metersPerLatitude + (longitude - other.longitude).abs() * metersPerLongitude; - bool isNear(GpsCoordinates other) => - (latitude - other.latitude).abs() < epsilonLatitude && - (longitude - other.longitude).abs() < epsilonLongitude; + bool isNear(GpsCoordinates other, [double? tolerance]) { + tolerance ??= maxErrorMeters; + return (latitude - other.latitude).abs() < tolerance * latitudePerMeter && + (longitude - other.longitude).abs() < tolerance * longitudePerMeter; + } GpsCoordinates operator +(GpsCoordinates other) => GpsCoordinates( latitude: latitude + other.latitude, diff --git a/lib/src/rover/orchestrator.dart b/lib/src/rover/orchestrator.dart index 2000093..a8cbe0c 100644 --- a/lib/src/rover/orchestrator.dart +++ b/lib/src/rover/orchestrator.dart @@ -38,9 +38,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { final destination = command.destination; collection.logger.info("Got GPS Task: Go to ${destination.prettyPrint()}"); collection.logger.debug("Currently at ${collection.gps.coordinates.prettyPrint()}"); + traversed.clear(); collection.drive.setLedStrip(ProtoColor.RED); collection.detector.findObstacles(); - // await collection.drive.faceNorth(); + await collection.drive.faceNorth(); while (!collection.gps.coordinates.isNear(destination)) { // Calculate a path collection.logger.debug("Finding a path"); @@ -57,19 +58,19 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } // Try to take that path final current = collection.gps.coordinates; - collection.logger.info("Found a path from ${current.prettyPrint()} to ${destination.prettyPrint()}: ${path.length} steps"); + collection.logger.debug("Found a path from ${current.prettyPrint()} to ${destination.prettyPrint()}: ${path.length} steps"); collection.logger.debug("Here is a summary of the path"); for (final step in path) { collection.logger.debug(step.toString()); } currentState = AutonomyState.DRIVING; - // var count = 0; + var count = 0; for (final state in path) { collection.logger.debug(state.toString()); await collection.drive.goDirection(state.direction); traversed.add(state.position); // if (state.direction != DriveDirection.forward) continue; - // if (count++ == 5) break; + if (count++ == 5) break; final foundObstacle = collection.detector.findObstacles(); if (foundObstacle) { collection.logger.debug("Found an obstacle. Recalculating path..."); From 40b14410efb6d407f8dbc713e6a0b960f9a44c95 Mon Sep 17 00:00:00 2001 From: BinghamtonRover Date: Sun, 24 Nov 2024 13:06:09 -0500 Subject: [PATCH 006/110] Added tank autonomy file --- bin/tank_autonomy.dart | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 bin/tank_autonomy.dart diff --git a/bin/tank_autonomy.dart b/bin/tank_autonomy.dart new file mode 100644 index 0000000..98e672f --- /dev/null +++ b/bin/tank_autonomy.dart @@ -0,0 +1,23 @@ +import "dart:io"; + +import "package:autonomy/interfaces.dart"; +import "package:autonomy/rover.dart"; +import "package:autonomy/simulator.dart"; +import "package:autonomy/src/simulator/network_detector.dart"; +import "package:burt_network/burt_network.dart"; + +void main() async { + ServerUtils.subsystemsDestination = SocketInfo( + address: InternetAddress("192.168.1.40"), + port: 8001, + ); + final tank = RoverAutonomy(); + tank.detector = NetworkDetector(collection: tank); + tank.gps = GpsSimulator(collection: tank); + tank.imu = ImuSimulator(collection: tank); + tank.drive = SensorlessDrive(collection: tank, useGps: false, useImu: false); + await tank.init(); + await tank.imu.waitForValue(); + + await tank.server.waitForConnection(); +} From 1a877fe73448e9211cf27e46f8cc27b62e5cf163 Mon Sep 17 00:00:00 2001 From: BinghamtonRover Date: Mon, 25 Nov 2024 20:49:06 -0500 Subject: [PATCH 007/110] Tested on tank 11/25 --- bin/tank_autonomy.dart | 6 ++- lib/src/interfaces/a_star.dart | 2 +- lib/src/interfaces/drive.dart | 4 +- lib/src/interfaces/gps_utils.dart | 2 +- lib/src/interfaces/imu_utils.dart | 11 ++--- lib/src/rover/drive/sensor.dart | 74 ++++++++++++++++++++++--------- lib/src/rover/drive/timed.dart | 31 ++++++++++--- lib/src/rover/orchestrator.dart | 2 +- 8 files changed, 93 insertions(+), 39 deletions(-) diff --git a/bin/tank_autonomy.dart b/bin/tank_autonomy.dart index 98e672f..dac6948 100644 --- a/bin/tank_autonomy.dart +++ b/bin/tank_autonomy.dart @@ -3,6 +3,7 @@ import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; import "package:autonomy/simulator.dart"; +import "package:autonomy/src/rover/imu.dart"; import "package:autonomy/src/simulator/network_detector.dart"; import "package:burt_network/burt_network.dart"; @@ -14,8 +15,9 @@ void main() async { final tank = RoverAutonomy(); tank.detector = NetworkDetector(collection: tank); tank.gps = GpsSimulator(collection: tank); - tank.imu = ImuSimulator(collection: tank); - tank.drive = SensorlessDrive(collection: tank, useGps: false, useImu: false); + // tank.imu = ImuSimulator(collection: tank); + tank.imu = RoverImu(collection: tank); + tank.drive = RoverDrive(collection: tank, useGps: false); await tank.init(); await tank.imu.waitForValue(); diff --git a/lib/src/interfaces/a_star.dart b/lib/src/interfaces/a_star.dart index 07bcf0a..a0177d3 100644 --- a/lib/src/interfaces/a_star.dart +++ b/lib/src/interfaces/a_star.dart @@ -29,7 +29,7 @@ class AutonomyAStarState extends AStarState { goal: goal, collection: collection, direction: DriveDirection.stop, - orientation: collection.imu.orientation!, + orientation: collection.imu.orientation ?? DriveOrientation.north, depth: 0, ); diff --git a/lib/src/interfaces/drive.dart b/lib/src/interfaces/drive.dart index 80439b7..f9b6c96 100644 --- a/lib/src/interfaces/drive.dart +++ b/lib/src/interfaces/drive.dart @@ -1,6 +1,8 @@ import "package:autonomy/interfaces.dart"; import "package:burt_network/generated.dart"; +const bool isRover = false; + enum DriveDirection { forward, left, @@ -28,7 +30,7 @@ enum DriveOrientation { static DriveOrientation? fromRaw(Orientation orientation) { // TODO: Make this more precise. for (final value in values) { - if (orientation.isNear(value.angle.toDouble())) return value; + if (orientation.isNear(value.angle.toDouble(), OrientationUtils.orientationEpsilon)) return value; } return null; } diff --git a/lib/src/interfaces/gps_utils.dart b/lib/src/interfaces/gps_utils.dart index 63b3948..83d4711 100644 --- a/lib/src/interfaces/gps_utils.dart +++ b/lib/src/interfaces/gps_utils.dart @@ -6,7 +6,7 @@ import "package:burt_network/generated.dart"; extension GpsUtils on GpsCoordinates { static double maxErrorMeters = 1; - static double moveLengthMeters = 0.5; + static double moveLengthMeters = 1; static double get epsilonLatitude => maxErrorMeters * latitudePerMeter; static double get epsilonLongitude => maxErrorMeters * longitudePerMeter; diff --git a/lib/src/interfaces/imu_utils.dart b/lib/src/interfaces/imu_utils.dart index 96d928e..c0f8a37 100644 --- a/lib/src/interfaces/imu_utils.dart +++ b/lib/src/interfaces/imu_utils.dart @@ -1,7 +1,8 @@ import "package:burt_network/generated.dart"; extension OrientationUtils on Orientation { - static const double epsilon = 10; + static const double epsilon = 3.5; + static const double orientationEpsilon = 10; static final north = Orientation(z: 0); static final west = Orientation(z: 90); @@ -19,9 +20,9 @@ extension OrientationUtils on Orientation { return Orientation(x: x, y: y, z: adjustedHeading); } - bool isNear(double value) => value > 270 && z < 90 - ? (z + 360 - value).abs() < epsilon + bool isNear(double value, [double tolerance = epsilon]) => value > 270 && z < 90 + ? (z + 360 - value).abs() < tolerance : value < 90 && z > 270 - ? (z - value - 360).abs() < epsilon - : (z - value).abs() < epsilon; + ? (z - value - 360).abs() < tolerance + : (z - value).abs() < tolerance; } diff --git a/lib/src/rover/drive/sensor.dart b/lib/src/rover/drive/sensor.dart index b019a87..adaab3f 100644 --- a/lib/src/rover/drive/sensor.dart +++ b/lib/src/rover/drive/sensor.dart @@ -5,7 +5,11 @@ import "motors.dart"; class SensorDrive extends DriveInterface with RoverMotors { static const double maxThrottle = 0.1; - static const double turnThrottle = 0.1; + static const double turnThrottleRover = 0.1; + static const double turnThrottleTank = 0.35; + + static double get turnThrottle => isRover ? turnThrottleRover : turnThrottleTank; + static const predicateDelay = Duration(milliseconds: 100); static const turnDelay = Duration(milliseconds: 1500); @@ -24,6 +28,12 @@ class SensorDrive extends DriveInterface with RoverMotors { } } + Future runFeedback(bool Function() completed, [Duration period = predicateDelay]) async { + while (!completed()) { + await Future.delayed(period); + } + } + @override Future init() async => true; @@ -46,11 +56,12 @@ class SensorDrive extends DriveInterface with RoverMotors { Future faceNorth() async { collection.logger.info("Turning to face north..."); setThrottle(turnThrottle); - setSpeeds(left: -1, right: 1); - await waitFor(() { - collection.logger.trace("Current heading: ${collection.imu.heading}"); - return collection.imu.raw.isNear(0); - }); + // setSpeeds(left: -1, right: 1); + // await waitFor(() { + // collection.logger.trace("Current heading: ${collection.imu.heading}"); + // return collection.imu.raw.isNear(0, OrientationUtils.orientationEpsilon); + // }); + await faceDirection(DriveOrientation.north); await stop(); } @@ -59,10 +70,25 @@ class SensorDrive extends DriveInterface with RoverMotors { collection.logger.info("Turning to face $orientation..."); setThrottle(turnThrottle); setSpeeds(left: -1, right: 1); - await waitFor(() { - collection.logger.trace("Current heading: ${collection.imu.heading}"); - return collection.imu.raw.isNear(orientation.angle.toDouble()); - }); + await runFeedback( + () { + var delta = orientation.angle.toDouble() - collection.imu.raw.z; + if (delta < -180) { + delta += 360; + } else if (delta > 180) { + delta -= 360; + } + + if (delta < 0) { + setSpeeds(left: 1, right: -1); + } else { + setSpeeds(left: -1, right: 1); + } + collection.logger.trace("Current heading: ${collection.imu.heading}"); + return collection.imu.raw.isNear(orientation.angle.toDouble()); + }, + const Duration(milliseconds: 10), + ); await stop(); await super.faceDirection(orientation); } @@ -80,9 +106,10 @@ class SensorDrive extends DriveInterface with RoverMotors { final orientation = collection.imu.orientation; final destination = orientation!.turnLeft(); // do NOT clamp! print("Going from ${orientation} to ${destination}"); - setThrottle(maxThrottle); - setSpeeds(left: -1, right: 1); - await waitFor(() => collection.imu.orientation == destination); + setThrottle(turnThrottle); + // setSpeeds(left: -1, right: 1); + // await waitFor(() => collection.imu.orientation == destination); + await faceDirection(destination); await stop(); this.orientation = this.orientation.turnLeft(); } @@ -97,9 +124,10 @@ class SensorDrive extends DriveInterface with RoverMotors { await collection.imu.waitForValue(); final orientation = collection.imu.orientation; final destination = orientation!.turnRight(); // do NOT clamp! - setThrottle(maxThrottle); - setSpeeds(left: 1, right: -1); - await waitFor(() => collection.imu.orientation == destination); + setThrottle(turnThrottle); + // setSpeeds(left: 1, right: -1); + // await waitFor(() => collection.imu.orientation == destination); + await faceDirection(destination); await stop(); this.orientation = this.orientation.turnRight(); } @@ -117,9 +145,10 @@ class SensorDrive extends DriveInterface with RoverMotors { final orientation = collection.imu.orientation; final destination = orientation!.turnQuarterLeft(); // do NOT clamp! print("Going from ${orientation} to ${destination}"); - setThrottle(maxThrottle); - setSpeeds(left: -1, right: 1); - await waitFor(() => collection.imu.orientation == destination); + setThrottle(turnThrottle); + // setSpeeds(left: -1, right: 1); + // await waitFor(() => collection.imu.orientation == destination); + await faceDirection(destination); await stop(); this.orientation = this.orientation.turnQuarterLeft(); } @@ -137,9 +166,10 @@ class SensorDrive extends DriveInterface with RoverMotors { final orientation = collection.imu.orientation; final destination = orientation!.turnQuarterRight(); // do NOT clamp! print("Going from ${orientation} to ${destination}"); - setThrottle(maxThrottle); - setSpeeds(left: 1, right: -1); - await waitFor(() => collection.imu.orientation == destination); + setThrottle(turnThrottle); + // setSpeeds(left: 1, right: -1); + // await waitFor(() => collection.imu.orientation == destination); + await faceDirection(destination); await stop(); this.orientation = this.orientation.turnQuarterRight(); } diff --git a/lib/src/rover/drive/timed.dart b/lib/src/rover/drive/timed.dart index 0d03a22..bba9d6e 100644 --- a/lib/src/rover/drive/timed.dart +++ b/lib/src/rover/drive/timed.dart @@ -3,10 +3,23 @@ import "package:autonomy/interfaces.dart"; import "motors.dart"; class TimedDrive extends DriveInterface with RoverMotors { - static const maxThrottle = 0.1; - static const turnThrottle = 0.1; - static const oneMeterDelay = Duration(milliseconds: 5500); - static const turnDelay = Duration(milliseconds: 4500); + static const maxThrottleTank = 0.3; + static const turnThrottleTank = 0.35; + + static const maxThrottleRover = 0.1; + static const turnThrottleRover = 0.1; + + static const oneMeterDelayRover = Duration(milliseconds: 5500); + static const turnDelayRover = Duration(milliseconds: 4500); + + static const oneMeterDelayTank = Duration(milliseconds: 2000); + static const turnDelayTank = Duration(milliseconds: 1000); + + static double get maxThrottle => isRover ? maxThrottleRover : maxThrottleTank; + static double get turnThrottle => isRover ? turnThrottleRover : turnThrottleTank; + + static Duration get oneMeterDelay => isRover ? oneMeterDelayRover : oneMeterDelayTank; + static Duration get turnDelay => isRover ? turnDelayRover : turnDelayTank; TimedDrive({required super.collection}); @@ -28,7 +41,13 @@ class TimedDrive extends DriveInterface with RoverMotors { @override Future goForward() async { setThrottle(maxThrottle); - setSpeeds(left: 1, right: 1); + setSpeeds(left: 1 * 0.9, right: 1); + final position = collection.gps.coordinates; + final orientation = collection.imu.orientation; + if (orientation != null) { + final newPosition = position.goForward(orientation); + collection.gps.update(newPosition); + } await Future.delayed(oneMeterDelay); await stop(); } @@ -36,7 +55,7 @@ class TimedDrive extends DriveInterface with RoverMotors { @override Future turnLeft() async { setThrottle(turnThrottle); - setSpeeds(left: -1, right: 1); + setSpeeds(left: -1 * 0.9, right: 1); await Future.delayed(turnDelay); await stop(); } diff --git a/lib/src/rover/orchestrator.dart b/lib/src/rover/orchestrator.dart index a8cbe0c..c33e1e9 100644 --- a/lib/src/rover/orchestrator.dart +++ b/lib/src/rover/orchestrator.dart @@ -41,7 +41,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { traversed.clear(); collection.drive.setLedStrip(ProtoColor.RED); collection.detector.findObstacles(); - await collection.drive.faceNorth(); + // await collection.drive.faceNorth(); while (!collection.gps.coordinates.isNear(destination)) { // Calculate a path collection.logger.debug("Finding a path"); From 315c3ed19cd2637557706405855834c4a6b20740 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:20:10 -0500 Subject: [PATCH 008/110] Fixed all occurances of toGps() --- bin/path.dart | 2 +- lib/src/simulator/pathfinding.dart | 20 ++++++++++---------- test/network_test.dart | 2 +- test/orchestrator_test.dart | 30 +++++++++++++++--------------- test/path_test.dart | 22 +++++++++++----------- test/rover_test.dart | 14 +++++++------- test/sensor_test.dart | 12 ++++++------ 7 files changed, 51 insertions(+), 51 deletions(-) diff --git a/bin/path.dart b/bin/path.dart index cf0b5be..58fe8ba 100644 --- a/bin/path.dart +++ b/bin/path.dart @@ -4,7 +4,7 @@ import "package:autonomy/simulator.dart"; void main() { GpsUtils.maxErrorMeters = 1; - final destination = (1000, 1000).toGps(); + final destination = (lat: 1000, long: 1000).toGps(); final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); final path = simulator.pathfinder.getPath(destination); diff --git a/lib/src/simulator/pathfinding.dart b/lib/src/simulator/pathfinding.dart index 8c64b6a..bce45b6 100644 --- a/lib/src/simulator/pathfinding.dart +++ b/lib/src/simulator/pathfinding.dart @@ -11,15 +11,15 @@ class PathfindingSimulator extends PathfindingInterface { @override List getPath(GpsCoordinates destination, {bool verbose = false}) => [ - AutonomyAStarState(depth: i++, goal: (2, 1).toGps(), position: (0, 0).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.right, collection: collection), - AutonomyAStarState(depth: i++, goal: (2, 1).toGps(), position: (0, 0).toGps(), orientation: DriveOrientation.east, direction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (2, 1).toGps(), position: (0, 1).toGps(), orientation: DriveOrientation.east, direction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (2, 1).toGps(), position: (0, 2).toGps(), orientation: DriveOrientation.east, direction: DriveDirection.left, collection: collection), - AutonomyAStarState(depth: i++, goal: (2, 1).toGps(), position: (0, 2).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (2, 1).toGps(), position: (1, 2).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (2, 1).toGps(), position: (2, 2).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.left, collection: collection), - AutonomyAStarState(depth: i++, goal: (2, 1).toGps(), position: (2, 2).toGps(), orientation: DriveOrientation.west, direction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (2, 1).toGps(), position: (2, 1).toGps(), orientation: DriveOrientation.west, direction: DriveDirection.right, collection: collection), - AutonomyAStarState(depth: i++, goal: (2, 1).toGps(), position: (2, 1).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.forward, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 0).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.right, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 0).toGps(), orientation: DriveOrientation.east, direction: DriveDirection.forward, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 1).toGps(), orientation: DriveOrientation.east, direction: DriveDirection.forward, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 2).toGps(), orientation: DriveOrientation.east, direction: DriveDirection.left, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 2).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.forward, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 1, long: 2).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.forward, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 2).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.left, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 2).toGps(), orientation: DriveOrientation.west, direction: DriveDirection.forward, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 1).toGps(), orientation: DriveOrientation.west, direction: DriveDirection.right, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 1).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.forward, collection: collection), ]; } diff --git a/test/network_test.dart b/test/network_test.dart index 8e482df..dc5a1fd 100644 --- a/test/network_test.dart +++ b/test/network_test.dart @@ -110,7 +110,7 @@ void main() => group("[Network]", tags: ["network"], () { expect(simulator.imu.orientation, DriveOrientation.north); final origin = GpsCoordinates(latitude: 0, longitude: 0); - final oneMeter = (1, 0).toGps(); + final oneMeter = (lat: 1, long: 0).toGps(); expect(subsystems.throttle, 0); expect(subsystems.left, 0); expect(subsystems.right, 0); diff --git a/test/orchestrator_test.dart b/test/orchestrator_test.dart index 27549f5..7144f89 100644 --- a/test/orchestrator_test.dart +++ b/test/orchestrator_test.dart @@ -13,9 +13,9 @@ void main() => group("[Orchestrator]", tags: ["orchestrator"], () { await simulator.init(); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.orchestrator = RoverOrchestrator(collection: simulator); - simulator.pathfinder.recordObstacle((2, 0).toGps()); + simulator.pathfinder.recordObstacle((lat: 2, long: 0).toGps()); // Test blocked command: - final command = AutonomyCommand(destination: (2, 0).toGps(), task: AutonomyTask.GPS_ONLY); + final command = AutonomyCommand(destination: (lat: 2, long: 0).toGps(), task: AutonomyTask.GPS_ONLY); expect(simulator.gps.latitude, 0); expect(simulator.gps.longitude, 0); expect(simulator.imu.heading, 0); @@ -37,13 +37,13 @@ void main() => group("[Orchestrator]", tags: ["orchestrator"], () { simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.orchestrator = RoverOrchestrator(collection: simulator); simulator.pathfinder.obstacles.addAll([ - (2, 0).toGps(), - (4, -1).toGps(), - (4, 1).toGps(), + (lat: 2, long: 0).toGps(), + (lat: 4, long: -1).toGps(), + (lat: 4, long: 1).toGps(), ]); await simulator.init(); // Test normal command: - final destination = (4, 0).toGps(); + final destination = (lat: 4, long: 0).toGps(); final command = AutonomyCommand(destination: destination, task: AutonomyTask.GPS_ONLY); expect(simulator.gps.latitude, 0); expect(simulator.gps.longitude, 0); @@ -56,9 +56,9 @@ void main() => group("[Orchestrator]", tags: ["orchestrator"], () { expect(status1.task, AutonomyTask.AUTONOMY_TASK_UNDEFINED); expect(status1.destination, GpsCoordinates()); expect(status1.obstacles, [ - (2, 0).toGps(), - (4, -1).toGps(), - (4, 1).toGps(), + (lat: 2, long: 0).toGps(), + (lat: 4, long: -1).toGps(), + (lat: 4, long: 1).toGps(), ]); expect(status1.state, AutonomyState.AT_DESTINATION); await simulator.dispose(); @@ -68,9 +68,9 @@ void main() => group("[Orchestrator]", tags: ["orchestrator"], () { Logger.level = LogLevel.off; final simulator = AutonomySimulator(); final obstacles = [ - SimulatedObstacle(coordinates: (2, 0).toGps(), radius: 1), - SimulatedObstacle(coordinates: (6, -1).toGps(), radius: 1), - SimulatedObstacle(coordinates: (6, 1).toGps(), radius: 1), + SimulatedObstacle(coordinates: (lat: 2, long: 0).toGps(), radius: 1), + SimulatedObstacle(coordinates: (lat: 6, long: -1).toGps(), radius: 1), + SimulatedObstacle(coordinates: (lat: 6, long: 1).toGps(), radius: 1), ]; simulator.detector = DetectorSimulator(collection: simulator, obstacles: obstacles); simulator.pathfinder = RoverPathfinder(collection: simulator); @@ -78,7 +78,7 @@ void main() => group("[Orchestrator]", tags: ["orchestrator"], () { simulator.drive = DriveSimulator(collection: simulator); await simulator.init(); final origin = GpsCoordinates(); - final destination = (0, 7).toGps(); + final destination = (lat: 0, long: 7).toGps(); expect(simulator.gps.isNear(origin), isTrue); expect(simulator.imu.heading, 0); final command = AutonomyCommand(destination: destination, task: AutonomyTask.GPS_ONLY); @@ -89,8 +89,8 @@ void main() => group("[Orchestrator]", tags: ["orchestrator"], () { test("Rejects commands until latitude has been determined", () async { final simulator = AutonomySimulator(); - final start = (5, 0).toGps(); - final destination = (5, 5).toGps(); + final start = (lat: 5, long: 0).toGps(); + final destination = (lat: 5, long: 5).toGps(); final command = AutonomyCommand(destination: destination, task: AutonomyTask.GPS_ONLY); simulator.orchestrator = RoverOrchestrator(collection: simulator); simulator.pathfinder = RoverPathfinder(collection: simulator); diff --git a/test/path_test.dart b/test/path_test.dart index 499fe46..9f43d99 100644 --- a/test/path_test.dart +++ b/test/path_test.dart @@ -11,7 +11,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("Simple path from (0, 0) to (5, 5) exists", () { final simulator = AutonomySimulator(); - final destination = (5, 5).toGps(); + final destination = (lat: 5, long: 5).toGps(); simulator.logger.info("Each step is ${GpsUtils.north.latitude.toStringAsFixed(5)}"); simulator.logger.info("Going to ${destination.prettyPrint()}"); simulator.pathfinder = RoverPathfinder(collection: simulator); @@ -26,7 +26,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { // Plan a path from (0, 0) to (5, 5) simulator.pathfinder = RoverPathfinder(collection: simulator); - final destination = (5, 5).toGps(); + final destination = (lat: 5, long: 5).toGps(); simulator.logger.info("Going to ${destination.prettyPrint()}"); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); if (path == null) return; @@ -49,7 +49,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("Following path gets to the end", () async { final simulator = AutonomySimulator(); - final destination = (5, 5).toGps(); + final destination = (lat: 5, long: 5).toGps(); simulator.pathfinder = RoverPathfinder(collection: simulator); final path = simulator.pathfinder.getPath(destination); @@ -65,10 +65,10 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("Avoid obstacles but reach the goal", () async { // Logger.level = LogLevel.all; final simulator = AutonomySimulator(); - final destination = (5, 0).toGps(); + final destination = (lat: 5, long: 0).toGps(); simulator.pathfinder = RoverPathfinder(collection: simulator); - simulator.pathfinder.recordObstacle((1, 0).toGps()); - simulator.pathfinder.recordObstacle((2, 0).toGps()); + simulator.pathfinder.recordObstacle((lat: 1, long: 0).toGps()); + simulator.pathfinder.recordObstacle((lat: 2, long: 0).toGps()); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); if (path == null) { @@ -92,7 +92,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.logger.trace("Starting from ${simulator.gps.coordinates.prettyPrint()}"); simulator.logger.trace("Each step is +/- ${GpsUtils.north.prettyPrint()}"); - final destination = (1000, 1000).toGps(); + final destination = (lat: 1000, long: 1000).toGps(); simulator.logger.info("Going to ${destination.prettyPrint()}"); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); @@ -103,11 +103,11 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("Impossible paths are reported", () async { final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); - final destination = (5, 5).toGps(); + final destination = (lat: 5, long: 5).toGps(); final obstacles = { - (1, -1).toGps(), (1, 0).toGps(), (1, 1).toGps(), - (0, -1).toGps(), /* Rover */ (0, 1).toGps(), - (-1, -1).toGps(), (-1, 0).toGps(), (-1, 1).toGps(), + (lat: 1, long: -1).toGps(), (lat: 1, long: 0).toGps(), (lat: 1, long: 1).toGps(), + (lat: 0, long: -1).toGps(), /* Rover */ (lat: 0, long: 1).toGps(), + (lat: -1, long: -1).toGps(), (lat: -1, long: 0).toGps(), (lat: -1, long: 1).toGps(), }; for (final obstacle in obstacles) { simulator.pathfinder.recordObstacle(obstacle); diff --git a/test/rover_test.dart b/test/rover_test.dart index 2f40af5..2fb4bbb 100644 --- a/test/rover_test.dart +++ b/test/rover_test.dart @@ -21,7 +21,7 @@ void main() => group("[Rover]", tags: ["rover"], () { final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); await testPath(simulator); - simulator.gps.update((0, 0).toGps()); + simulator.gps.update((lat: 0, long: 0).toGps()); simulator.imu.update(Orientation()); await testPath2(simulator); await simulator.dispose(); @@ -29,7 +29,7 @@ void main() => group("[Rover]", tags: ["rover"], () { test("Waits for sensor data", () async { final rover = RoverAutonomy(); - final position = (5, 5).toGps(); + final position = (lat: 5, long: 5).toGps(); final orientation = Orientation(); final data = VideoData(); @@ -58,7 +58,7 @@ void main() => group("[Rover]", tags: ["rover"], () { }); Future testPath(AutonomyInterface simulator) async { - final destination = (5, 5).toGps(); + final destination = (lat: 5, long: 5).toGps(); final result = simulator.pathfinder.getPath(destination); expect(simulator.gps.latitude, 0); expect(simulator.gps.longitude, 0); @@ -78,10 +78,10 @@ Future testPath(AutonomyInterface simulator) async { Future testPath2(AutonomyInterface simulator) async { // Logger.level = LogLevel.all; - final destination = (4, 0).toGps(); - simulator.pathfinder.recordObstacle((2, 0).toGps()); - simulator.pathfinder.recordObstacle((4, -1).toGps()); - simulator.pathfinder.recordObstacle((4, 1).toGps()); + final destination = (lat: 4, long: 0).toGps(); + simulator.pathfinder.recordObstacle((lat: 2, long: 0).toGps()); + simulator.pathfinder.recordObstacle((lat: 4, long: -1).toGps()); + simulator.pathfinder.recordObstacle((lat: 4, long: 1).toGps()); final result = simulator.pathfinder.getPath(destination); expect(simulator.gps.latitude, 0); expect(simulator.gps.longitude, 0); diff --git a/test/sensor_test.dart b/test/sensor_test.dart index 623e2cb..017662a 100644 --- a/test/sensor_test.dart +++ b/test/sensor_test.dart @@ -16,13 +16,13 @@ void main() => group("[Sensors]", tags: ["sensors"], () { tearDown(() => Logger.level = LogLevel.off); test("GpsUtils.isNear", () { - final origin = (0, 0).toGps(); + final origin = (lat: 0, long: 0).toGps(); expect(GpsCoordinates(latitude: 0, longitude: 0), origin); expect(origin.isNear(origin), isTrue); - final a = (5, 5).toGps(); - final a2 = (5, 5).toGps(); - final b = (5, 6.5).toGps(); + final a = (lat: 5, long: 5).toGps(); + final a2 = (lat: 5, long: 5).toGps(); + final b = (lat: 5, long: 6.5).toGps(); expect(a.isNear(a), isTrue); expect(a.isNear(a2), isTrue); @@ -36,8 +36,8 @@ void main() => group("[Sensors]", tags: ["sensors"], () { expect(c.isNear(e), isFalse); expect(d.isNear(e), isFalse); - final f = (12, 12).toGps(); - final g = (12.2, 12.2).toGps(); + final f = (lat: 12, long: 12).toGps(); + final g = (lat: 12.2, long: 12.2).toGps(); expect(f.isNear(g), isTrue); }); From e480c377b6406325490b45cb9ed3a0040cf668f7 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:21:52 -0500 Subject: [PATCH 009/110] Fixed toGps() in task.dart --- bin/task.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/task.dart b/bin/task.dart index 798289b..e51da7b 100644 --- a/bin/task.dart +++ b/bin/task.dart @@ -3,10 +3,10 @@ import "package:autonomy/simulator.dart"; import "package:autonomy/rover.dart"; import "package:burt_network/logging.dart"; -final chair = (2, 0).toGps(); +final chair = (lat: 2, long: 0).toGps(); final obstacles = [ - SimulatedObstacle(coordinates: (6, -1).toGps(), radius: 3), - SimulatedObstacle(coordinates: (6, 1).toGps(), radius: 3), + SimulatedObstacle(coordinates: (lat: 6, long: -1).toGps(), radius: 3), + SimulatedObstacle(coordinates: (lat: 6, long: 1).toGps(), radius: 3), ]; // Enter in the Dashboard: Destination = (lat=7, long=0); From a41b18441cf14f846ed5a86935a0c36741d6a507 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:44:08 -0500 Subject: [PATCH 010/110] Specified networking library version --- pubspec.lock | 8 ++++---- pubspec.yaml | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 7366b66..69567f5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -58,11 +58,11 @@ packages: dependency: "direct main" description: path: "." - ref: HEAD - resolved-ref: ceeb9602a332d612bd9392d2b865403eea76b465 - url: "https://github.com/BinghamtonRover/Dart-Networking.git" + ref: "2.2.0" + resolved-ref: "7ec80052bd9d2777782f7ce44cfcfc1e69e8a582" + url: "https://github.com/BinghamtonRover/Networking.git" source: git - version: "2.0.0" + version: "2.2.0" collection: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c117b53..daf64d3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,9 @@ dependencies: opencv_ffi: git: https://github.com/BinghamtonRover/OpenCV-FFI.git burt_network: - git: https://github.com/BinghamtonRover/Dart-Networking.git + git: + url: https://github.com/BinghamtonRover/Networking.git + ref: 2.2.0 a_star: ^3.0.0 meta: ^1.11.0 From e19dae721d1c10e38965deb17006ff83bdc18728 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:14:23 -0500 Subject: [PATCH 011/110] Fixed isNear method --- lib/src/interfaces/gps_utils.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/src/interfaces/gps_utils.dart b/lib/src/interfaces/gps_utils.dart index 83d4711..dc6c19e 100644 --- a/lib/src/interfaces/gps_utils.dart +++ b/lib/src/interfaces/gps_utils.dart @@ -42,8 +42,17 @@ extension GpsUtils on GpsCoordinates { bool isNear(GpsCoordinates other, [double? tolerance]) { tolerance ??= maxErrorMeters; - return (latitude - other.latitude).abs() < tolerance * latitudePerMeter && - (longitude - other.longitude).abs() < tolerance * longitudePerMeter; + final currentMeters = inMeters; + final otherMeters = other.inMeters; + + final (deltaX, deltaY) = ( + currentMeters.lat - otherMeters.lat, + currentMeters.long - otherMeters.long + ); + + final distance = sqrt(pow(deltaX, 2) + pow(deltaY, 2)); + + return distance < tolerance; } GpsCoordinates operator +(GpsCoordinates other) => GpsCoordinates( From c0d71ae941a8dc90eb766c5728e7ec214d34a886 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 8 Dec 2024 03:57:58 -0500 Subject: [PATCH 012/110] Refactor and Reorganization (#7) Co-authored-by: Binghamton Rover Co-authored-by: Levi Lesches --- .vscode/settings.json | 5 + analysis_options.yaml | 17 +- bin/arcuo.dart | 2 +- bin/path.dart | 2 +- bin/random.dart | 3 +- bin/sensorless.dart | 115 +++++---- bin/tank_autonomy.dart | 25 -- bin/task.dart | 5 +- bin/test.dart | 42 ++-- lib/interfaces.dart | 29 +-- lib/rover.dart | 29 ++- lib/simulator.dart | 21 +- .../autonomy.dart => autonomy_interface.dart} | 13 +- .../detector_interface.dart} | 0 .../network_detector.dart | 10 +- .../rover_detector.dart} | 2 +- .../sim_detector.dart} | 7 +- lib/src/drive/drive_commands.dart | 39 ++++ lib/src/drive/drive_config.dart | 40 ++++ lib/src/drive/drive_interface.dart | 55 +++++ lib/src/drive/rover_drive.dart | 88 +++++++ lib/src/drive/sensor_drive.dart | 87 +++++++ lib/src/drive/sim_drive.dart | 29 +++ lib/src/drive/timed_drive.dart | 79 +++++++ .../gps.dart => gps/gps_interface.dart} | 1 - lib/src/gps/rover_gps.dart | 42 ++++ .../{simulator/gps.dart => gps/sim_gps.dart} | 1 - lib/src/imu/cardinal_direction.dart | 79 +++++++ .../imu.dart => imu/imu_interface.dart} | 10 +- lib/src/imu/rover_imu.dart | 43 ++++ .../{simulator/imu.dart => imu/sim_imu.dart} | 9 +- lib/src/interfaces/a_star.dart | 167 ------------- lib/src/interfaces/drive.dart | 125 ---------- lib/src/interfaces/imu_utils.dart | 28 --- lib/src/interfaces/receiver.dart | 9 - lib/src/interfaces/server.dart | 25 -- lib/src/interfaces/service.dart | 1 - .../orchestrator_interface.dart} | 12 +- .../rover_orchestrator.dart} | 8 +- .../sim_orchestrator.dart} | 1 - .../pathfinding_interface.dart} | 1 - .../rover_pathfinding.dart} | 6 +- lib/src/pathfinding/sim_pathfinding.dart | 24 ++ lib/src/rover/drive.dart | 73 ------ lib/src/rover/drive/motors.dart | 26 --- lib/src/rover/drive/sensor.dart | 219 ------------------ lib/src/rover/drive/timed.dart | 86 ------- lib/src/rover/gps.dart | 41 ---- lib/src/rover/imu.dart | 44 ---- lib/src/rover/sensorless.dart | 76 ------ lib/src/simulator/drive.dart | 73 ------ lib/src/simulator/pathfinding.dart | 25 -- lib/src/utils/a_star.dart | 184 +++++++++++++++ lib/src/{rover => utils}/corrector.dart | 9 +- lib/src/{interfaces => utils}/error.dart | 0 lib/src/{interfaces => utils}/gps_utils.dart | 35 ++- lib/src/utils/imu_utils.dart | 29 +++ lib/src/utils/receiver.dart | 21 ++ lib/src/{interfaces => utils}/reporter.dart | 3 +- .../video.dart => video/rover_video.dart} | 11 +- .../realsense.dart => video/sim_video.dart} | 5 +- .../video.dart => video/video_interface.dart} | 5 +- lib/utils.dart | 9 + pubspec.lock | 19 +- pubspec.yaml | 6 +- test/network_test.dart | 3 - test/orchestrator_test.dart | 1 - test/path_test.dart | 2 +- test/rover_test.dart | 16 +- test/sensor_test.dart | 18 +- 70 files changed, 1065 insertions(+), 1310 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 bin/tank_autonomy.dart rename lib/src/{interfaces/autonomy.dart => autonomy_interface.dart} (85%) rename lib/src/{interfaces/detector.dart => detector/detector_interface.dart} (100%) rename lib/src/{simulator => detector}/network_detector.dart (79%) rename lib/src/{rover/detector.dart => detector/rover_detector.dart} (89%) rename lib/src/{simulator/detector.dart => detector/sim_detector.dart} (88%) create mode 100644 lib/src/drive/drive_commands.dart create mode 100644 lib/src/drive/drive_config.dart create mode 100644 lib/src/drive/drive_interface.dart create mode 100644 lib/src/drive/rover_drive.dart create mode 100644 lib/src/drive/sensor_drive.dart create mode 100644 lib/src/drive/sim_drive.dart create mode 100644 lib/src/drive/timed_drive.dart rename lib/src/{interfaces/gps.dart => gps/gps_interface.dart} (93%) create mode 100644 lib/src/gps/rover_gps.dart rename lib/src/{simulator/gps.dart => gps/sim_gps.dart} (93%) create mode 100644 lib/src/imu/cardinal_direction.dart rename lib/src/{interfaces/imu.dart => imu/imu_interface.dart} (57%) create mode 100644 lib/src/imu/rover_imu.dart rename lib/src/{simulator/imu.dart => imu/sim_imu.dart} (77%) delete mode 100644 lib/src/interfaces/a_star.dart delete mode 100644 lib/src/interfaces/drive.dart delete mode 100644 lib/src/interfaces/imu_utils.dart delete mode 100644 lib/src/interfaces/receiver.dart delete mode 100644 lib/src/interfaces/server.dart delete mode 100644 lib/src/interfaces/service.dart rename lib/src/{interfaces/orchestrator.dart => orchestrator/orchestrator_interface.dart} (83%) rename lib/src/{rover/orchestrator.dart => orchestrator/rover_orchestrator.dart} (93%) rename lib/src/{simulator/orchestrator.dart => orchestrator/sim_orchestrator.dart} (92%) rename lib/src/{interfaces/pathfinding.dart => pathfinding/pathfinding_interface.dart} (92%) rename lib/src/{rover/pathfinding.dart => pathfinding/rover_pathfinding.dart} (73%) create mode 100644 lib/src/pathfinding/sim_pathfinding.dart delete mode 100644 lib/src/rover/drive.dart delete mode 100644 lib/src/rover/drive/motors.dart delete mode 100644 lib/src/rover/drive/sensor.dart delete mode 100644 lib/src/rover/drive/timed.dart delete mode 100644 lib/src/rover/gps.dart delete mode 100644 lib/src/rover/imu.dart delete mode 100644 lib/src/rover/sensorless.dart delete mode 100644 lib/src/simulator/drive.dart delete mode 100644 lib/src/simulator/pathfinding.dart create mode 100644 lib/src/utils/a_star.dart rename lib/src/{rover => utils}/corrector.dart (88%) rename lib/src/{interfaces => utils}/error.dart (100%) rename lib/src/{interfaces => utils}/gps_utils.dart (72%) create mode 100644 lib/src/utils/imu_utils.dart create mode 100644 lib/src/utils/receiver.dart rename lib/src/{interfaces => utils}/reporter.dart (86%) rename lib/src/{rover/video.dart => video/rover_video.dart} (67%) rename lib/src/{simulator/realsense.dart => video/sim_video.dart} (89%) rename lib/src/{interfaces/video.dart => video/video_interface.dart} (71%) create mode 100644 lib/utils.dart diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..14029d2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "setpoint" + ] +} \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 76649d1..0f5506c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,23 +9,24 @@ include: package:very_good_analysis/analysis_options.yaml # has more lints analyzer: language: - # Strict casts isn't helpful with null safety. It only notifies you on `dynamic`, - # which happens all the time in JSON. - # + # Strict casts isn't helpful with null safety. It only notifies you on `dynamic`, + # which happens all the time in JSON. + # # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-casts.md strict-casts: false # Don't let any types be inferred as `dynamic`. - # + # # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-inference.md strict-inference: true - # Don't let Dart infer the wrong type on the left side of an assignment. - # + # Don't let Dart infer the wrong type on the left side of an assignment. + # # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-raw-types.md strict-raw-types: true - exclude: + exclude: + - test/*.dart linter: rules: @@ -44,7 +45,7 @@ linter: sort_constructors_first: false # final properties, then constructor avoid_dynamic_calls: false # this lint takes over errors in the IDE cascade_invocations: false # cascades are often less readable - + # Temporarily disabled lints public_member_api_docs: false # until we are ready to document flutter_style_todos: false # until we are ready to address them diff --git a/bin/arcuo.dart b/bin/arcuo.dart index ae9f7d8..d924090 100644 --- a/bin/arcuo.dart +++ b/bin/arcuo.dart @@ -2,7 +2,7 @@ import "package:autonomy/autonomy.dart"; void main() async { final rover = RoverAutonomy(); - rover.drive = RoverDrive(collection: rover, useImu: false, useGps: false); + rover.drive = RoverDrive(collection: rover, useImu: false, useGps: false); await rover.init(); //await rover.waitForValue(); final didSeeAruco = await rover.drive.spinForAruco(); diff --git a/bin/path.dart b/bin/path.dart index 58fe8ba..8ae1f28 100644 --- a/bin/path.dart +++ b/bin/path.dart @@ -18,7 +18,7 @@ void main() { } var turnCount = 0; for (final step in path) { - if (step.direction == DriveDirection.left || step.direction == DriveDirection.right) { + if (step.instruction == DriveDirection.left || step.instruction == DriveDirection.right) { turnCount++; } } diff --git a/bin/random.dart b/bin/random.dart index fe52f37..c356d40 100644 --- a/bin/random.dart +++ b/bin/random.dart @@ -1,7 +1,6 @@ // ignore_for_file: avoid_print import "package:autonomy/interfaces.dart"; -import "package:autonomy/src/rover/corrector.dart"; const maxError = GpsInterface.gpsError; const maxSamples = 10; @@ -17,7 +16,7 @@ bool test() { corrector.addValue(value); if (verbose) { final calibrated = corrector.calibratedValue; - print("Current value: $value, Corrected value: $calibrated"); + print("Current value: $value, Corrected value: $calibrated"); print(" Difference: ${calibrated.toStringAsFixed(7)} < ${epsilon.toStringAsFixed(7)}"); } } diff --git a/bin/sensorless.dart b/bin/sensorless.dart index acc2569..1605430 100644 --- a/bin/sensorless.dart +++ b/bin/sensorless.dart @@ -1,64 +1,63 @@ -import "package:burt_network/logging.dart"; -import "package:burt_network/generated.dart"; +// import "package:burt_network/logging.dart"; +// import "package:burt_network/protobuf.dart"; -import "package:autonomy/interfaces.dart"; -import "package:autonomy/simulator.dart"; -import "package:autonomy/rover.dart"; +// import "package:autonomy/simulator.dart"; +// import "package:autonomy/rover.dart"; -void main() async { - Logger.level = LogLevel.debug; - final simulator = AutonomySimulator(); - simulator.drive = SensorlessDrive(collection: simulator, useGps: false, useImu: false); - await simulator.init(); - await simulator.server.waitForConnection(); +// void main() async { +// Logger.level = LogLevel.debug; +// final simulator = AutonomySimulator(); +// simulator.drive = SensorlessDrive(collection: simulator, useGps: false, useImu: false); +// await simulator.init(); +// await simulator.server.waitForConnection(); - final message = AutonomyData( - destination: GpsCoordinates(latitude: 0, longitude: 0), - state: AutonomyState.DRIVING, - task: AutonomyTask.GPS_ONLY, - obstacles: [], - path: [ - for (double x = 0; x < 3; x++) - for (double y = 0; y < 3; y++) - GpsCoordinates(latitude: y, longitude: x), - ], - ); - simulator.server.sendMessage(message); +// final message = AutonomyData( +// destination: GpsCoordinates(latitude: 0, longitude: 0), +// state: AutonomyState.DRIVING, +// task: AutonomyTask.GPS_ONLY, +// obstacles: [], +// path: [ +// for (double x = 0; x < 3; x++) +// for (double y = 0; y < 3; y++) +// GpsCoordinates(latitude: y, longitude: x), +// ], +// ); +// simulator.server.sendMessage(message); - // "Snakes" around a 3x3 meter box. (0, 0), North - await simulator.drive.goForward(); // (1, 0), North - await simulator.drive.goForward(); // (2, 0), North - await simulator.drive.turnRight(); // (2, 0), East - await simulator.drive.goForward(); // (2, 1), East - await simulator.drive.turnRight(); // (2, 1), South - await simulator.drive.goForward(); // (1, 1), South - await simulator.drive.goForward(); // (0, 1), South - await simulator.drive.turnLeft(); // (0, 1), East - await simulator.drive.goForward(); // (0, 2), East - await simulator.drive.turnLeft(); // (0, 2), North - await simulator.drive.goForward(); // (1, 2), North - await simulator.drive.goForward(); // (2, 2), North - await simulator.drive.turnLeft(); // (2, 2), West - await simulator.drive.goForward(); // (2, 1), West - await simulator.drive.goForward(); // (2, 0), West - await simulator.drive.turnLeft(); // (2, 0), South - await simulator.drive.goForward(); // (1, 0), South - await simulator.drive.goForward(); // (0, 0), South - await simulator.drive.turnLeft(); // (0, 0), East - await simulator.drive.turnLeft(); // (0, 0), North +// // "Snakes" around a 3x3 meter box. (0, 0), North +// await simulator.drive.goForward(); // (1, 0), North +// await simulator.drive.goForward(); // (2, 0), North +// await simulator.drive.turnRight(); // (2, 0), East +// await simulator.drive.goForward(); // (2, 1), East +// await simulator.drive.turnRight(); // (2, 1), South +// await simulator.drive.goForward(); // (1, 1), South +// await simulator.drive.goForward(); // (0, 1), South +// await simulator.drive.turnLeft(); // (0, 1), East +// await simulator.drive.goForward(); // (0, 2), East +// await simulator.drive.turnLeft(); // (0, 2), North +// await simulator.drive.goForward(); // (1, 2), North +// await simulator.drive.goForward(); // (2, 2), North +// await simulator.drive.turnLeft(); // (2, 2), West +// await simulator.drive.goForward(); // (2, 1), West +// await simulator.drive.goForward(); // (2, 0), West +// await simulator.drive.turnLeft(); // (2, 0), South +// await simulator.drive.goForward(); // (1, 0), South +// await simulator.drive.goForward(); // (0, 0), South +// await simulator.drive.turnLeft(); // (0, 0), East +// await simulator.drive.turnLeft(); // (0, 0), North - final message2 = AutonomyData( - state: AutonomyState.AT_DESTINATION, - task: AutonomyTask.AUTONOMY_TASK_UNDEFINED, - obstacles: [], - path: [ - for (double x = 0; x < 3; x++) - for (double y = 0; y < 3; y++) - GpsCoordinates(latitude: y, longitude: x), - ], - ); - simulator.server.sendMessage(message2); - simulator.server.sendMessage(message2); +// final message2 = AutonomyData( +// state: AutonomyState.AT_DESTINATION, +// task: AutonomyTask.AUTONOMY_TASK_UNDEFINED, +// obstacles: [], +// path: [ +// for (double x = 0; x < 3; x++) +// for (double y = 0; y < 3; y++) +// GpsCoordinates(latitude: y, longitude: x), +// ], +// ); +// simulator.server.sendMessage(message2); +// simulator.server.sendMessage(message2); - await simulator.dispose(); -} +// await simulator.dispose(); +// } diff --git a/bin/tank_autonomy.dart b/bin/tank_autonomy.dart deleted file mode 100644 index dac6948..0000000 --- a/bin/tank_autonomy.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "dart:io"; - -import "package:autonomy/interfaces.dart"; -import "package:autonomy/rover.dart"; -import "package:autonomy/simulator.dart"; -import "package:autonomy/src/rover/imu.dart"; -import "package:autonomy/src/simulator/network_detector.dart"; -import "package:burt_network/burt_network.dart"; - -void main() async { - ServerUtils.subsystemsDestination = SocketInfo( - address: InternetAddress("192.168.1.40"), - port: 8001, - ); - final tank = RoverAutonomy(); - tank.detector = NetworkDetector(collection: tank); - tank.gps = GpsSimulator(collection: tank); - // tank.imu = ImuSimulator(collection: tank); - tank.imu = RoverImu(collection: tank); - tank.drive = RoverDrive(collection: tank, useGps: false); - await tank.init(); - await tank.imu.waitForValue(); - - await tank.server.waitForConnection(); -} diff --git a/bin/task.dart b/bin/task.dart index e51da7b..628f823 100644 --- a/bin/task.dart +++ b/bin/task.dart @@ -1,7 +1,6 @@ -import "package:autonomy/interfaces.dart"; import "package:autonomy/simulator.dart"; import "package:autonomy/rover.dart"; -import "package:burt_network/logging.dart"; +import "package:burt_network/burt_network.dart"; final chair = (lat: 2, long: 0).toGps(); final obstacles = [ @@ -22,8 +21,6 @@ void main() async { simulator.drive = DriveSimulator(collection: simulator, shouldDelay: true); await simulator.init(); await simulator.imu.waitForValue(); -// await simulator.drive.faceNorth(); await simulator.server.waitForConnection(); - print("Ready"); } diff --git a/bin/test.dart b/bin/test.dart index acae8bd..63fa627 100644 --- a/bin/test.dart +++ b/bin/test.dart @@ -1,24 +1,22 @@ -import "package:autonomy/autonomy.dart"; -import "package:burt_network/logging.dart"; -import "package:autonomy/src/rover/gps.dart"; -import "package:autonomy/src/rover/imu.dart"; +// import "package:autonomy/autonomy.dart"; +// import "package:burt_network/logging.dart"; -void main() async { - Logger.level = LogLevel.all; - final rover = RoverAutonomy(); - rover.gps = RoverGps(collection: rover); - rover.imu = RoverImu(collection: rover); - rover.drive = RoverDrive(collection: rover, useGps: false, useImu: true); - - await rover.init(); - print("Waiting for readings"); -// await rover.waitForReadings(); -// await rover.waitForConnection(); +// void main() async { +// Logger.level = LogLevel.all; +// final rover = RoverAutonomy(); +// rover.gps = RoverGps(collection: rover); +// rover.imu = RoverImu(collection: rover); +// rover.drive = RoverDrive(collection: rover, useGps: false); - rover.logger.info("Starting"); - await rover.drive.turnLeft(); -await rover.drive.turnRight(); - - rover.logger.info("Done"); - await rover.dispose(); -} +// await rover.init(); +// rover.logger.info("Waiting for readings"); +// // await rover.waitForReadings(); +// // await rover.waitForConnection(); + +// rover.logger.info("Starting"); +// await rover.drive.turnLeft(); +// await rover.drive.turnRight(); + +// rover.logger.info("Done"); +// await rover.dispose(); +// } diff --git a/lib/interfaces.dart b/lib/interfaces.dart index a6ecf3b..90b06b4 100644 --- a/lib/interfaces.dart +++ b/lib/interfaces.dart @@ -1,17 +1,12 @@ -export "src/interfaces/a_star.dart"; -export "src/interfaces/autonomy.dart"; -export "src/interfaces/detector.dart"; -export "src/interfaces/drive.dart"; -export "src/interfaces/error.dart"; -export "src/interfaces/gps.dart"; -export "src/interfaces/gps_utils.dart"; -export "src/interfaces/imu.dart"; -export "src/interfaces/imu_utils.dart"; -export "src/interfaces/pathfinding.dart"; -export "src/interfaces/server.dart"; -export "src/interfaces/video.dart"; -export "src/interfaces/receiver.dart"; -export "src/interfaces/reporter.dart"; -export "src/interfaces/service.dart"; -export "src/interfaces/orchestrator.dart"; -export "package:burt_network/src/utils.dart"; +export "src/autonomy_interface.dart"; +export "src/detector/detector_interface.dart"; +export "src/drive/drive_interface.dart"; +export "src/gps/gps_interface.dart"; +export "src/imu/imu_interface.dart"; +export "src/imu/cardinal_direction.dart"; +export "src/pathfinding/pathfinding_interface.dart"; +export "src/video/video_interface.dart"; +export "src/orchestrator/orchestrator_interface.dart"; + +export "utils.dart"; +export "package:burt_network/protobuf.dart"; diff --git a/lib/rover.dart b/lib/rover.dart index 9f664d9..38f5845 100644 --- a/lib/rover.dart +++ b/lib/rover.dart @@ -1,26 +1,23 @@ -export "src/rover/drive.dart"; -export "src/rover/pathfinding.dart"; -export "src/rover/orchestrator.dart"; -export "src/rover/sensorless.dart"; +export "src/drive/rover_drive.dart"; +export "src/pathfinding/rover_pathfinding.dart"; +export "src/orchestrator/rover_orchestrator.dart"; +export "src/imu/rover_imu.dart"; +export "src/gps/rover_gps.dart"; +export "src/detector/network_detector.dart"; +export "src/video/rover_video.dart"; +export "src/detector/rover_detector.dart"; import "package:autonomy/interfaces.dart"; +import "package:autonomy/rover.dart"; import "package:burt_network/burt_network.dart"; -import "src/rover/pathfinding.dart"; -import "src/rover/drive.dart"; -import "src/rover/gps.dart"; -import "src/rover/imu.dart"; -import "src/rover/orchestrator.dart"; -import "src/rover/video.dart"; -import "src/rover/detector.dart"; - /// A collection of all the different services used by the autonomy program. class RoverAutonomy extends AutonomyInterface { - /// A server to communicate with the dashboard and receive data from the subsystems. - // @override late final AutonomyServer server = AutonomyServer(collection: this); + /// A server to communicate with the dashboard and receive data from the subsystems. + // @override late final AutonomyServer server = AutonomyServer(collection: this); @override late final server = RoverSocket(port: 8003, device: Device.AUTONOMY, collection: this); - /// A helper class to handle driving the rover. - @override late DriveInterface drive = RoverDrive(collection: this); + /// A helper class to handle driving the rover. + @override late DriveInterface drive = RoverDrive(collection: this); @override late GpsInterface gps = RoverGps(collection: this); @override late ImuInterface imu = RoverImu(collection: this); @override late final logger = BurtLogger(socket: server); diff --git a/lib/simulator.dart b/lib/simulator.dart index 908a8ae..8d3d068 100644 --- a/lib/simulator.dart +++ b/lib/simulator.dart @@ -1,20 +1,15 @@ -export "src/simulator/detector.dart"; -export "src/simulator/drive.dart"; -export "src/simulator/gps.dart"; -export "src/simulator/imu.dart"; -export "src/simulator/realsense.dart"; +export "src/detector/sim_detector.dart"; +export "src/drive/sim_drive.dart"; +export "src/gps/sim_gps.dart"; +export "src/imu/sim_imu.dart"; +export "src/video/sim_video.dart"; +export "src/pathfinding/sim_pathfinding.dart"; +export "src/orchestrator/sim_orchestrator.dart"; import "package:autonomy/interfaces.dart"; -import "package:autonomy/src/simulator/orchestrator.dart"; +import "package:autonomy/simulator.dart"; import "package:burt_network/burt_network.dart"; -import "src/simulator/detector.dart"; -import "src/simulator/drive.dart"; -import "src/simulator/gps.dart"; -import "src/simulator/imu.dart"; -import "src/simulator/pathfinding.dart"; -import "src/simulator/realsense.dart"; - class AutonomySimulator extends AutonomyInterface { @override late final logger = BurtLogger(socket: server); @override late final server = RoverSocket(port: 8003, device: Device.AUTONOMY, collection: this); diff --git a/lib/src/interfaces/autonomy.dart b/lib/src/autonomy_interface.dart similarity index 85% rename from lib/src/interfaces/autonomy.dart rename to lib/src/autonomy_interface.dart index bfd948f..dbafe47 100644 --- a/lib/src/interfaces/autonomy.dart +++ b/lib/src/autonomy_interface.dart @@ -24,7 +24,6 @@ abstract class AutonomyInterface extends Service with Receiver { result &= await detector.init(); result &= await video.init(); logger.info("Init orchestrator"); - print("Orchestrator init 1"); await orchestrator.init(); logger.info("Init orchestrator done"); if (result) { @@ -53,15 +52,17 @@ abstract class AutonomyInterface extends Service with Receiver { await init(); } + List get _receivers => [gps, imu, video]; + @override Future waitForValue() async { logger.info("Waiting for readings..."); - await gps.waitForValue(); - await imu.waitForValue(); - await video.waitForValue(); - logger.info("Received GPS and IMU values"); + for (final receiver in _receivers) { + await receiver.waitForValue(); + } + logger.info("Received all necessary values"); } @override - bool get hasValue => gps.hasValue && imu.hasValue && video.hasValue; + bool get hasValue => _receivers.every((r) => r.hasValue); } diff --git a/lib/src/interfaces/detector.dart b/lib/src/detector/detector_interface.dart similarity index 100% rename from lib/src/interfaces/detector.dart rename to lib/src/detector/detector_interface.dart diff --git a/lib/src/simulator/network_detector.dart b/lib/src/detector/network_detector.dart similarity index 79% rename from lib/src/simulator/network_detector.dart rename to lib/src/detector/network_detector.dart index 804519f..c91934d 100644 --- a/lib/src/simulator/network_detector.dart +++ b/lib/src/detector/network_detector.dart @@ -1,5 +1,4 @@ -import "package:autonomy/autonomy.dart"; -import "package:burt_network/burt_network.dart"; +import "package:autonomy/autonomy.dart"; class NetworkDetector extends DetectorInterface { final List _newObstacles = []; @@ -29,9 +28,10 @@ class NetworkDetector extends DetectorInterface { @override Future init() async { collection.server.messages.onMessage( - name: AutonomyData().messageName, - constructor: AutonomyData.fromBuffer, - callback: _onDataReceived); + name: AutonomyData().messageName, + constructor: AutonomyData.fromBuffer, + callback: _onDataReceived, + ); return true; } diff --git a/lib/src/rover/detector.dart b/lib/src/detector/rover_detector.dart similarity index 89% rename from lib/src/rover/detector.dart rename to lib/src/detector/rover_detector.dart index f28c508..3ed0161 100644 --- a/lib/src/rover/detector.dart +++ b/lib/src/detector/rover_detector.dart @@ -11,7 +11,7 @@ class RoverDetector extends DetectorInterface { @override // bool canSeeAruco() => collection.video.data.arucoDetected == BoolState.YES; - bool canSeeAruco() => collection.video.flag; + bool canSeeAruco() => collection.video.flag; @override Future init() async => true; diff --git a/lib/src/simulator/detector.dart b/lib/src/detector/sim_detector.dart similarity index 88% rename from lib/src/simulator/detector.dart rename to lib/src/detector/sim_detector.dart index 0b17967..dfac538 100644 --- a/lib/src/simulator/detector.dart +++ b/lib/src/detector/sim_detector.dart @@ -1,16 +1,15 @@ import "package:autonomy/interfaces.dart"; -import "package:burt_network/generated.dart"; class SimulatedObstacle { final GpsCoordinates coordinates; final int radius; SimulatedObstacle({required this.coordinates, required this.radius}); - bool isNear(GpsCoordinates other) => + bool isNear(GpsCoordinates other) => coordinates.distanceTo(other) <= radius; } -class DetectorSimulator extends DetectorInterface { +class DetectorSimulator extends DetectorInterface { static const arucoPosition = (10, 10); static const slopedLatitude = -5; @@ -24,7 +23,7 @@ class DetectorSimulator extends DetectorInterface { @override Future dispose() async => obstacles.clear(); - + @override bool findObstacles() { final coordinates = collection.gps.coordinates; diff --git a/lib/src/drive/drive_commands.dart b/lib/src/drive/drive_commands.dart new file mode 100644 index 0000000..94f8889 --- /dev/null +++ b/lib/src/drive/drive_commands.dart @@ -0,0 +1,39 @@ +import "package:autonomy/interfaces.dart"; + +mixin RoverDriveCommands on DriveInterface { + /// Sets the max speed of the rover. + /// + /// [_setSpeeds] takes the speeds of each side of wheels. These numbers are percentages of the + /// max speed allowed by the rover, which we call the throttle. This function adjusts the + /// throttle, as a percentage of the rover's top speed. + void setThrottle(double throttle) { + collection.logger.trace("Setting throttle to $throttle"); + sendCommand(DriveCommand(throttle: throttle, setThrottle: true)); + } + + /// Sets the speeds of the left and right wheels, using differential steering. + /// + /// These values are percentages of the max speed allowed by the rover. See [setThrottle]. + void _setSpeeds({required double left, required double right}) { + right *= -1; + collection.logger.trace("Setting speeds to $left and $right"); + sendCommand(DriveCommand(left: left, setLeft: true)); + sendCommand(DriveCommand(right: right, setRight: true)); + } + + void stopMotors() { + setThrottle(0); + _setSpeeds(left: 0, right: 0); + } + + void spinLeft() => _setSpeeds(left: -1, right: 1); + void spinRight() => _setSpeeds(left: 1, right: -1); + void moveForward() => _setSpeeds(left: 1, right: 1); + + /// Sets the angle of the front camera. + void setCameraAngle({required double swivel, required double tilt}) { + collection.logger.trace("Setting camera angles to $swivel (swivel) and $tilt (tilt)"); + final command = DriveCommand(frontSwivel: swivel, frontTilt: tilt); + sendCommand(command); + } +} diff --git a/lib/src/drive/drive_config.dart b/lib/src/drive/drive_config.dart new file mode 100644 index 0000000..2977da6 --- /dev/null +++ b/lib/src/drive/drive_config.dart @@ -0,0 +1,40 @@ +import "dart:io"; + +import "package:burt_network/burt_network.dart"; + +class DriveConfig { + final double forwardThrottle; + final double turnThrottle; + final Duration turnDelay; + final Duration oneMeterDelay; + final String subsystemsAddress; + + const DriveConfig({ + required this.forwardThrottle, + required this.turnThrottle, + required this.turnDelay, + required this.oneMeterDelay, + required this.subsystemsAddress, + }); + + SocketInfo get subsystems => SocketInfo( + address: InternetAddress(subsystemsAddress), + port: 8001, + ); +} + +const roverConfig = DriveConfig( + forwardThrottle: 0.1, + turnThrottle: 0.1, + oneMeterDelay: Duration(milliseconds: 5500), + turnDelay: Duration(milliseconds: 4500), + subsystemsAddress: "192.168.1.20", +); + +const tankConfig = DriveConfig( + forwardThrottle: 0.3, + turnThrottle: 0.35, + turnDelay: Duration(milliseconds: 1000), + oneMeterDelay: Duration(milliseconds: 2000), + subsystemsAddress: "localhost", +); diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart new file mode 100644 index 0000000..a1f3951 --- /dev/null +++ b/lib/src/drive/drive_interface.dart @@ -0,0 +1,55 @@ +import "package:autonomy/interfaces.dart"; + +import "drive_config.dart"; + +enum DriveDirection { + forward, + left, + right, + quarterLeft, + quarterRight, + stop; + + bool get isTurn => this != forward && this != stop; +} + +abstract class DriveInterface extends Service { + AutonomyInterface collection; + DriveInterface({required this.collection}); + + DriveConfig get config => roverConfig; + + Future stop(); + + Future driveForward(GpsCoordinates position); + + Future faceDirection(CardinalDirection orientation); + + void sendCommand(Message message) => collection.server + .sendMessage(message, destination: config.subsystems); + + Future resolveOrientation() => faceDirection(collection.imu.nearest); + + /// Turns to face the state's [AutonomyAStarState.orientation]. + /// + /// Exists so that the TimedDrive can implement this in terms of [AutonomyAStarState.instruction]. + Future turnState(AutonomyAStarState state) => faceDirection(state.orientation); + + Future driveState(AutonomyAStarState state) { + if (state.instruction == DriveDirection.stop) { + return stop(); + } else if (state.instruction == DriveDirection.forward) { + return driveForward(state.position); + } else { + return turnState(state); + } + } + + void setLedStrip(ProtoColor color, {bool blink = false}) { + final command = DriveCommand(color: color, blink: blink ? BoolState.YES : BoolState.NO); + sendCommand(command); + } + + Future spinForAruco() async => false; + Future approachAruco() async { } +} diff --git a/lib/src/drive/rover_drive.dart b/lib/src/drive/rover_drive.dart new file mode 100644 index 0000000..434b9a5 --- /dev/null +++ b/lib/src/drive/rover_drive.dart @@ -0,0 +1,88 @@ +import "package:autonomy/interfaces.dart"; +import "package:autonomy/rover.dart"; + +import "sensor_drive.dart"; +import "timed_drive.dart"; +import "sim_drive.dart"; + +/// A helper class to send drive commands to the rover with a simpler API. +class RoverDrive extends DriveInterface { + final bool useGps; + final bool useImu; + + late final sensorDrive = SensorDrive(collection: collection); + late final timedDrive = TimedDrive(collection: collection); + late final simDrive = DriveSimulator(collection: collection); + + RoverDrive({ + required super.collection, + this.useGps = true, + this.useImu = true, + }); + + /// Initializes the rover's drive subsystems. + @override + Future init() async { + if (!useImu && collection.imu is RoverImu) { + collection.logger.critical( + "Cannot use Rover IMU with simulated turning", + body: "Set useImu to true, or use the simulated IMU", + ); + return false; + } + if (!useGps && collection.imu is RoverGps) { + collection.logger.critical( + "Cannot use Rover GPS with simulated driving", + body: "Set useGps to true, or use the simulated GPS", + ); + return false; + } + + var result = true; + result &= await sensorDrive.init(); + result &= await timedDrive.init(); + result &= await simDrive.init(); + return result; + } + + /// Stops the rover from driving. + @override + Future dispose() async { + await sensorDrive.dispose(); + await timedDrive.dispose(); + await simDrive.dispose(); + } + + @override + Future stop() async { + await sensorDrive.stop(); + await timedDrive.stop(); + await simDrive.stop(); + } + + @override + Future spinForAruco() => sensorDrive.spinForAruco(); + + @override + Future approachAruco() => sensorDrive.approachAruco(); + + @override + Future faceDirection(CardinalDirection orientation) async { + if (useImu) { + await sensorDrive.faceDirection(orientation); + } else { + await timedDrive.faceDirection(orientation); + await simDrive.faceDirection(orientation); + } + } + + @override + Future driveForward(GpsCoordinates position) async { + if (useGps) { + await sensorDrive.driveForward(position); + } else { + await timedDrive.driveForward(position); + await simDrive.driveForward(position); + } + } +} diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart new file mode 100644 index 0000000..22c03d3 --- /dev/null +++ b/lib/src/drive/sensor_drive.dart @@ -0,0 +1,87 @@ +import "package:autonomy/autonomy.dart"; +import "package:autonomy/interfaces.dart"; + +import "drive_commands.dart"; + +class SensorDrive extends DriveInterface with RoverDriveCommands { + static const predicateDelay = Duration(milliseconds: 10); + + SensorDrive({required super.collection}); + + @override + Future stop() async => stopMotors(); + + Future waitFor(bool Function() predicate) async { + while (!predicate()) { + await Future.delayed(predicateDelay); + } + } + + @override + Future init() async => true; + + @override + Future dispose() async { } + + @override + Future driveForward(GpsCoordinates position) async { + collection.logger.info("Driving forward one meter"); + setThrottle(config.forwardThrottle); + moveForward(); + await waitFor(() => collection.gps.isNear(position)); + await stop(); + } + + @override + Future faceDirection(CardinalDirection orientation) async { + collection.logger.info("Turning to face $orientation..."); + setThrottle(config.turnThrottle); + await waitFor(() => _tryToFace(orientation)); + await stop(); + } + + bool _tryToFace(CardinalDirection orientation) { + final current = collection.imu.heading; + final target = orientation.angle; + if ((current - target).abs() < 180) { + if (current < target) { + spinRight(); + } else { + spinLeft(); + } + } else { + if (current < target) { + spinLeft(); + } else { + spinRight(); + } + } + collection.logger.trace("Current heading: $current"); + return collection.imu.isNear(orientation); + } + + @override + Future spinForAruco() async { + setThrottle(config.turnThrottle); + spinLeft(); + final result = await waitFor(() => collection.detector.canSeeAruco()) + .then((_) => true) + .timeout(config.turnDelay * 4, onTimeout: () => false); + await stop(); + return result; + } + + @override + Future approachAruco() async { + const sizeThreshold = 0.2; + const epsilon = 0.00001; + setThrottle(config.forwardThrottle); + moveForward(); + await waitFor(() { + final size = collection.video.arucoSize; + collection.logger.trace("The Aruco tag is at $size percent"); + return (size.abs() < epsilon && !collection.detector.canSeeAruco()) || size >= sizeThreshold; + }).timeout(config.oneMeterDelay * 5); + await stop(); + } +} diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart new file mode 100644 index 0000000..35d6cb2 --- /dev/null +++ b/lib/src/drive/sim_drive.dart @@ -0,0 +1,29 @@ +import "package:autonomy/interfaces.dart"; + +class DriveSimulator extends DriveInterface { + static const delay = Duration(milliseconds: 500); + + final bool shouldDelay; + DriveSimulator({required super.collection, this.shouldDelay = false}); + + @override + Future init() async => true; + + @override + Future dispose() async { } + + @override + Future driveForward(GpsCoordinates position) async { + if (shouldDelay) await Future.delayed(delay); + collection.gps.update(position); + } + + @override + Future faceDirection(CardinalDirection orientation) async { + if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); + collection.imu.update(orientation.orientation); + } + + @override + Future stop() async => collection.logger.debug("Stopping"); +} diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart new file mode 100644 index 0000000..1ab997a --- /dev/null +++ b/lib/src/drive/timed_drive.dart @@ -0,0 +1,79 @@ +import "dart:math"; + +import "package:autonomy/interfaces.dart"; + +import "drive_commands.dart"; + +class TimedDrive extends DriveInterface with RoverDriveCommands { + TimedDrive({required super.collection}); + + @override + Future init() async => true; + + @override + Future dispose() async { } + + @override + Future driveForward(GpsCoordinates position) async { + await goForward(collection.imu.nearest.isPerpendicular ? 1 : sqrt2); + } + + @override + Future turnState(AutonomyAStarState state) async { + switch (state.instruction) { + case DriveDirection.left: + await turnLeft(); + case DriveDirection.right: + await turnRight(); + case DriveDirection.quarterLeft: + await turnQuarterLeft(); + case DriveDirection.quarterRight: + await turnQuarterRight(); + case DriveDirection.forward || DriveDirection.stop: + break; + } + } + + @override + Future stop() async => stopMotors(); + + Future goForward([double distance = 1]) async { + collection.logger.info("Driving forward $distance meters"); + setThrottle(config.forwardThrottle); + moveForward(); + await Future.delayed(config.oneMeterDelay * distance); + await stop(); + } + + Future turnLeft() async { + setThrottle(config.turnThrottle); + spinLeft(); + await Future.delayed(config.turnDelay); + await stop(); + } + + Future turnRight() async { + setThrottle(config.turnThrottle); + spinRight(); + await Future.delayed(config.turnDelay); + await stop(); + } + + Future turnQuarterLeft() async { + setThrottle(config.turnThrottle); + spinLeft(); + await Future.delayed(config.turnDelay * 0.5); + await stop(); + } + + Future turnQuarterRight() async { + setThrottle(config.turnThrottle); + spinRight(); + await Future.delayed(config.turnDelay * 0.5); + await stop(); + } + + @override + Future faceDirection(CardinalDirection orientation) => + throw UnsupportedError("Cannot face any arbitrary direction using TimedDrive"); +} diff --git a/lib/src/interfaces/gps.dart b/lib/src/gps/gps_interface.dart similarity index 93% rename from lib/src/interfaces/gps.dart rename to lib/src/gps/gps_interface.dart index 953b8b0..e5f8ae7 100644 --- a/lib/src/interfaces/gps.dart +++ b/lib/src/gps/gps_interface.dart @@ -1,4 +1,3 @@ -import "package:burt_network/generated.dart"; import "package:autonomy/interfaces.dart"; abstract class GpsInterface extends Service with Receiver { diff --git a/lib/src/gps/rover_gps.dart b/lib/src/gps/rover_gps.dart new file mode 100644 index 0000000..80a1cb0 --- /dev/null +++ b/lib/src/gps/rover_gps.dart @@ -0,0 +1,42 @@ +import "package:autonomy/interfaces.dart"; + +class RoverGps extends GpsInterface { + final _latitudeCorrector = ErrorCorrector.disabled(); + final _longitudeCorrector = ErrorCorrector.disabled(); + RoverGps({required super.collection}); + + @override + Future init() async { + collection.server.messages.onMessage( + name: RoverPosition().messageName, + constructor: RoverPosition.fromBuffer, + callback: _internalUpdate, + ); + return super.init(); + } + + @override + Future dispose() async { + _latitudeCorrector.clear(); + _longitudeCorrector.clear(); + } + + @override + void update(GpsCoordinates newValue) { + // Do nothing, since this should only be internally updated + } + + void _internalUpdate(RoverPosition newValue) { + if (!newValue.hasGps()) return; + final position = newValue.gps; + _latitudeCorrector.addValue(position.latitude); + _longitudeCorrector.addValue(position.longitude); + hasValue = true; + } + + @override + GpsCoordinates get coordinates => GpsCoordinates( + latitude: _latitudeCorrector.calibratedValue, + longitude: _longitudeCorrector.calibratedValue, + ); +} diff --git a/lib/src/simulator/gps.dart b/lib/src/gps/sim_gps.dart similarity index 93% rename from lib/src/simulator/gps.dart rename to lib/src/gps/sim_gps.dart index 48d652f..d90a7f5 100644 --- a/lib/src/simulator/gps.dart +++ b/lib/src/gps/sim_gps.dart @@ -1,4 +1,3 @@ -import "package:burt_network/generated.dart"; import "package:autonomy/interfaces.dart"; class GpsSimulator extends GpsInterface with ValueReporter { diff --git a/lib/src/imu/cardinal_direction.dart b/lib/src/imu/cardinal_direction.dart new file mode 100644 index 0000000..40b2521 --- /dev/null +++ b/lib/src/imu/cardinal_direction.dart @@ -0,0 +1,79 @@ + +import "package:autonomy/interfaces.dart"; + +enum CardinalDirection { + north(0), + west(90), + south(180), + east(270), + northEast(000 + 45), + northWest(360 - 45), + southWest(180 + 45), + southEast(180 - 45); + + final double angle; + const CardinalDirection(this.angle); + + Orientation get orientation => Orientation(z: angle); + + static CardinalDirection nearest(Orientation orientation) { + var smallestDiff = double.infinity; + var closestOrientation = CardinalDirection.north; + + for (final value in values) { + final diff = (value.angle - orientation.z).clampAngle(); + if (diff < smallestDiff) { + smallestDiff = diff; + closestOrientation = value; + } + } + + return closestOrientation; + } + + bool get isPerpendicular => angle.abs() % 90 == 0; + + CardinalDirection turnLeft() => switch (this) { + north => west, + west => south, + south => east, + east => north, + northEast => northWest, + northWest => southWest, + southWest => southEast, + southEast => northEast, + }; + + CardinalDirection turnRight() => switch (this) { + north => east, + west => north, + south => west, + east => south, + northEast => southEast, + southEast => southWest, + southWest => northWest, + northWest => northEast, + }; + + CardinalDirection turnQuarterLeft() => switch (this) { + north => northWest, + northWest => west, + west => southWest, + southWest => south, + south => southEast, + southEast => east, + east => northEast, + northEast => north, + }; + + CardinalDirection turnQuarterRight() => switch (this) { + north => northEast, + northEast => east, + east => southEast, + southEast => south, + south => southWest, + southWest => west, + west => northWest, + northWest => north, + }; +} diff --git a/lib/src/interfaces/imu.dart b/lib/src/imu/imu_interface.dart similarity index 57% rename from lib/src/interfaces/imu.dart rename to lib/src/imu/imu_interface.dart index 5bb6bba..7fbbf83 100644 --- a/lib/src/interfaces/imu.dart +++ b/lib/src/imu/imu_interface.dart @@ -1,5 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:burt_network/burt_network.dart"; abstract class ImuInterface extends Service with Receiver { final AutonomyInterface collection; @@ -7,12 +6,11 @@ abstract class ImuInterface extends Service with Receiver { double get heading => raw.z; Orientation get raw; - DriveOrientation? get orientation { - collection.logger.trace("Trying to find orientation at $heading"); - return DriveOrientation.fromRaw(raw); - } + + CardinalDirection get nearest => CardinalDirection.nearest(raw); + void update(Orientation newValue); - bool isNear(double angle) => raw.isNear(angle); + bool isNear(CardinalDirection direction) => raw.isNear(direction.angle); @override Future init() async => true; diff --git a/lib/src/imu/rover_imu.dart b/lib/src/imu/rover_imu.dart new file mode 100644 index 0000000..2d9d63a --- /dev/null +++ b/lib/src/imu/rover_imu.dart @@ -0,0 +1,43 @@ +import "package:autonomy/interfaces.dart"; + +class RoverImu extends ImuInterface { + final _xCorrector = ErrorCorrector.disabled(); + final _yCorrector = ErrorCorrector.disabled(); + final _zCorrector = ErrorCorrector.disabled(); + RoverImu({required super.collection}); + + @override + Future init() async { + collection.server.messages.onMessage( + name: RoverPosition().messageName, + constructor: RoverPosition.fromBuffer, + callback: _internalUpdate, + ); + return super.init(); + } + + @override + Future dispose() async { + _zCorrector.clear(); + } + + @override + void update(Orientation newValue) { + // Do nothing, since this should only be internally updated + } + + void _internalUpdate(RoverPosition newValue) { + if (!newValue.hasOrientation()) return; + _xCorrector.addValue(newValue.orientation.x); + _yCorrector.addValue(newValue.orientation.y); + _zCorrector.addValue(newValue.orientation.z); + hasValue = true; + } + + @override + Orientation get raw => Orientation( + x: _xCorrector.calibratedValue.clampAngle(), + y: _yCorrector.calibratedValue.clampAngle(), + z: _zCorrector.calibratedValue.clampAngle(), + ); +} diff --git a/lib/src/simulator/imu.dart b/lib/src/imu/sim_imu.dart similarity index 77% rename from lib/src/simulator/imu.dart rename to lib/src/imu/sim_imu.dart index da7678a..8798446 100644 --- a/lib/src/simulator/imu.dart +++ b/lib/src/imu/sim_imu.dart @@ -1,11 +1,10 @@ -import "package:burt_network/generated.dart"; import "package:autonomy/interfaces.dart"; -class ImuSimulator extends ImuInterface with ValueReporter { +class ImuSimulator extends ImuInterface with ValueReporter { final RandomError _error; - ImuSimulator({required super.collection, double maxError = 0}) : + ImuSimulator({required super.collection, double maxError = 0}) : _error = RandomError(maxError); - + @override RoverPosition getMessage() => RoverPosition(orientation: raw); @@ -19,7 +18,7 @@ class ImuSimulator extends ImuInterface with ValueReporter { ); @override - void update(Orientation newValue) => _orientation = newValue.clampHeading(); + void update(Orientation newValue) => _orientation = newValue; @override Future init() async { diff --git a/lib/src/interfaces/a_star.dart b/lib/src/interfaces/a_star.dart deleted file mode 100644 index a0177d3..0000000 --- a/lib/src/interfaces/a_star.dart +++ /dev/null @@ -1,167 +0,0 @@ -import "dart:math"; - -import "package:a_star/a_star.dart"; - -import "package:burt_network/generated.dart"; -import "package:autonomy/interfaces.dart"; - -class AutonomyAStarState extends AStarState { - final DriveDirection direction; - final DriveOrientation orientation; - final GpsCoordinates position; - final GpsCoordinates goal; - final AutonomyInterface collection; - - AutonomyAStarState({ - required this.position, - required this.goal, - required this.collection, - required this.direction, - required this.orientation, - required super.depth, - }); - - factory AutonomyAStarState.start({ - required AutonomyInterface collection, - required GpsCoordinates goal, - }) => AutonomyAStarState( - position: collection.gps.coordinates, - goal: goal, - collection: collection, - direction: DriveDirection.stop, - orientation: collection.imu.orientation ?? DriveOrientation.north, - depth: 0, - ); - - @override - String toString() => switch(direction) { - DriveDirection.forward => "Go forward to ${position.prettyPrint()}", - DriveDirection.left => "Turn left to face $direction", - DriveDirection.right => "Turn right to face $direction", - DriveDirection.stop => "Start/Stop at ${position.prettyPrint()}", - DriveDirection.forwardLeft => "Turn 45 degrees left to face $direction", - DriveDirection.forwardRight => "Turn 45 degrees right to face $direction", - }; - - @override - double heuristic() => position.distanceTo(goal); - - @override - String hash() => "${position.prettyPrint()} ($orientation)"; - - @override - bool isGoal() => position.isNear(goal, min(GpsUtils.moveLengthMeters, GpsUtils.maxErrorMeters)); - - AutonomyAStarState copyWith({required DriveDirection direction, required DriveOrientation orientation, required GpsCoordinates position}) => AutonomyAStarState( - collection: collection, - position: position, - orientation: orientation, - direction: direction, - goal: goal, - depth: (direction == DriveDirection.forward) - ? depth + 1 - : (direction == DriveDirection.forwardLeft || direction == DriveDirection.forwardRight) - ? depth + sqrt2 - : depth + 2 * sqrt2, - ); - - /// Returns whether or not the rover will drive between or right next to an obstacle diagonally
- ///
- /// Case 1:
- /// 0 X
- /// X R
- /// Assuming the rover is facing 0 and trying to drive forward, will return false
- ///
- /// Case 2:
- /// 0 X
- /// X R
- /// Assuming the rover is facing north and trying to turn 45 degrees left, will return false
- ///
- /// Case 3:
- /// 0 X
- /// 0 R
- /// If the rover is facing left but trying to turn 45 degrees right, will return false
- ///
- /// Case 4:
- /// 0 X 0
- /// 0 R 0
- /// If the rover is facing northeast to 0 and trying to turn left, will return false - bool drivingThroughObstacle(AutonomyAStarState state) { - final isTurn = state.direction != DriveDirection.forward; - final isQuarterTurn = state.direction == DriveDirection.forwardLeft || state.direction == DriveDirection.forwardRight; - - // Forward drive across the perpendicular axis - if (!isTurn && state.orientation.isPerpendicular) { - return false; - } - - // Not encountering any sort of diagonal angle - if (isTurn && isQuarterTurn && state.orientation.isPerpendicular) { - return false; - } - - // No diagonal movement, won't drive between obstacles - if (!isQuarterTurn && orientation.isPerpendicular) { - return false; - } - - DriveOrientation orientation1; - DriveOrientation orientation2; - - // Case 1, trying to drive while facing a 45 degree angle - if (!isTurn) { - orientation1 = state.orientation.turnQuarterLeft(); - orientation2 = state.orientation.turnQuarterRight(); - } else if (isQuarterTurn) { // Case 2 and Case 3 - orientation1 = orientation; - orientation2 = (state.direction == DriveDirection.forwardLeft) - ? orientation1.turnLeft() - : orientation1.turnRight(); - } else { // Case 4 - orientation1 = (state.direction == DriveDirection.left) - ? orientation.turnQuarterLeft() - : orientation.turnQuarterRight(); - orientation2 = (state.direction == DriveDirection.left) - ? state.orientation.turnQuarterLeft() - : state.orientation.turnQuarterRight(); - } - - // Since the state being passed has a position of moving after the - // turn, we have to check the position of where it started - return collection.pathfinder.isObstacle( - position.goForward(orientation1), - ) || - collection.pathfinder.isObstacle( - position.goForward(orientation2), - ); - } - - @override - Iterable expand() => [ - copyWith( - direction: DriveDirection.forward, - orientation: orientation, - position: position.goForward(orientation), - ), - copyWith( - direction: DriveDirection.left, - orientation: orientation.turnLeft(), - position: position, - ), - copyWith( - direction: DriveDirection.right, - orientation: orientation.turnRight(), - position: position, - ), - copyWith( - direction: DriveDirection.forwardLeft, - orientation: orientation.turnQuarterLeft(), - position: position, - ), - copyWith( - direction: DriveDirection.forwardRight, - orientation: orientation.turnQuarterRight(), - position: position, - ), - ].where((state) => !collection.pathfinder.isObstacle(state.position) && !drivingThroughObstacle(state)); -} diff --git a/lib/src/interfaces/drive.dart b/lib/src/interfaces/drive.dart deleted file mode 100644 index f9b6c96..0000000 --- a/lib/src/interfaces/drive.dart +++ /dev/null @@ -1,125 +0,0 @@ -import "package:autonomy/interfaces.dart"; -import "package:burt_network/generated.dart"; - -const bool isRover = false; - -enum DriveDirection { - forward, - left, - right, - forwardLeft, - forwardRight, - stop; - - bool get isTurn => this != forward && this != stop; -} - -enum DriveOrientation { - north(0), - west(90), - south(180), - east(270), - northEast(360 - 45), - northWest(45), - southEast(180 + 45), - southWest(180 - 45); - - final int angle; - const DriveOrientation(this.angle); - - static DriveOrientation? fromRaw(Orientation orientation) { - // TODO: Make this more precise. - for (final value in values) { - if (orientation.isNear(value.angle.toDouble(), OrientationUtils.orientationEpsilon)) return value; - } - return null; - } - - bool get isPerpendicular => angle.abs() % 90 == 0; - - DriveOrientation turnLeft() => switch (this) { - north => west, - west => south, - south => east, - east => north, - northEast => northWest, - northWest => southWest, - southWest => southEast, - southEast => northEast, - }; - - DriveOrientation turnRight() => switch (this) { - north => east, - west => north, - south => west, - east => south, - northEast => southEast, - southEast => southWest, - southWest => northWest, - northWest => northEast, - }; - - DriveOrientation turnQuarterLeft() => switch (this) { - north => northWest, - northWest => west, - west => southWest, - southWest => south, - south => southEast, - southEast => east, - east => northEast, - northEast => north, - }; - - DriveOrientation turnQuarterRight() => switch (this) { - north => northEast, - northEast => east, - east => southEast, - southEast => south, - south => southWest, - southWest => west, - west => northWest, - northWest => north, - }; -} - -abstract class DriveInterface extends Service { - AutonomyInterface collection; - DriveOrientation orientation = DriveOrientation.north; - DriveInterface({required this.collection}); - - Future goDirection(DriveDirection direction) async => switch (direction) { - DriveDirection.forward => await goForward(), - DriveDirection.left => await turnLeft(), - DriveDirection.right => await turnRight(), - DriveDirection.forwardLeft => await turnQuarterLeft(), - DriveDirection.forwardRight => await turnQuarterRight(), - DriveDirection.stop => await stop(), - }; - - Future faceNorth(); - - Future goForward(); - Future turnLeft(); - Future turnRight(); - Future turnQuarterLeft(); - Future turnQuarterRight(); - Future stop(); - - Future faceDirection(DriveOrientation orientation) async { - this.orientation = orientation; - } - - Future followPath(List path) async { - for (final state in path) { - await goDirection(state.direction); - } - } - - void setLedStrip(ProtoColor color, {bool blink = false}) { - final command = DriveCommand(color: color, blink: blink ? BoolState.YES : BoolState.NO); - collection.server.sendCommand(command); - } - - Future spinForAruco() async => false; - Future approachAruco() async { } -} diff --git a/lib/src/interfaces/imu_utils.dart b/lib/src/interfaces/imu_utils.dart deleted file mode 100644 index c0f8a37..0000000 --- a/lib/src/interfaces/imu_utils.dart +++ /dev/null @@ -1,28 +0,0 @@ -import "package:burt_network/generated.dart"; - -extension OrientationUtils on Orientation { - static const double epsilon = 3.5; - static const double orientationEpsilon = 10; - - static final north = Orientation(z: 0); - static final west = Orientation(z: 90); - static final south = Orientation(z: 180); - static final east = Orientation(z: 270); - - double get heading => z; - - bool get isEmpty => x == 0 && y == 0 && z == 0; - - Orientation clampHeading() { - var adjustedHeading = heading; - if (heading >= 360) adjustedHeading -= 360; - if (heading < 0) adjustedHeading = 360 + heading; - return Orientation(x: x, y: y, z: adjustedHeading); - } - - bool isNear(double value, [double tolerance = epsilon]) => value > 270 && z < 90 - ? (z + 360 - value).abs() < tolerance - : value < 90 && z > 270 - ? (z - value - 360).abs() < tolerance - : (z - value).abs() < tolerance; -} diff --git a/lib/src/interfaces/receiver.dart b/lib/src/interfaces/receiver.dart deleted file mode 100644 index 59ec05b..0000000 --- a/lib/src/interfaces/receiver.dart +++ /dev/null @@ -1,9 +0,0 @@ -mixin Receiver { - bool hasValue = false; - - Future waitForValue() async { - while (!hasValue) { - await Future.delayed(const Duration(seconds: 1)); - } - } -} diff --git a/lib/src/interfaces/server.dart b/lib/src/interfaces/server.dart deleted file mode 100644 index cec6850..0000000 --- a/lib/src/interfaces/server.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "dart:io"; - -import "package:burt_network/burt_network.dart"; - -extension ServerUtils on RoverSocket { - static SocketInfo subsystemsDestination = SocketInfo( - address: InternetAddress("192.168.1.20"), - port: 8001, - ); - - void sendCommand(Message message) => sendMessage(message, destination: subsystemsDestination); - - Future waitForConnection() async { - logger.info("Waiting for connection..."); - while (!isConnected) { - await Future.delayed(const Duration(milliseconds: 100)); - } - return; - } - - void sendDone() { - final message = AutonomyData(state: AutonomyState.AT_DESTINATION, task: AutonomyTask.AUTONOMY_TASK_UNDEFINED); - sendMessage(message); - } -} diff --git a/lib/src/interfaces/service.dart b/lib/src/interfaces/service.dart deleted file mode 100644 index 19cbaca..0000000 --- a/lib/src/interfaces/service.dart +++ /dev/null @@ -1 +0,0 @@ -export "package:burt_network/burt_network.dart" show Service; diff --git a/lib/src/interfaces/orchestrator.dart b/lib/src/orchestrator/orchestrator_interface.dart similarity index 83% rename from lib/src/interfaces/orchestrator.dart rename to lib/src/orchestrator/orchestrator_interface.dart index a1dc437..ce8fcfe 100644 --- a/lib/src/interfaces/orchestrator.dart +++ b/lib/src/orchestrator/orchestrator_interface.dart @@ -1,7 +1,6 @@ import "dart:io"; import "package:autonomy/interfaces.dart"; -import "package:burt_network/burt_network.dart"; import "package:meta/meta.dart"; abstract class OrchestratorInterface extends Service { @@ -18,24 +17,25 @@ abstract class OrchestratorInterface extends Service { return; } - if (!collection.hasValue && false) { + if (!collection.hasValue) { + // We don't wait here because this was explicitly requested by the operator. + // Users expect immediate feedback, so we give an error instead of freezing. collection.logger.error("Sensors haven't gotten any readings yet!"); currentState = AutonomyState.NO_SOLUTION; return; } - await collection.drive.faceNorth(); + await collection.drive.resolveOrientation(); currentCommand = command; switch (command.task) { + case AutonomyTask.BETWEEN_GATES: break; // TODO + case AutonomyTask.AUTONOMY_TASK_UNDEFINED: break; case AutonomyTask.GPS_ONLY: await handleGpsTask(command); case AutonomyTask.VISUAL_MARKER: await handleArucoTask(command); - // TODO: Add more tasks - default: collection.logger.error("Unrecognized task: ${command.task}"); // ignore: no_default_cases } } @override Future init() async { - print("Orchestrator init 2"); collection.server.messages.onMessage( name: AutonomyCommand().messageName, constructor: AutonomyCommand.fromBuffer, diff --git a/lib/src/rover/orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart similarity index 93% rename from lib/src/rover/orchestrator.dart rename to lib/src/orchestrator/rover_orchestrator.dart index c33e1e9..7d536c8 100644 --- a/lib/src/rover/orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -1,5 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:burt_network/generated.dart"; import "dart:async"; class RoverOrchestrator extends OrchestratorInterface with ValueReporter { @@ -36,17 +35,16 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { @override Future handleGpsTask(AutonomyCommand command) async { final destination = command.destination; - collection.logger.info("Got GPS Task: Go to ${destination.prettyPrint()}"); + collection.logger.info("Got GPS Task", body: "Go to ${destination.prettyPrint()}"); collection.logger.debug("Currently at ${collection.gps.coordinates.prettyPrint()}"); traversed.clear(); collection.drive.setLedStrip(ProtoColor.RED); collection.detector.findObstacles(); - // await collection.drive.faceNorth(); + await collection.drive.resolveOrientation(); while (!collection.gps.coordinates.isNear(destination)) { // Calculate a path collection.logger.debug("Finding a path"); currentState = AutonomyState.PATHING; - // await collection.drive.faceNorth(); final path = collection.pathfinder.getPath(destination); currentPath = path; // also use local variable path for promotion if (path == null) { @@ -67,7 +65,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { var count = 0; for (final state in path) { collection.logger.debug(state.toString()); - await collection.drive.goDirection(state.direction); + await collection.drive.driveState(state); traversed.add(state.position); // if (state.direction != DriveDirection.forward) continue; if (count++ == 5) break; diff --git a/lib/src/simulator/orchestrator.dart b/lib/src/orchestrator/sim_orchestrator.dart similarity index 92% rename from lib/src/simulator/orchestrator.dart rename to lib/src/orchestrator/sim_orchestrator.dart index 6df6e49..ae2dd75 100644 --- a/lib/src/simulator/orchestrator.dart +++ b/lib/src/orchestrator/sim_orchestrator.dart @@ -1,5 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:burt_network/generated.dart"; class OrchestratorSimulator extends OrchestratorInterface { OrchestratorSimulator({required super.collection}); diff --git a/lib/src/interfaces/pathfinding.dart b/lib/src/pathfinding/pathfinding_interface.dart similarity index 92% rename from lib/src/interfaces/pathfinding.dart rename to lib/src/pathfinding/pathfinding_interface.dart index cc780c8..25bce8e 100644 --- a/lib/src/interfaces/pathfinding.dart +++ b/lib/src/pathfinding/pathfinding_interface.dart @@ -1,5 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:burt_network/generated.dart"; abstract class PathfindingInterface extends Service { final AutonomyInterface collection; diff --git a/lib/src/rover/pathfinding.dart b/lib/src/pathfinding/rover_pathfinding.dart similarity index 73% rename from lib/src/rover/pathfinding.dart rename to lib/src/pathfinding/rover_pathfinding.dart index a7235d8..a72d279 100644 --- a/lib/src/rover/pathfinding.dart +++ b/lib/src/pathfinding/rover_pathfinding.dart @@ -1,10 +1,8 @@ import "package:a_star/a_star.dart"; import "package:autonomy/interfaces.dart"; -import "package:burt_network/burt_network.dart"; -import "package:burt_network/generated.dart"; -class RoverPathfinder extends PathfindingInterface { +class RoverPathfinder extends PathfindingInterface { RoverPathfinder({required super.collection}); @override @@ -12,7 +10,7 @@ class RoverPathfinder extends PathfindingInterface { @override List? getPath(GpsCoordinates destination, {bool verbose = false}) { - if (isObstacle(destination)) return null; + if (isObstacle(destination)) return null; final state = AutonomyAStarState.start(collection: collection, goal: destination); final result = aStar(state, verbose: verbose, limit: 50000); if (result == null) return null; diff --git a/lib/src/pathfinding/sim_pathfinding.dart b/lib/src/pathfinding/sim_pathfinding.dart new file mode 100644 index 0000000..3df3970 --- /dev/null +++ b/lib/src/pathfinding/sim_pathfinding.dart @@ -0,0 +1,24 @@ +import "package:autonomy/interfaces.dart"; + +class PathfindingSimulator extends PathfindingInterface { + static int i = 0; + + PathfindingSimulator({required super.collection}); + + @override + Future init() async => true; + + @override + List getPath(GpsCoordinates destination, {bool verbose = false}) => [ + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 0).toGps(), orientation: CardinalDirection.north, instruction: DriveDirection.right, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 0).toGps(), orientation: CardinalDirection.east, instruction: DriveDirection.forward, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 1).toGps(), orientation: CardinalDirection.east, instruction: DriveDirection.forward, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 2).toGps(), orientation: CardinalDirection.east, instruction: DriveDirection.left, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 2).toGps(), orientation: CardinalDirection.north, instruction: DriveDirection.forward, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 1, long: 2).toGps(), orientation: CardinalDirection.north, instruction: DriveDirection.forward, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 2).toGps(), orientation: CardinalDirection.north, instruction: DriveDirection.left, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 2).toGps(), orientation: CardinalDirection.west, instruction: DriveDirection.forward, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 1).toGps(), orientation: CardinalDirection.west, instruction: DriveDirection.right, collection: collection), + AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 1).toGps(), orientation: CardinalDirection.north, instruction: DriveDirection.forward, collection: collection), + ]; +} diff --git a/lib/src/rover/drive.dart b/lib/src/rover/drive.dart deleted file mode 100644 index 7235668..0000000 --- a/lib/src/rover/drive.dart +++ /dev/null @@ -1,73 +0,0 @@ -import "package:burt_network/burt_network.dart"; -import "package:autonomy/interfaces.dart"; - -import "drive/timed.dart"; -import "drive/sensor.dart"; - -/// A helper class to send drive commands to the rover with a simpler API. -class RoverDrive extends DriveInterface { - final bool useGps; - final bool useImu; - - final SensorDrive sensorDrive; - final TimedDrive timedDrive; - - // TODO: Calibrate these - RoverDrive({required super.collection, this.useGps = true, this.useImu = true}) : - sensorDrive = SensorDrive(collection: collection), - timedDrive = TimedDrive(collection: collection); - - /// Initializes the rover's drive subsystems. - @override - Future init() async => true; - - /// Stops the rover from driving. - @override - Future dispose() => stop(); - - /// Sets the angle of the front camera. - void setCameraAngle({required double swivel, required double tilt}) { - collection.logger.trace("Setting camera angles to $swivel (swivel) and $tilt (tilt)"); - final command = DriveCommand(frontSwivel: swivel, frontTilt: tilt); - collection.server.sendCommand(command); - } - - @override - Future stop() async { - await timedDrive.stop(); - } - - @override - Future faceNorth() => useImu ? sensorDrive.faceNorth() : timedDrive.faceNorth(); - - @override - Future spinForAruco() => sensorDrive.spinForAruco(); - @override - Future approachAruco() => sensorDrive.approachAruco(); - - - @override - Future faceDirection(DriveOrientation orientation) async { - if (useImu) { - await sensorDrive.faceDirection(orientation); - } else { - await timedDrive.faceDirection(orientation); - } - await super.faceDirection(orientation); - } - - @override - Future goForward() => useGps ? sensorDrive.goForward() : timedDrive.goForward(); - - @override - Future turnLeft() => useImu ? sensorDrive.turnLeft() : timedDrive.turnLeft(); - - @override - Future turnRight() => useImu ? sensorDrive.turnRight() : timedDrive.turnRight(); - - @override - Future turnQuarterLeft() => useImu ? sensorDrive.turnQuarterLeft() : timedDrive.turnQuarterLeft(); - - @override - Future turnQuarterRight() => useImu ? sensorDrive.turnQuarterRight() : timedDrive.turnQuarterRight(); -} diff --git a/lib/src/rover/drive/motors.dart b/lib/src/rover/drive/motors.dart deleted file mode 100644 index 98710c2..0000000 --- a/lib/src/rover/drive/motors.dart +++ /dev/null @@ -1,26 +0,0 @@ -import "package:autonomy/interfaces.dart"; -import "package:burt_network/generated.dart"; - -mixin RoverMotors { - AutonomyInterface get collection; - - /// Sets the max speed of the rover. - /// - /// [setSpeeds] takes the speeds of each side of wheels. These numbers are percentages of the - /// max speed allowed by the rover, which we call the throttle. This function adjusts the - /// throttle, as a percentage of the rover's top speed. - void setThrottle(double throttle) { - collection.logger.trace("Setting throttle to $throttle"); - collection.server.sendCommand(DriveCommand(throttle: throttle, setThrottle: true)); - } - - /// Sets the speeds of the left and right wheels, using differential steering. - /// - /// These values are percentages of the max speed allowed by the rover. See [setThrottle]. - void setSpeeds({required double left, required double right}) { - right *= -1; - collection.logger.trace("Setting speeds to $left and $right"); - collection.server.sendCommand(DriveCommand(left: left, setLeft: true)); - collection.server.sendCommand(DriveCommand(right: right, setRight: true)); - } -} diff --git a/lib/src/rover/drive/sensor.dart b/lib/src/rover/drive/sensor.dart deleted file mode 100644 index adaab3f..0000000 --- a/lib/src/rover/drive/sensor.dart +++ /dev/null @@ -1,219 +0,0 @@ -import "package:autonomy/autonomy.dart"; -import "package:autonomy/interfaces.dart"; - -import "motors.dart"; - -class SensorDrive extends DriveInterface with RoverMotors { - static const double maxThrottle = 0.1; - static const double turnThrottleRover = 0.1; - static const double turnThrottleTank = 0.35; - - static double get turnThrottle => isRover ? turnThrottleRover : turnThrottleTank; - - static const predicateDelay = Duration(milliseconds: 100); - static const turnDelay = Duration(milliseconds: 1500); - - SensorDrive({required super.collection}); - - @override - Future stop() async { - setThrottle(0); - setSpeeds(left: 0, right: 0); - } - - Future waitFor(bool Function() predicate) async { - while (!predicate()) { - await Future.delayed(predicateDelay); - await collection.imu.waitForValue(); - } - } - - Future runFeedback(bool Function() completed, [Duration period = predicateDelay]) async { - while (!completed()) { - await Future.delayed(period); - } - } - - @override - Future init() async => true; - - @override - Future dispose() async { } - - @override - Future goForward() async { - final orientation = collection.imu.orientation; - final currentCoordinates = collection.gps.coordinates; - final destination = currentCoordinates.goForward(orientation!); - - setThrottle(maxThrottle); - setSpeeds(left: 1, right: 1); - await waitFor(() => collection.gps.coordinates.isNear(destination)); - await stop(); - } - - @override - Future faceNorth() async { - collection.logger.info("Turning to face north..."); - setThrottle(turnThrottle); - // setSpeeds(left: -1, right: 1); - // await waitFor(() { - // collection.logger.trace("Current heading: ${collection.imu.heading}"); - // return collection.imu.raw.isNear(0, OrientationUtils.orientationEpsilon); - // }); - await faceDirection(DriveOrientation.north); - await stop(); - } - - @override - Future faceDirection(DriveOrientation orientation) async { - collection.logger.info("Turning to face $orientation..."); - setThrottle(turnThrottle); - setSpeeds(left: -1, right: 1); - await runFeedback( - () { - var delta = orientation.angle.toDouble() - collection.imu.raw.z; - if (delta < -180) { - delta += 360; - } else if (delta > 180) { - delta -= 360; - } - - if (delta < 0) { - setSpeeds(left: 1, right: -1); - } else { - setSpeeds(left: -1, right: 1); - } - collection.logger.trace("Current heading: ${collection.imu.heading}"); - return collection.imu.raw.isNear(orientation.angle.toDouble()); - }, - const Duration(milliseconds: 10), - ); - await stop(); - await super.faceDirection(orientation); - } - - @override - Future turnLeft() async { - await collection.imu.waitForValue(); - - if (collection.imu.orientation == null) { - await faceNorth(); - await faceDirection(this.orientation); - } - await collection.imu.waitForValue(); - - final orientation = collection.imu.orientation; - final destination = orientation!.turnLeft(); // do NOT clamp! - print("Going from ${orientation} to ${destination}"); - setThrottle(turnThrottle); - // setSpeeds(left: -1, right: 1); - // await waitFor(() => collection.imu.orientation == destination); - await faceDirection(destination); - await stop(); - this.orientation = this.orientation.turnLeft(); - } - - @override - Future turnRight() async { - await collection.imu.waitForValue(); - if (collection.imu.orientation == null) { - await faceNorth(); - await faceDirection(this.orientation); - } - await collection.imu.waitForValue(); - final orientation = collection.imu.orientation; - final destination = orientation!.turnRight(); // do NOT clamp! - setThrottle(turnThrottle); - // setSpeeds(left: 1, right: -1); - // await waitFor(() => collection.imu.orientation == destination); - await faceDirection(destination); - await stop(); - this.orientation = this.orientation.turnRight(); - } - - @override - Future turnQuarterLeft() async { - await collection.imu.waitForValue(); - - if (collection.imu.orientation == null) { - await faceNorth(); - await faceDirection(this.orientation); - } - await collection.imu.waitForValue(); - - final orientation = collection.imu.orientation; - final destination = orientation!.turnQuarterLeft(); // do NOT clamp! - print("Going from ${orientation} to ${destination}"); - setThrottle(turnThrottle); - // setSpeeds(left: -1, right: 1); - // await waitFor(() => collection.imu.orientation == destination); - await faceDirection(destination); - await stop(); - this.orientation = this.orientation.turnQuarterLeft(); - } - - @override - Future turnQuarterRight() async { - await collection.imu.waitForValue(); - - if (collection.imu.orientation == null) { - await faceNorth(); - await faceDirection(this.orientation); - } - await collection.imu.waitForValue(); - - final orientation = collection.imu.orientation; - final destination = orientation!.turnQuarterRight(); // do NOT clamp! - print("Going from ${orientation} to ${destination}"); - setThrottle(turnThrottle); - // setSpeeds(left: 1, right: -1); - // await waitFor(() => collection.imu.orientation == destination); - await faceDirection(destination); - await stop(); - this.orientation = this.orientation.turnQuarterRight(); - } - - @override - Future spinForAruco() async { - for (var i = 0; i < 16; i++) { - setThrottle(turnThrottle); - setSpeeds(left: -1, right: 1); - await Future.delayed(turnDelay); - await stop(); - - for (var j = 0; j < 300; j++) { - await Future.delayed(const Duration(milliseconds: 10)); - collection.logger.trace("Can see aruco? ${collection.detector.canSeeAruco()}"); - - if (collection.detector.canSeeAruco()) { - // Spin a bit more to center it - // print("We found it!"); - // setThrottle(0.1); - // setSpeeds(left: -1, right: 1); - // await waitFor(() { - // final pos = collection.video.arucoPosition; - // collection.logger.debug("aruco is at $pos"); - // return pos > 0.2; - // }); - // await stop(); - return true; - }} - } - return false; - } - - @override - Future approachAruco() async { - setThrottle(maxThrottle); - setSpeeds(left: 1, right: 1); - // const threshold = 0.2; - // await waitFor(() { - // final pos = collection.video.arucoSize; - // collection.logger.debug("It is at $pos percent"); - // return (pos.abs() < 0.00001 && !collection.detector.canSeeAruco()) || pos >= threshold; - // }); - await Future.delayed(const Duration(seconds: 10)); - await stop(); - } -} diff --git a/lib/src/rover/drive/timed.dart b/lib/src/rover/drive/timed.dart deleted file mode 100644 index bba9d6e..0000000 --- a/lib/src/rover/drive/timed.dart +++ /dev/null @@ -1,86 +0,0 @@ -import "package:autonomy/interfaces.dart"; - -import "motors.dart"; - -class TimedDrive extends DriveInterface with RoverMotors { - static const maxThrottleTank = 0.3; - static const turnThrottleTank = 0.35; - - static const maxThrottleRover = 0.1; - static const turnThrottleRover = 0.1; - - static const oneMeterDelayRover = Duration(milliseconds: 5500); - static const turnDelayRover = Duration(milliseconds: 4500); - - static const oneMeterDelayTank = Duration(milliseconds: 2000); - static const turnDelayTank = Duration(milliseconds: 1000); - - static double get maxThrottle => isRover ? maxThrottleRover : maxThrottleTank; - static double get turnThrottle => isRover ? turnThrottleRover : turnThrottleTank; - - static Duration get oneMeterDelay => isRover ? oneMeterDelayRover : oneMeterDelayTank; - static Duration get turnDelay => isRover ? turnDelayRover : turnDelayTank; - - TimedDrive({required super.collection}); - - @override - Future init() async => true; - - @override - Future dispose() async { } - - @override - Future stop() async { - setThrottle(0); - setSpeeds(left: 0, right: 0); - } - - @override - Future faceNorth() async { /* Assume already facing north */ } - - @override - Future goForward() async { - setThrottle(maxThrottle); - setSpeeds(left: 1 * 0.9, right: 1); - final position = collection.gps.coordinates; - final orientation = collection.imu.orientation; - if (orientation != null) { - final newPosition = position.goForward(orientation); - collection.gps.update(newPosition); - } - await Future.delayed(oneMeterDelay); - await stop(); - } - - @override - Future turnLeft() async { - setThrottle(turnThrottle); - setSpeeds(left: -1 * 0.9, right: 1); - await Future.delayed(turnDelay); - await stop(); - } - - @override - Future turnRight() async { - setThrottle(turnThrottle); - setSpeeds(left: 1, right: -1); - await Future.delayed(turnDelay); - await stop(); - } - - @override - Future turnQuarterLeft() async { - setThrottle(turnThrottle); - setSpeeds(left: -1, right: 1); - await Future.delayed(turnDelay * 0.5); - await stop(); - } - - @override - Future turnQuarterRight() async { - setThrottle(turnThrottle); - setSpeeds(left: 1, right: -1); - await Future.delayed(turnDelay * 0.5); - await stop(); - } -} diff --git a/lib/src/rover/gps.dart b/lib/src/rover/gps.dart deleted file mode 100644 index 2704b85..0000000 --- a/lib/src/rover/gps.dart +++ /dev/null @@ -1,41 +0,0 @@ -import "package:burt_network/burt_network.dart"; -import "package:autonomy/interfaces.dart"; - -import "corrector.dart"; - -class RoverGps extends GpsInterface { - final _latitudeCorrector = ErrorCorrector(maxSamples: 1, maxDeviation: GpsInterface.gpsError * 10); - final _longitudeCorrector = ErrorCorrector(maxSamples: 1, maxDeviation: GpsInterface.gpsError * 10); - RoverGps({required super.collection}); - - @override - Future init() async { - collection.server.messages.onMessage( - name: RoverPosition().messageName, - constructor: RoverPosition.fromBuffer, - callback: (pos) { - if (pos.hasGps()) update(pos.gps); - }, - ); - return super.init(); - } - - @override - Future dispose() async { - _latitudeCorrector.clear(); - _longitudeCorrector.clear(); - } - - @override - void update(GpsCoordinates newValue) { - _latitudeCorrector.addValue(newValue.latitude); - _longitudeCorrector.addValue(newValue.longitude); - hasValue = true; - } - - @override - GpsCoordinates get coordinates => GpsCoordinates( - latitude: _latitudeCorrector.calibratedValue, - longitude: _longitudeCorrector.calibratedValue, - ); -} diff --git a/lib/src/rover/imu.dart b/lib/src/rover/imu.dart deleted file mode 100644 index 8428ae5..0000000 --- a/lib/src/rover/imu.dart +++ /dev/null @@ -1,44 +0,0 @@ -import "package:burt_network/burt_network.dart"; -import "package:autonomy/interfaces.dart"; - -import "corrector.dart"; - -class RoverImu extends ImuInterface { - final _zCorrector = ErrorCorrector(maxSamples: 10, maxDeviation: 15); - RoverImu({required super.collection}); - - Orientation value = Orientation(); - - @override - Future init() async { - collection.server.messages.onMessage( - name: RoverPosition().messageName, - constructor: RoverPosition.fromBuffer, - callback: (pos) { - if (pos.hasOrientation()) update(pos.orientation); - }, - ); - return super.init(); - } - - @override - Future dispose() async { - _zCorrector.clear(); - } - - @override - void update(Orientation newValue) { - // _zCorrector.addValue(newValue.heading); - // collection.logger.trace("Got IMU value"); - print("Got imu: ${newValue.heading}. Direction: ${collection.drive.orientation}"); - hasValue = true; - value = newValue; - } - - @override - Orientation get raw => Orientation( - x: 0, - y: 0, - z: value.z, - ).clampHeading(); -} diff --git a/lib/src/rover/sensorless.dart b/lib/src/rover/sensorless.dart deleted file mode 100644 index 7a9b7b4..0000000 --- a/lib/src/rover/sensorless.dart +++ /dev/null @@ -1,76 +0,0 @@ -import "package:autonomy/interfaces.dart"; -import "package:autonomy/rover.dart"; -import "package:autonomy/simulator.dart"; - -class SensorlessDrive extends DriveInterface { - final DriveInterface simulatedDrive; - final DriveInterface realDrive; - - SensorlessDrive({required super.collection, bool useGps = true, bool useImu = true}) : - simulatedDrive = DriveSimulator(collection: collection), - realDrive = RoverDrive(collection: collection, useGps: useGps, useImu: useImu); - - @override - Future init() async { - var result = true; - result &= await simulatedDrive.init(); - result &= await realDrive.init(); - return result; - } - - @override - Future dispose() async { - await simulatedDrive.dispose(); - await realDrive.dispose(); - } - - @override - Future goForward() async { - await simulatedDrive.goForward(); - await realDrive.goForward(); - } - - @override - Future stop() async { - await simulatedDrive.stop(); - await realDrive.stop(); - } - - @override - Future faceNorth() async { - await simulatedDrive.faceNorth(); - await realDrive.faceNorth(); - } - - - @override - Future faceDirection(DriveOrientation orientation) async { - await simulatedDrive.faceDirection(orientation); - await realDrive.faceDirection(orientation); - await super.faceDirection(orientation); - } - - @override - Future turnLeft() async { - await simulatedDrive.turnLeft(); - await realDrive.turnLeft(); - } - - @override - Future turnRight() async { - await simulatedDrive.turnRight(); - await realDrive.turnRight(); - } - - @override - Future turnQuarterLeft() async { - await simulatedDrive.turnQuarterLeft(); - await realDrive.turnQuarterLeft(); - } - - @override - Future turnQuarterRight() async { - await simulatedDrive.turnQuarterRight(); - await realDrive.turnQuarterRight(); - } -} diff --git a/lib/src/simulator/drive.dart b/lib/src/simulator/drive.dart deleted file mode 100644 index b1b04d8..0000000 --- a/lib/src/simulator/drive.dart +++ /dev/null @@ -1,73 +0,0 @@ -// ignore_for_file: cascade_invocations - -import "package:autonomy/interfaces.dart"; -import "package:burt_network/generated.dart"; - -class DriveSimulator extends DriveInterface { - final bool shouldDelay; - DriveSimulator({required super.collection, this.shouldDelay = false}); - - @override - Future init() async => true; - - @override - Future dispose() async { } - - @override - Future goForward() async { - final position = collection.gps.coordinates; - final orientation = collection.imu.orientation; - final newPosition = position.goForward(orientation!); - collection.logger.debug("Going forward"); - collection.logger.trace(" Old position: ${position.prettyPrint()}"); - collection.logger.trace(" Orientation: $orientation"); - collection.logger.trace(" New position: ${newPosition.prettyPrint()}"); - collection.gps.update(newPosition); - if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); - } - - @override - Future faceNorth() async { - collection.imu.update(OrientationUtils.north); - if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); - } - - @override - Future turnLeft() async { - collection.logger.debug("Turning left"); - final heading = collection.imu.heading; - final orientation = Orientation(z: heading + 90); - collection.imu.update(orientation); - if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); - } - - @override - Future turnRight() async { - collection.logger.debug("Turning right"); - final heading = collection.imu.heading; - final orientation = Orientation(z: heading - 90); - collection.imu.update(orientation); - if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); - } - - @override - Future turnQuarterLeft() async { - collection.logger.debug("Turning quarter left"); - final heading = collection.imu.heading; - final orientation = Orientation(z: heading + 45).clampHeading(); - collection.imu.update(orientation); - if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); - } - - @override - Future turnQuarterRight() async { - collection.logger.debug("Turning quarter right"); - final heading = collection.imu.heading; - final orientation = Orientation(z: heading - 45).clampHeading(); - collection.imu.update(orientation); - if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); - } - - @override - Future stop() async => collection.logger.debug("Stopping"); -} diff --git a/lib/src/simulator/pathfinding.dart b/lib/src/simulator/pathfinding.dart deleted file mode 100644 index bce45b6..0000000 --- a/lib/src/simulator/pathfinding.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "package:autonomy/interfaces.dart"; -import "package:burt_network/generated.dart"; - -class PathfindingSimulator extends PathfindingInterface { - static int i = 0; - - PathfindingSimulator({required super.collection}); - - @override - Future init() async => true; - - @override - List getPath(GpsCoordinates destination, {bool verbose = false}) => [ - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 0).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.right, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 0).toGps(), orientation: DriveOrientation.east, direction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 1).toGps(), orientation: DriveOrientation.east, direction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 2).toGps(), orientation: DriveOrientation.east, direction: DriveDirection.left, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 2).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 1, long: 2).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 2).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.left, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 2).toGps(), orientation: DriveOrientation.west, direction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 1).toGps(), orientation: DriveOrientation.west, direction: DriveDirection.right, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 1).toGps(), orientation: DriveOrientation.north, direction: DriveDirection.forward, collection: collection), - ]; -} diff --git a/lib/src/utils/a_star.dart b/lib/src/utils/a_star.dart new file mode 100644 index 0000000..add8e17 --- /dev/null +++ b/lib/src/utils/a_star.dart @@ -0,0 +1,184 @@ +import "dart:math"; + +import "package:a_star/a_star.dart"; + +import "package:autonomy/interfaces.dart"; + +class AutonomyAStarState extends AStarState { + static double getCost(DriveDirection direction) { + if (direction == DriveDirection.forward) { + return 1; + } else if (direction == DriveDirection.quarterLeft || direction == DriveDirection.quarterRight) { + return sqrt2; + } else { + return 2 * sqrt2; + } + } + + final DriveDirection instruction; + final GpsCoordinates position; + final CardinalDirection orientation; + final GpsCoordinates goal; + final AutonomyInterface collection; + + AutonomyAStarState({ + required this.position, + required this.goal, + required this.collection, + required this.instruction, + required this.orientation, + required super.depth, + }); + + factory AutonomyAStarState.start({ + required AutonomyInterface collection, + required GpsCoordinates goal, + }) => AutonomyAStarState( + position: collection.gps.coordinates, + goal: goal, + collection: collection, + instruction: DriveDirection.stop, + orientation: collection.imu.nearest, + depth: 0, + ); + + AutonomyAStarState copyWith({ + required DriveDirection direction, + required CardinalDirection orientation, + required GpsCoordinates position, + }) => AutonomyAStarState( + collection: collection, + position: position, + orientation: orientation, + instruction: direction, + goal: goal, + depth: depth + getCost(direction), + ); + + @override + String toString() => switch(instruction) { + DriveDirection.forward => "Go forward to ${position.prettyPrint()}", + DriveDirection.left => "Turn left to face $instruction", + DriveDirection.right => "Turn right to face $instruction", + DriveDirection.stop => "Start/Stop at ${position.prettyPrint()}", + DriveDirection.quarterLeft => "Turn 45 degrees left to face $instruction", + DriveDirection.quarterRight => "Turn 45 degrees right to face $instruction", + }; + + @override + double heuristic() => position.distanceTo(goal); + + @override + String hash() => "${position.prettyPrint()} ($orientation)"; + + @override + bool isGoal() => position.isNear(goal, min(GpsUtils.moveLengthMeters, GpsUtils.maxErrorMeters)); + + /// Returns whether or not the rover will drive between or right next to an obstacle diagonally
+ ///
+ /// Case 1:
+ /// 0 X
+ /// X R
+ /// Assuming the rover is facing 0 and trying to drive forward, will return false
+ ///
+ /// Case 2:
+ /// 0 X
+ /// X R
+ /// Assuming the rover is facing north and trying to turn 45 degrees left, will return false
+ ///
+ /// Case 3:
+ /// 0 X
+ /// 0 R
+ /// If the rover is facing left but trying to turn 45 degrees right, will return false
+ ///
+ /// Case 4:
+ /// 0 X 0
+ /// 0 R 0
+ /// If the rover is facing northeast to 0 and trying to turn left, will return false + bool willDriveThroughObstacle(AutonomyAStarState state) { + final isTurn = state.instruction != DriveDirection.forward; + final isQuarterTurn = state.instruction == DriveDirection.quarterLeft || state.instruction == DriveDirection.quarterRight; + + if ( + // Can't hit an obstacle while turning + state.instruction != DriveDirection.forward + + // Forward drive across the perpendicular axis + || (!isTurn && state.orientation.isPerpendicular) + + // Not encountering any sort of diagonal angle + || (isTurn && isQuarterTurn && state.orientation.isPerpendicular) + + // No diagonal movement, won't drive between obstacles + || (!isQuarterTurn && orientation.isPerpendicular) + ) { + return false; + } + + final CardinalDirection orientation1; + final CardinalDirection orientation2; + + // Case 1, trying to drive while facing a 45 degree angle + if (!isTurn) { + orientation1 = state.orientation.turnQuarterLeft(); + orientation2 = state.orientation.turnQuarterRight(); + } else if (isQuarterTurn) { // Case 2 and Case 3 + orientation1 = orientation; + orientation2 = (state.instruction == DriveDirection.quarterLeft) + ? orientation1.turnLeft() + : orientation1.turnRight(); + } else { // Case 4 + orientation1 = (state.instruction == DriveDirection.left) + ? orientation.turnQuarterLeft() + : orientation.turnQuarterRight(); + orientation2 = (state.instruction == DriveDirection.left) + ? state.orientation.turnQuarterLeft() + : state.orientation.turnQuarterRight(); + } + + // Since the state being passed has a position of moving after the + // turn, we have to check the position of where it started + return collection.pathfinder.isObstacle(position.goForward(orientation1)) + || collection.pathfinder.isObstacle(position.goForward(orientation2)); + } + + bool isValidState(AutonomyAStarState state) => + !collection.pathfinder.isObstacle(state.position) + && !willDriveThroughObstacle(state); + + Iterable _allNeighbors() => [ + copyWith( + direction: DriveDirection.forward, + orientation: orientation, + position: position.goForward(orientation), + ), + copyWith( + direction: DriveDirection.left, + orientation: orientation.turnLeft(), + position: position, + ), + copyWith( + direction: DriveDirection.right, + orientation: orientation.turnRight(), + position: position, + ), + copyWith( + direction: DriveDirection.quarterLeft, + orientation: orientation.turnQuarterLeft(), + position: position, + ), + copyWith( + direction: DriveDirection.quarterRight, + orientation: orientation.turnQuarterRight(), + position: position, + ), + copyWith( + direction: DriveDirection.stop, + orientation: orientation, + position: position, + ), + ]; + + @override + Iterable expand() => _allNeighbors().where(isValidState); +} diff --git a/lib/src/rover/corrector.dart b/lib/src/utils/corrector.dart similarity index 88% rename from lib/src/rover/corrector.dart rename to lib/src/utils/corrector.dart index 46b59e1..7339f02 100644 --- a/lib/src/rover/corrector.dart +++ b/lib/src/utils/corrector.dart @@ -1,13 +1,14 @@ import "dart:collection"; -class ErrorCorrector { // non-nullable +class ErrorCorrector { final int maxSamples; final double maxDeviation; ErrorCorrector({required this.maxSamples, this.maxDeviation = double.infinity}); - + factory ErrorCorrector.disabled() => ErrorCorrector(maxSamples: 1); + double calibratedValue = 0; final Queue recentSamples = DoubleLinkedQueue(); - + void addValue(double value) { if (recentSamples.isEmpty) { recentSamples.add(value); @@ -32,7 +33,7 @@ extension on Iterable { num sum = 0; var count = 0; for (final element in this) { - sum += element; + sum += element; count++; } return sum / count; diff --git a/lib/src/interfaces/error.dart b/lib/src/utils/error.dart similarity index 100% rename from lib/src/interfaces/error.dart rename to lib/src/utils/error.dart diff --git a/lib/src/interfaces/gps_utils.dart b/lib/src/utils/gps_utils.dart similarity index 72% rename from lib/src/interfaces/gps_utils.dart rename to lib/src/utils/gps_utils.dart index dc6c19e..7e56f0e 100644 --- a/lib/src/interfaces/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -2,15 +2,14 @@ import "dart:math"; import "package:autonomy/interfaces.dart"; -import "package:burt_network/generated.dart"; extension GpsUtils on GpsCoordinates { - static double maxErrorMeters = 1; + static double maxErrorMeters = 0.5; static double moveLengthMeters = 1; - static double get epsilonLatitude => maxErrorMeters * latitudePerMeter; + static double get epsilonLatitude => maxErrorMeters * latitudePerMeter; static double get epsilonLongitude => maxErrorMeters * longitudePerMeter; - static double get movementLatitude => moveLengthMeters * latitudePerMeter; + static double get movementLatitude => moveLengthMeters * latitudePerMeter; static double get movementLongitude => moveLengthMeters * longitudePerMeter; static GpsCoordinates get east => GpsCoordinates(longitude: -movementLongitude); @@ -24,20 +23,19 @@ extension GpsUtils on GpsCoordinates { // Taken from https://stackoverflow.com/a/39540339/9392211 static const metersPerLatitude = 111.32 * 1000; // 111.32 km - // static const metersPerLatitude = 1; static const radiansPerDegree = pi / 180; static double get metersPerLongitude => 40075 * cos(GpsInterface.currentLatitude * radiansPerDegree) / 360 * 1000.0; - + static double get latitudePerMeter => 1 / metersPerLatitude; static double get longitudePerMeter => 1 / metersPerLongitude; - + double distanceTo(GpsCoordinates other) => sqrt( pow(latitude - other.latitude, 2) + pow(longitude - other.longitude, 2), ); - double manhattanDistance(GpsCoordinates other) => - (latitude - other.latitude).abs() * metersPerLatitude + + double manhattanDistance(GpsCoordinates other) => + (latitude - other.latitude).abs() * metersPerLatitude + (longitude - other.longitude).abs() * metersPerLongitude; bool isNear(GpsCoordinates other, [double? tolerance]) { @@ -60,17 +58,16 @@ extension GpsUtils on GpsCoordinates { longitude: longitude + other.longitude, ); -// String prettyPrint() => "(lat=${(latitude * GpsUtils.metersPerLatitude).toStringAsFixed(2)}, long=${(longitude * GpsUtils.metersPerLongitude).toStringAsFixed(2)})"; String prettyPrint() => toProto3Json().toString(); - GpsCoordinates goForward(DriveOrientation orientation) => this + switch(orientation) { - DriveOrientation.north => GpsUtils.north, - DriveOrientation.south => GpsUtils.south, - DriveOrientation.west => GpsUtils.west, - DriveOrientation.east => GpsUtils.east, - DriveOrientation.northEast => GpsUtils.northEast, - DriveOrientation.northWest => GpsUtils.northWest, - DriveOrientation.southEast => GpsUtils.southEast, - DriveOrientation.southWest => GpsUtils.southWest, + GpsCoordinates goForward(CardinalDirection orientation) => this + switch(orientation) { + CardinalDirection.north => GpsUtils.north, + CardinalDirection.south => GpsUtils.south, + CardinalDirection.west => GpsUtils.west, + CardinalDirection.east => GpsUtils.east, + CardinalDirection.northEast => GpsUtils.northEast, + CardinalDirection.northWest => GpsUtils.northWest, + CardinalDirection.southEast => GpsUtils.southEast, + CardinalDirection.southWest => GpsUtils.southWest, }; } diff --git a/lib/src/utils/imu_utils.dart b/lib/src/utils/imu_utils.dart new file mode 100644 index 0000000..5a45f9a --- /dev/null +++ b/lib/src/utils/imu_utils.dart @@ -0,0 +1,29 @@ +import "package:burt_network/protobuf.dart"; + +extension OrientationUtils on Orientation { + static const double epsilon = 3.5; + static const double orientationEpsilon = 10; + + static final north = Orientation(z: 0); + static final west = Orientation(z: 90); + static final south = Orientation(z: 180); + static final east = Orientation(z: 270); + + double get heading => z; + + bool get isEmpty => x == 0 && y == 0 && z == 0; + + bool isNear(double value) { + if (value > 270 && z < 90) { + return (z + 360 - value).abs() < epsilon; + } else if (value < 90 && z > 270) { + return (z - value - 360).abs() < epsilon; + } else { + return (z - value).abs() < epsilon; + } + } +} + +extension AngleUtils on double { + double clampAngle() => ((this % 360) + 360) % 360; +} diff --git a/lib/src/utils/receiver.dart b/lib/src/utils/receiver.dart new file mode 100644 index 0000000..d301496 --- /dev/null +++ b/lib/src/utils/receiver.dart @@ -0,0 +1,21 @@ +import "dart:async"; + +mixin Receiver { + Completer? _completer; + + bool _hasValue = false; + + set hasValue(bool value) { + _hasValue = value; + if (!value) return; + _completer?.complete(); + _completer = null; + } + + bool get hasValue => _hasValue; + + Future waitForValue() { + _completer = Completer(); + return _completer!.future; + } +} diff --git a/lib/src/interfaces/reporter.dart b/lib/src/utils/reporter.dart similarity index 86% rename from lib/src/interfaces/reporter.dart rename to lib/src/utils/reporter.dart index 05e848b..744eb80 100644 --- a/lib/src/interfaces/reporter.dart +++ b/lib/src/utils/reporter.dart @@ -1,5 +1,4 @@ import "dart:async"; -import "package:burt_network/generated.dart"; import "package:autonomy/interfaces.dart"; @@ -13,7 +12,7 @@ mixin ValueReporter on Service { @override Future init() async { timer = Timer.periodic(reportInterval, (timer) => _reportValue()); - return await super.init(); + return super.init(); } @override diff --git a/lib/src/rover/video.dart b/lib/src/video/rover_video.dart similarity index 67% rename from lib/src/rover/video.dart rename to lib/src/video/rover_video.dart index 2296331..b9442d2 100644 --- a/lib/src/rover/video.dart +++ b/lib/src/video/rover_video.dart @@ -1,7 +1,6 @@ import "dart:async"; import "package:autonomy/interfaces.dart"; -import "package:burt_network/burt_network.dart"; class RoverVideo extends VideoInterface { RoverVideo({required super.collection}); @@ -22,11 +21,11 @@ class RoverVideo extends VideoInterface { @override void updateFrame(VideoData newData) { data = newData; - if (data.arucoDetected == BoolState.YES) { - flag = true; - Timer(const Duration(seconds: 3), () => flag = false); - collection.logger.info("Is ArUco detected: ${data.arucoDetected}"); - } + // if (data.arucoDetected == BoolState.YES) { + // flag = true; + // Timer(const Duration(seconds: 3), () => flag = false); + // collection.logger.info("Is ArUco detected: ${data.arucoDetected}"); + // } hasValue = true; } } diff --git a/lib/src/simulator/realsense.dart b/lib/src/video/sim_video.dart similarity index 89% rename from lib/src/simulator/realsense.dart rename to lib/src/video/sim_video.dart index 0e20191..c7d5f1c 100644 --- a/lib/src/simulator/realsense.dart +++ b/lib/src/video/sim_video.dart @@ -1,11 +1,10 @@ import "dart:typed_data"; import "package:autonomy/interfaces.dart"; -import "package:burt_network/generated.dart"; class VideoSimulator extends VideoInterface { VideoSimulator({required super.collection}); - + @override Future init() async { hasValue = true; @@ -14,7 +13,7 @@ class VideoSimulator extends VideoInterface { @override Future dispose() async => depthFrame = Uint16List.fromList([]); - + Uint16List depthFrame = Uint16List.fromList([]); @override diff --git a/lib/src/interfaces/video.dart b/lib/src/video/video_interface.dart similarity index 71% rename from lib/src/interfaces/video.dart rename to lib/src/video/video_interface.dart index e66dc3e..ed76473 100644 --- a/lib/src/interfaces/video.dart +++ b/lib/src/video/video_interface.dart @@ -1,5 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:burt_network/generated.dart"; /// Handles obstacle detection data and ArUco data from video abstract class VideoInterface extends Service with Receiver { @@ -12,6 +11,6 @@ bool flag = false; void updateFrame(VideoData newData); - double get arucoSize => data.arucoSize; - double get arucoPosition => data.arucoPosition; + double get arucoSize => 0; //data.arucoSize; + double get arucoPosition => 0; //data.arucoPosition; } diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 0000000..4829620 --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,9 @@ +export "src/utils/a_star.dart"; +export "src/utils/corrector.dart"; +export "src/utils/error.dart"; +export "src/utils/gps_utils.dart"; +export "src/utils/imu_utils.dart"; +export "src/utils/receiver.dart"; +export "src/utils/reporter.dart"; + +export "package:burt_network/burt_network.dart" show Service; diff --git a/pubspec.lock b/pubspec.lock index 69567f5..f67921e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,12 +57,10 @@ packages: burt_network: dependency: "direct main" description: - path: "." - ref: "2.2.0" - resolved-ref: "7ec80052bd9d2777782f7ce44cfcfc1e69e8a582" - url: "https://github.com/BinghamtonRover/Networking.git" - source: git - version: "2.2.0" + path: "../../burt_network" + relative: true + source: path + version: "2.3.0" collection: dependency: transitive description: @@ -239,15 +237,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - opencv_ffi: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: fb721ce518faa62b470c73ae58edfe825a5f9052 - url: "https://github.com/BinghamtonRover/OpenCV-FFI.git" - source: git - version: "1.2.0" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index daf64d3..841baf4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,12 +8,10 @@ environment: # Try not to edit this section by hand. Use `dart pub add my_package` dependencies: - opencv_ffi: - git: https://github.com/BinghamtonRover/OpenCV-FFI.git burt_network: - git: + git: url: https://github.com/BinghamtonRover/Networking.git - ref: 2.2.0 + ref: 2.3.0 a_star: ^3.0.0 meta: ^1.11.0 diff --git a/test/network_test.dart b/test/network_test.dart index dc5a1fd..f6595f5 100644 --- a/test/network_test.dart +++ b/test/network_test.dart @@ -106,9 +106,6 @@ void main() => group("[Network]", tags: ["network"], () { simulator.drive = SensorlessDrive(collection: simulator, useGps: false, useImu: false); await simulator.init(); - await simulator.drive.faceNorth(); - expect(simulator.imu.orientation, DriveOrientation.north); - final origin = GpsCoordinates(latitude: 0, longitude: 0); final oneMeter = (lat: 1, long: 0).toGps(); expect(subsystems.throttle, 0); diff --git a/test/orchestrator_test.dart b/test/orchestrator_test.dart index 7144f89..82084eb 100644 --- a/test/orchestrator_test.dart +++ b/test/orchestrator_test.dart @@ -1,4 +1,3 @@ -import "package:autonomy/src/rover/gps.dart"; import "package:test/test.dart"; import "package:autonomy/autonomy.dart"; import "package:burt_network/burt_network.dart"; diff --git a/test/path_test.dart b/test/path_test.dart index 9f43d99..f246b74 100644 --- a/test/path_test.dart +++ b/test/path_test.dart @@ -77,7 +77,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { expect(path, isNotEmpty); for (final step in path) { simulator.logger.trace(step.toString()); - expect(simulator.pathfinder.isObstacle(step.position), isFalse); + expect(simulator.pathfinder.isObstacle(step.endPosition), isFalse); } expect(path.length, 10, reason: "1 turn + 1 forward + 1 turn + 4 forward + 1 45 degree turn + 1 forward + 1 stop = 10 steps total"); await simulator.drive.followPath(path); diff --git a/test/rover_test.dart b/test/rover_test.dart index 2fb4bbb..9621d32 100644 --- a/test/rover_test.dart +++ b/test/rover_test.dart @@ -1,6 +1,6 @@ import "package:test/test.dart"; -import "package:burt_network/generated.dart"; +import "package:burt_network/protobuf.dart"; import "package:burt_network/logging.dart"; import "package:autonomy/interfaces.dart"; @@ -8,7 +8,7 @@ import "package:autonomy/rover.dart"; import "package:autonomy/simulator.dart"; void main() => group("[Rover]", tags: ["rover"], () { - test("Can be restarted", () async { + test("Can be restarted", () async { Logger.level = LogLevel.off; final rover = RoverAutonomy(); await rover.init(); @@ -16,7 +16,7 @@ void main() => group("[Rover]", tags: ["rover"], () { await rover.dispose(); }); - test("Real pathfinding is coherent", () async { + test("Real pathfinding is coherent", () async { Logger.level = LogLevel.off; final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); @@ -69,10 +69,10 @@ Future testPath(AutonomyInterface simulator) async { simulator.logger.trace(" From: ${simulator.gps.coordinates.prettyPrint()} facing ${simulator.imu.heading}"); simulator.logger.debug(" $transition"); await simulator.drive.goDirection(transition.direction); - expect(simulator.gps.isNear(transition.position), isTrue); + expect(simulator.gps.isNear(transition.endPosition), isTrue); simulator.logger.trace("New orientation: ${simulator.imu.heading}"); - simulator.logger.trace("Expected orientation: ${transition.orientation}"); - expect(simulator.imu.orientation, transition.orientation); + simulator.logger.trace("Expected orientation: ${transition.endOrientation}"); + expect(simulator.imu.orientation, transition.endOrientation); } } @@ -92,9 +92,9 @@ Future testPath2(AutonomyInterface simulator) async { simulator.logger.debug(transition.toString()); simulator.logger.trace(" From: ${simulator.gps.coordinates.prettyPrint()}"); await simulator.drive.goDirection(transition.direction); - expect(simulator.gps.isNear(transition.position), isTrue); + expect(simulator.gps.isNear(transition.endPosition), isTrue); expect(simulator.pathfinder.isObstacle(simulator.gps.coordinates), isFalse); - expect(simulator.imu.orientation, transition.orientation); + expect(simulator.imu.orientation, transition.endOrientation); simulator.logger.trace(" To: ${simulator.gps.coordinates.prettyPrint()}"); } } diff --git a/test/sensor_test.dart b/test/sensor_test.dart index 017662a..dce2233 100644 --- a/test/sensor_test.dart +++ b/test/sensor_test.dart @@ -1,12 +1,10 @@ -import "package:autonomy/src/rover/imu.dart"; -import "package:burt_network/logging.dart"; import "package:test/test.dart"; -import "package:burt_network/generated.dart"; +import "package:burt_network/protobuf.dart"; +import "package:burt_network/logging.dart"; import "package:autonomy/interfaces.dart"; import "package:autonomy/simulator.dart"; -import "package:autonomy/src/rover/gps.dart"; const imuError = 2.5; const gpsPrecision = 7; @@ -46,7 +44,7 @@ void main() => group("[Sensors]", tags: ["sensors"], () { final oldError = GpsUtils.maxErrorMeters; GpsUtils.maxErrorMeters = 3; Logger.level = LogLevel.off; - + final simulator = AutonomySimulator(); final realGps = RoverGps(collection: simulator); final simulatedGps = GpsSimulator(collection: simulator, maxError: GpsInterface.gpsError); @@ -84,7 +82,7 @@ void main() => group("[Sensors]", tags: ["sensors"], () { final realImu = RoverImu(collection: simulator); final north = OrientationUtils.north; simulatedImu.update(north); - + var count = 0; for (var i = 0; i < 1000; i++) { final newData = simulatedImu.raw; @@ -101,7 +99,7 @@ void main() => group("[Sensors]", tags: ["sensors"], () { simulator.logger.info("Final orientation: ${realImu.heading}"); expect(percentage, greaterThan(0.75), reason: "IMU should be accurate >75% of the time: $percentage"); - + realImu.update(OrientationUtils.south); expect(realImu.isNear(OrientationUtils.north.heading), isTrue); await simulator.dispose(); @@ -149,7 +147,7 @@ void main() => group("[Sensors]", tags: ["sensors"], () { final realImu = RoverImu(collection: simulator); final orientation = OrientationUtils.north; simulatedImu.update(orientation); - + var count = 0; for (var i = 0; i < 350; i++) { orientation.z += 1; @@ -184,7 +182,7 @@ void main() => group("[Sensors]", tags: ["sensors"], () { simulator.gps.update(utah); expect(simulator.hasValue, isFalse); expect(GpsInterface.currentLatitude, 0); - + await simulator.init(); await simulator.waitForValue(); expect(simulator.hasValue, isTrue); @@ -219,7 +217,7 @@ void main() => group("[Sensors]", tags: ["sensors"], () { final realImu = RoverImu(collection: simulator); final orientation = Orientation(z: 360); simulatedImu.update(orientation); - + var count = 0; for (var i = 0; i < 1000; i++) { final newData = simulatedImu.raw; From b0655a55d7bf1934ebc7299d846cfda9312578c7 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sat, 14 Dec 2024 00:28:56 -0500 Subject: [PATCH 013/110] Corrected directions --- lib/src/imu/cardinal_direction.dart | 8 ++++---- lib/src/utils/gps_utils.dart | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/src/imu/cardinal_direction.dart b/lib/src/imu/cardinal_direction.dart index 40b2521..410e456 100644 --- a/lib/src/imu/cardinal_direction.dart +++ b/lib/src/imu/cardinal_direction.dart @@ -6,10 +6,10 @@ enum CardinalDirection { west(90), south(180), east(270), - northEast(000 + 45), - northWest(360 - 45), - southWest(180 + 45), - southEast(180 - 45); + northEast(0 - 45), + northWest(0 + 45), + southWest(180 - 45), + southEast(180 + 45); final double angle; const CardinalDirection(this.angle); diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 7e56f0e..ac49d42 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -12,14 +12,14 @@ extension GpsUtils on GpsCoordinates { static double get movementLatitude => moveLengthMeters * latitudePerMeter; static double get movementLongitude => moveLengthMeters * longitudePerMeter; - static GpsCoordinates get east => GpsCoordinates(longitude: -movementLongitude); - static GpsCoordinates get west => GpsCoordinates(longitude: movementLongitude); + static GpsCoordinates get east => GpsCoordinates(longitude: movementLongitude); + static GpsCoordinates get west => GpsCoordinates(longitude: -movementLongitude); static GpsCoordinates get north => GpsCoordinates(latitude: movementLatitude); static GpsCoordinates get south => GpsCoordinates(latitude: -movementLatitude); - static GpsCoordinates get northEast => GpsCoordinates(latitude: movementLatitude, longitude: -movementLongitude); - static GpsCoordinates get northWest => GpsCoordinates(latitude: movementLatitude, longitude: movementLongitude); - static GpsCoordinates get southEast => GpsCoordinates(latitude: -movementLatitude, longitude: -movementLongitude); - static GpsCoordinates get southWest => GpsCoordinates(latitude: -movementLatitude, longitude: movementLongitude); + static GpsCoordinates get northEast => GpsCoordinates(latitude: movementLatitude, longitude: movementLongitude); + static GpsCoordinates get northWest => GpsCoordinates(latitude: movementLatitude, longitude: -movementLongitude); + static GpsCoordinates get southEast => GpsCoordinates(latitude: -movementLatitude, longitude: movementLongitude); + static GpsCoordinates get southWest => GpsCoordinates(latitude: -movementLatitude, longitude: -movementLongitude); // Taken from https://stackoverflow.com/a/39540339/9392211 static const metersPerLatitude = 111.32 * 1000; // 111.32 km From fa81bf839f7b2822e9494b6637aeee013b0a34f8 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:25:48 -0500 Subject: [PATCH 014/110] Initial work on fixing unit tests --- lib/src/drive/drive_config.dart | 2 +- lib/src/drive/drive_interface.dart | 5 +- lib/src/drive/rover_drive.dart | 8 +-- lib/src/drive/sensor_drive.dart | 2 +- lib/src/drive/sim_drive.dart | 2 +- lib/src/drive/timed_drive.dart | 2 +- lib/src/gps/gps_interface.dart | 4 ++ lib/src/gps/rover_gps.dart | 4 ++ lib/src/imu/imu_interface.dart | 4 ++ lib/src/imu/rover_imu.dart | 4 ++ lib/src/utils/a_star.dart | 5 +- lib/src/utils/gps_utils.dart | 25 +++++++ lib/src/utils/receiver.dart | 12 ++-- pubspec.lock | 8 ++- test/network_test.dart | 60 ++++++++++------ test/orchestrator_test.dart | 14 ++-- test/path_test.dart | 15 +++- test/rover_test.dart | 18 ++--- test/sensor_test.dart | 106 ++++++++++++++--------------- 19 files changed, 191 insertions(+), 109 deletions(-) diff --git a/lib/src/drive/drive_config.dart b/lib/src/drive/drive_config.dart index 2977da6..dfb2919 100644 --- a/lib/src/drive/drive_config.dart +++ b/lib/src/drive/drive_config.dart @@ -36,5 +36,5 @@ const tankConfig = DriveConfig( turnThrottle: 0.35, turnDelay: Duration(milliseconds: 1000), oneMeterDelay: Duration(milliseconds: 2000), - subsystemsAddress: "localhost", + subsystemsAddress: "127.0.0.1", ); diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index a1f3951..495851e 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -15,9 +15,8 @@ enum DriveDirection { abstract class DriveInterface extends Service { AutonomyInterface collection; - DriveInterface({required this.collection}); - - DriveConfig get config => roverConfig; + DriveConfig config; + DriveInterface({required this.collection, this.config = roverConfig}); Future stop(); diff --git a/lib/src/drive/rover_drive.dart b/lib/src/drive/rover_drive.dart index 434b9a5..60df772 100644 --- a/lib/src/drive/rover_drive.dart +++ b/lib/src/drive/rover_drive.dart @@ -10,14 +10,15 @@ class RoverDrive extends DriveInterface { final bool useGps; final bool useImu; - late final sensorDrive = SensorDrive(collection: collection); - late final timedDrive = TimedDrive(collection: collection); - late final simDrive = DriveSimulator(collection: collection); + late final sensorDrive = SensorDrive(collection: collection, config: config); + late final timedDrive = TimedDrive(collection: collection, config: config); + late final simDrive = DriveSimulator(collection: collection, config: config); RoverDrive({ required super.collection, this.useGps = true, this.useImu = true, + super.config, }); /// Initializes the rover's drive subsystems. @@ -71,7 +72,6 @@ class RoverDrive extends DriveInterface { if (useImu) { await sensorDrive.faceDirection(orientation); } else { - await timedDrive.faceDirection(orientation); await simDrive.faceDirection(orientation); } } diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 22c03d3..da5c316 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -6,7 +6,7 @@ import "drive_commands.dart"; class SensorDrive extends DriveInterface with RoverDriveCommands { static const predicateDelay = Duration(milliseconds: 10); - SensorDrive({required super.collection}); + SensorDrive({required super.collection, super.config}); @override Future stop() async => stopMotors(); diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index 35d6cb2..a04418f 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -4,7 +4,7 @@ class DriveSimulator extends DriveInterface { static const delay = Duration(milliseconds: 500); final bool shouldDelay; - DriveSimulator({required super.collection, this.shouldDelay = false}); + DriveSimulator({required super.collection, this.shouldDelay = false, super.config}); @override Future init() async => true; diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index 1ab997a..bf5f2ab 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -5,7 +5,7 @@ import "package:autonomy/interfaces.dart"; import "drive_commands.dart"; class TimedDrive extends DriveInterface with RoverDriveCommands { - TimedDrive({required super.collection}); + TimedDrive({required super.collection, super.config}); @override Future init() async => true; diff --git a/lib/src/gps/gps_interface.dart b/lib/src/gps/gps_interface.dart index e5f8ae7..e4505b6 100644 --- a/lib/src/gps/gps_interface.dart +++ b/lib/src/gps/gps_interface.dart @@ -1,4 +1,5 @@ import "package:autonomy/interfaces.dart"; +import "package:meta/meta.dart"; abstract class GpsInterface extends Service with Receiver { static const gpsError = 0.00003; @@ -11,6 +12,9 @@ abstract class GpsInterface extends Service with Receiver { double get latitude => coordinates.latitude; void update(GpsCoordinates newValue); + @visibleForTesting + void forceUpdate(GpsCoordinates newValue) {} + GpsCoordinates get coordinates; bool isNear(GpsCoordinates other) => coordinates.isNear(other); diff --git a/lib/src/gps/rover_gps.dart b/lib/src/gps/rover_gps.dart index 80a1cb0..25e60d0 100644 --- a/lib/src/gps/rover_gps.dart +++ b/lib/src/gps/rover_gps.dart @@ -26,6 +26,10 @@ class RoverGps extends GpsInterface { // Do nothing, since this should only be internally updated } + @override + void forceUpdate(GpsCoordinates newValue) => + _internalUpdate(RoverPosition(gps: newValue)); + void _internalUpdate(RoverPosition newValue) { if (!newValue.hasGps()) return; final position = newValue.gps; diff --git a/lib/src/imu/imu_interface.dart b/lib/src/imu/imu_interface.dart index 7fbbf83..2bf1d1f 100644 --- a/lib/src/imu/imu_interface.dart +++ b/lib/src/imu/imu_interface.dart @@ -1,4 +1,5 @@ import "package:autonomy/interfaces.dart"; +import "package:meta/meta.dart"; abstract class ImuInterface extends Service with Receiver { final AutonomyInterface collection; @@ -10,6 +11,9 @@ abstract class ImuInterface extends Service with Receiver { CardinalDirection get nearest => CardinalDirection.nearest(raw); void update(Orientation newValue); + @visibleForTesting + void forceUpdate(Orientation newValue) {} + bool isNear(CardinalDirection direction) => raw.isNear(direction.angle); @override diff --git a/lib/src/imu/rover_imu.dart b/lib/src/imu/rover_imu.dart index 2d9d63a..d9d6728 100644 --- a/lib/src/imu/rover_imu.dart +++ b/lib/src/imu/rover_imu.dart @@ -26,6 +26,10 @@ class RoverImu extends ImuInterface { // Do nothing, since this should only be internally updated } + @override + void forceUpdate(Orientation newValue) => + _internalUpdate(RoverPosition(orientation: newValue)); + void _internalUpdate(RoverPosition newValue) { if (!newValue.hasOrientation()) return; _xCorrector.addValue(newValue.orientation.x); diff --git a/lib/src/utils/a_star.dart b/lib/src/utils/a_star.dart index add8e17..530d52d 100644 --- a/lib/src/utils/a_star.dart +++ b/lib/src/utils/a_star.dart @@ -6,6 +6,9 @@ import "package:autonomy/interfaces.dart"; class AutonomyAStarState extends AStarState { static double getCost(DriveDirection direction) { + if (direction == DriveDirection.stop) { + return 0; + } if (direction == DriveDirection.forward) { return 1; } else if (direction == DriveDirection.quarterLeft || direction == DriveDirection.quarterRight) { @@ -66,7 +69,7 @@ class AutonomyAStarState extends AStarState { }; @override - double heuristic() => position.distanceTo(goal); + double heuristic() => position.heuristicDistance(goal); @override String hash() => "${position.prettyPrint()} ($orientation)"; diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index ac49d42..983dcc5 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -34,6 +34,26 @@ extension GpsUtils on GpsCoordinates { pow(longitude - other.longitude, 2), ); + double heuristicDistance(GpsCoordinates other) { + var steps = 0.0; + final delta = (this - other).inMeters; + final deltaLat = delta.lat.abs(); + final deltaLong = delta.long.abs(); + + final minimumDistance = min(deltaLat, deltaLong); + if (minimumDistance >= moveLengthMeters) { + steps += minimumDistance; + } + + if (deltaLat < deltaLong) { + steps += deltaLong - deltaLat; + } else if (deltaLong < deltaLat) { + steps += deltaLat - deltaLong; + } + + return steps; + } + double manhattanDistance(GpsCoordinates other) => (latitude - other.latitude).abs() * metersPerLatitude + (longitude - other.longitude).abs() * metersPerLongitude; @@ -58,6 +78,11 @@ extension GpsUtils on GpsCoordinates { longitude: longitude + other.longitude, ); + GpsCoordinates operator -(GpsCoordinates other) => GpsCoordinates( + latitude: latitude - other.latitude, + longitude: longitude - other.longitude, + ); + String prettyPrint() => toProto3Json().toString(); GpsCoordinates goForward(CardinalDirection orientation) => this + switch(orientation) { diff --git a/lib/src/utils/receiver.dart b/lib/src/utils/receiver.dart index d301496..c5837d8 100644 --- a/lib/src/utils/receiver.dart +++ b/lib/src/utils/receiver.dart @@ -1,21 +1,19 @@ import "dart:async"; mixin Receiver { - Completer? _completer; + final Completer _completer = Completer(); bool _hasValue = false; set hasValue(bool value) { _hasValue = value; if (!value) return; - _completer?.complete(); - _completer = null; + if (!_completer.isCompleted) { + _completer.complete(true); + } } bool get hasValue => _hasValue; - Future waitForValue() { - _completer = Completer(); - return _completer!.future; - } + Future waitForValue() => _completer.future; } diff --git a/pubspec.lock b/pubspec.lock index f67921e..34878c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,9 +57,11 @@ packages: burt_network: dependency: "direct main" description: - path: "../../burt_network" - relative: true - source: path + path: "." + ref: "2.3.0" + resolved-ref: "07cbb7b9add6e1a5221b696330998899644d5aed" + url: "https://github.com/BinghamtonRover/Networking.git" + source: git version: "2.3.0" collection: dependency: transitive diff --git a/test/network_test.dart b/test/network_test.dart index f6595f5..8c64bb6 100644 --- a/test/network_test.dart +++ b/test/network_test.dart @@ -1,6 +1,8 @@ +import "dart:async"; import "dart:io"; import "package:autonomy/autonomy.dart"; +import "package:autonomy/src/drive/drive_config.dart"; import "package:burt_network/burt_network.dart"; import "package:test/test.dart"; @@ -33,31 +35,40 @@ class MockSubsystems extends Service { if (!enabled) return; if (command.setLeft) left = command.left; if (command.setRight) right = command.right; - if (command.setThrottle) throttle = command.throttle; - if (throttle > 0) throttleFlag = true; + if (command.setThrottle) { + throttle = command.throttle; + throttleFlag = throttle > 0; + } } @override Future dispose() async { - left = 0; right = 0; - throttle = 0; throttleFlag = false; + left = 0; + right = 0; + throttle = 0; + throttleFlag = false; + enabled = false; await socket.dispose(); } } void main() => group("[Network]", tags: ["network"], () { - final subsystems = MockSubsystems(); + var subsystems = MockSubsystems(); final rover = RoverAutonomy(); - rover.drive = SensorlessDrive(collection: rover, useGps: false, useImu: false); + rover.drive = RoverDrive(collection: rover, useGps: false, useImu: false); - Future setup() async { + setUp(() async { Logger.level = LogLevel.off; + await subsystems.dispose(); + subsystems = MockSubsystems(); await subsystems.init(); await rover.init(); - } + }); - setUp(setup); - tearDown(() async { await subsystems.dispose(); await rover.dispose(); }); + tearDown(() async { + await subsystems.dispose(); + await rover.dispose(); + }); test("Rover waits for all data to arrive", () async { final gps = GpsCoordinates(latitude: 1, longitude: 2); @@ -92,18 +103,21 @@ void main() => group("[Network]", tags: ["network"], () { expect(rover.imu.hasValue, isTrue); expect(rover.video.hasValue, isTrue); expect(rover.hasValue, isTrue); + + await Future.delayed(const Duration(seconds: 1)); }); - test("Rover can drive", () async { + test("Rover can drive", retry: 5, () async { subsystems.enabled = true; - ServerUtils.subsystemsDestination = SocketInfo( - address: InternetAddress.loopbackIPv4, - port: 8001, - ); final simulator = AutonomySimulator(); simulator.gps = GpsSimulator(collection: simulator); simulator.imu = ImuSimulator(collection: simulator); - simulator.drive = SensorlessDrive(collection: simulator, useGps: false, useImu: false); + simulator.drive = RoverDrive( + collection: simulator, + useGps: false, + useImu: false, + config: tankConfig, + ); await simulator.init(); final origin = GpsCoordinates(latitude: 0, longitude: 0); @@ -114,11 +128,18 @@ void main() => group("[Network]", tags: ["network"], () { expect(simulator.gps.isNear(origin), isTrue); expect(simulator.gps.isNear(oneMeter), isFalse); - const driveDelay = Duration(milliseconds: 10); expect(subsystems.throttleFlag, isFalse); - await simulator.drive.goForward(); - await Future.delayed(driveDelay); + final forwardFuture = simulator.drive.driveForward(oneMeter); + await Future.delayed(simulator.drive.config.oneMeterDelay * 0.5); expect(subsystems.throttleFlag, isTrue); + expect(subsystems.throttle, isNot(0)); + expect(subsystems.left, isNot(0)); + expect(subsystems.right, isNot(0)); + expect(simulator.gps.isNear(origin), isTrue); + expect(simulator.gps.isNear(oneMeter), isFalse); + await forwardFuture; + await Future.delayed(simulator.drive.config.oneMeterDelay * 0.5); + expect(subsystems.throttleFlag, isFalse); expect(subsystems.throttle, 0); expect(subsystems.left, 0); expect(subsystems.right, 0); @@ -126,6 +147,7 @@ void main() => group("[Network]", tags: ["network"], () { expect(simulator.gps.isNear(oneMeter), isTrue); subsystems.enabled = false; + await subsystems.dispose(); await simulator.dispose(); }); }); diff --git a/test/orchestrator_test.dart b/test/orchestrator_test.dart index 82084eb..f8419b5 100644 --- a/test/orchestrator_test.dart +++ b/test/orchestrator_test.dart @@ -1,9 +1,12 @@ +import "dart:async"; + +import "package:autonomy/src/drive/drive_config.dart"; import "package:test/test.dart"; import "package:autonomy/autonomy.dart"; import "package:burt_network/burt_network.dart"; void main() => group("[Orchestrator]", tags: ["orchestrator"], () { - setUp(() => Logger.level = LogLevel.off); + setUp(() => Logger.level = LogLevel.info); tearDown(() => Logger.level = LogLevel.off); test("Fails for invalid destinations", () async { @@ -94,6 +97,7 @@ void main() => group("[Orchestrator]", tags: ["orchestrator"], () { simulator.orchestrator = RoverOrchestrator(collection: simulator); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.gps = RoverGps(collection: simulator); + simulator.drive = RoverDrive(collection: simulator, useGps: true, useImu: false, config: tankConfig); await simulator.init(); expect(simulator.gps.hasValue, isFalse); @@ -103,13 +107,15 @@ void main() => group("[Orchestrator]", tags: ["orchestrator"], () { expect(GpsInterface.currentLatitude, 0); expect(simulator.orchestrator.statusMessage.state, AutonomyState.NO_SOLUTION); - simulator.gps.update(start); + simulator.gps.forceUpdate(start); await simulator.init(); await simulator.waitForValue(); - await simulator.orchestrator.onCommand(command); expect(simulator.hasValue, isTrue); + unawaited(simulator.orchestrator.onCommand(command)); + await Future.delayed(Duration.zero); + expect(simulator.orchestrator.currentCommand, isNotNull); expect(GpsInterface.currentLatitude, start.latitude); - expect(simulator.orchestrator.currentState, AutonomyState.AT_DESTINATION); + expect(simulator.orchestrator.currentState, AutonomyState.DRIVING); GpsInterface.currentLatitude = 0; await simulator.dispose(); diff --git a/test/path_test.dart b/test/path_test.dart index f246b74..b877658 100644 --- a/test/path_test.dart +++ b/test/path_test.dart @@ -5,6 +5,14 @@ import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; import "package:autonomy/simulator.dart"; +extension DriveFollowPath on DriveInterface { + Future followPath(List path) async { + for (final step in path) { + await driveState(step); + } + } +} + void main() => group("[Pathfinding]", tags: ["path"], () { setUp(() => Logger.level = LogLevel.off); tearDown(() => Logger.level = LogLevel.off); @@ -33,7 +41,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { var turnCount = 0; for (final step in path) { - if (step.direction.isTurn) { + if (step.instruction.isTurn) { turnCount++; } simulator.logger.trace(step.toString()); @@ -77,7 +85,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { expect(path, isNotEmpty); for (final step in path) { simulator.logger.trace(step.toString()); - expect(simulator.pathfinder.isObstacle(step.endPosition), isFalse); + expect(simulator.pathfinder.isObstacle(step.position), isFalse); } expect(path.length, 10, reason: "1 turn + 1 forward + 1 turn + 4 forward + 1 45 degree turn + 1 forward + 1 stop = 10 steps total"); await simulator.drive.followPath(path); @@ -87,7 +95,9 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("Stress test", () async { final oldError = GpsUtils.maxErrorMeters; + final oldMoveLength = GpsUtils.moveLengthMeters; GpsUtils.maxErrorMeters = 1; + // GpsUtils.moveLengthMeters = 5; final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.logger.trace("Starting from ${simulator.gps.coordinates.prettyPrint()}"); @@ -98,6 +108,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { expect(path, isNotNull); await simulator.dispose(); GpsUtils.maxErrorMeters = oldError; + GpsUtils.moveLengthMeters = oldMoveLength; }); test("Impossible paths are reported", () async { diff --git a/test/rover_test.dart b/test/rover_test.dart index 9621d32..2563f64 100644 --- a/test/rover_test.dart +++ b/test/rover_test.dart @@ -37,13 +37,13 @@ void main() => group("[Rover]", tags: ["rover"], () { expect(rover.hasValue, isFalse); expect(rover.gps.hasValue, isFalse); - rover.gps.update(position); + rover.gps.forceUpdate(position); expect(rover.gps.hasValue, isTrue); expect(rover.hasValue, isFalse); expect(rover.hasValue, isFalse); expect(rover.imu.hasValue, isFalse); - rover.imu.update(orientation); + rover.imu.forceUpdate(orientation); expect(rover.imu.hasValue, isTrue); expect(rover.hasValue, isFalse); @@ -68,11 +68,11 @@ Future testPath(AutonomyInterface simulator) async { for (final transition in result) { simulator.logger.trace(" From: ${simulator.gps.coordinates.prettyPrint()} facing ${simulator.imu.heading}"); simulator.logger.debug(" $transition"); - await simulator.drive.goDirection(transition.direction); - expect(simulator.gps.isNear(transition.endPosition), isTrue); + await simulator.drive.driveState(transition); + expect(simulator.gps.isNear(transition.position), isTrue); simulator.logger.trace("New orientation: ${simulator.imu.heading}"); - simulator.logger.trace("Expected orientation: ${transition.endOrientation}"); - expect(simulator.imu.orientation, transition.endOrientation); + simulator.logger.trace("Expected orientation: ${transition.orientation}"); + expect(simulator.imu.nearest, transition.orientation); } } @@ -91,10 +91,10 @@ Future testPath2(AutonomyInterface simulator) async { for (final transition in result) { simulator.logger.debug(transition.toString()); simulator.logger.trace(" From: ${simulator.gps.coordinates.prettyPrint()}"); - await simulator.drive.goDirection(transition.direction); - expect(simulator.gps.isNear(transition.endPosition), isTrue); + await simulator.drive.driveState(transition); + expect(simulator.gps.isNear(transition.position), isTrue); expect(simulator.pathfinder.isObstacle(simulator.gps.coordinates), isFalse); - expect(simulator.imu.orientation, transition.endOrientation); + expect(simulator.imu.nearest, transition.orientation); simulator.logger.trace(" To: ${simulator.gps.coordinates.prettyPrint()}"); } } diff --git a/test/sensor_test.dart b/test/sensor_test.dart index dce2233..69557a1 100644 --- a/test/sensor_test.dart +++ b/test/sensor_test.dart @@ -1,11 +1,9 @@ +import "package:autonomy/autonomy.dart"; import "package:test/test.dart"; import "package:burt_network/protobuf.dart"; import "package:burt_network/logging.dart"; -import "package:autonomy/interfaces.dart"; -import "package:autonomy/simulator.dart"; - const imuError = 2.5; const gpsPrecision = 7; @@ -80,8 +78,8 @@ void main() => group("[Sensors]", tags: ["sensors"], () { final simulator = AutonomySimulator(); final simulatedImu = ImuSimulator(collection: simulator, maxError: imuError); final realImu = RoverImu(collection: simulator); - final north = OrientationUtils.north; - simulatedImu.update(north); + final north = CardinalDirection.north; + simulatedImu.update(north.orientation); var count = 0; for (var i = 0; i < 1000; i++) { @@ -89,10 +87,10 @@ void main() => group("[Sensors]", tags: ["sensors"], () { realImu.update(newData); simulator.logger.trace("Got new value: ${newData.heading}"); simulator.logger.trace(" New heading: ${realImu.heading}"); - simulator.logger.trace(" Real position: ${north.heading}"); + simulator.logger.trace(" Real position: ${north.angle}"); if (i < 10) continue; - simulator.logger.trace(" Values are similar: ${realImu.isNear(north.heading)}"); - if (realImu.isNear(north.heading)) count++; + simulator.logger.trace(" Values are similar: ${realImu.isNear(CardinalDirection.north)}"); + if (realImu.isNear(north)) count++; } final percentage = count / 1000; @@ -100,12 +98,14 @@ void main() => group("[Sensors]", tags: ["sensors"], () { expect(percentage, greaterThan(0.75), reason: "IMU should be accurate >75% of the time: $percentage"); - realImu.update(OrientationUtils.south); - expect(realImu.isNear(OrientationUtils.north.heading), isTrue); + realImu.forceUpdate(OrientationUtils.south); + expect(realImu.isNear(north), isTrue); await simulator.dispose(); }); - test("GPS noise when moving", () async { + test("GPS noise when moving", + skip: "GPS noise is reduced enough with RTK where filtering is not necessary", + () async { // Set up a simulated and real GPS, both starting at (0, 0) final oldError = GpsUtils.maxErrorMeters; GpsUtils.maxErrorMeters = 3; @@ -114,7 +114,7 @@ void main() => group("[Sensors]", tags: ["sensors"], () { final simulatedGps = GpsSimulator(collection: simulator, maxError: GpsInterface.gpsError); var realCoordinates = GpsCoordinates(); simulatedGps.update(realCoordinates); - realGps.update(realCoordinates); + realGps.forceUpdate(realCoordinates); expect(realGps.coordinates.isNear(realCoordinates), isTrue); // For each step forward, use the noisy GPS to update the real GPS. @@ -122,7 +122,7 @@ void main() => group("[Sensors]", tags: ["sensors"], () { for (var step = 0; step < 1000; step++) { realCoordinates += GpsUtils.north; simulatedGps.update(realCoordinates); - realGps.update(simulatedGps.coordinates); + realGps.forceUpdate(simulatedGps.coordinates); simulator.logger.trace("New coordinate: ${realGps.coordinates.latitude.toStringAsFixed(5)} vs real position: ${realCoordinates.latitude.toStringAsFixed(5)}"); simulator.logger.trace(" Difference: ${(realGps.latitude - realCoordinates.latitude).abs().toStringAsFixed(5)} < ${GpsUtils.epsilonLatitude.toStringAsFixed(5)}"); if (step < 10) continue; @@ -140,12 +140,12 @@ void main() => group("[Sensors]", tags: ["sensors"], () { GpsUtils.maxErrorMeters = oldError; }); - test("IMU noise when moving", () async { + test("IMU noise when moving", skip: "IMU is currently accurate enough to not need filtering", () async { Logger.level = LogLevel.off; final simulator = AutonomySimulator(); final simulatedImu = ImuSimulator(collection: simulator, maxError: imuError); final realImu = RoverImu(collection: simulator); - final orientation = OrientationUtils.north; + final orientation = CardinalDirection.north.orientation; simulatedImu.update(orientation); var count = 0; @@ -153,13 +153,13 @@ void main() => group("[Sensors]", tags: ["sensors"], () { orientation.z += 1; simulatedImu.update(orientation); final newData = simulatedImu.raw; - realImu.update(newData); + realImu.forceUpdate(newData); simulator.logger.trace("Got new value: ${newData.heading}"); simulator.logger.trace(" New heading: ${realImu.heading}"); simulator.logger.trace(" Real position: ${orientation.heading}"); if (i < 10) continue; - simulator.logger.trace(" Values are similar: ${realImu.isNear(orientation.heading)}"); - if (realImu.isNear(orientation.heading)) count++; + simulator.logger.trace(" Values are similar: ${realImu.isNear(CardinalDirection.north)}"); + if (realImu.isNear(CardinalDirection.north)) count++; } final percentage = count / 350; @@ -168,9 +168,9 @@ void main() => group("[Sensors]", tags: ["sensors"], () { final badData = Orientation(z: 10); simulator.logger.info("Final orientation: ${realImu.heading}"); simulator.logger.info("Bad orientation: ${badData.heading}"); - realImu.update(badData); + realImu.forceUpdate(badData); simulator.logger.info("Unaffected orientation: ${realImu.heading}"); - expect(realImu.isNear(orientation.heading), isTrue); + expect(realImu.isNear(CardinalDirection.north), isTrue); await simulator.dispose(); }); @@ -179,7 +179,7 @@ void main() => group("[Sensors]", tags: ["sensors"], () { const utahLatitude = 38.406683; final utah = GpsCoordinates(latitude: utahLatitude); - simulator.gps.update(utah); + simulator.gps.forceUpdate(utah); expect(simulator.hasValue, isFalse); expect(GpsInterface.currentLatitude, 0); @@ -203,42 +203,42 @@ void main() => group("[Sensors]", tags: ["sensors"], () { expect(simulator.gps.isNear(newYork), isFalse); expect(ocean.isNear(newYork), isFalse); - simulator.gps.update(newYork); + simulator.gps.forceUpdate(newYork); expect(simulator.gps.isNear(ocean), isFalse); expect(simulator.gps.isNear(newYork), isTrue); await simulator.dispose(); }); - test("IMU can handle values on the edge", () async { - Logger.level = LogLevel.off; - final simulator = AutonomySimulator(); - final simulatedImu = ImuSimulator(collection: simulator, maxError: imuError); - final realImu = RoverImu(collection: simulator); - final orientation = Orientation(z: 360); - simulatedImu.update(orientation); - - var count = 0; - for (var i = 0; i < 1000; i++) { - final newData = simulatedImu.raw; - realImu.update(newData); - simulator.logger.trace("Got new value: ${newData.heading}"); - simulator.logger.trace(" New heading: ${realImu.heading}"); - simulator.logger.trace(" Real position: ${orientation.heading}"); - if (i < 10) continue; - simulator.logger.trace(" Values are similar: ${realImu.isNear(orientation.heading)}"); - if (realImu.isNear(orientation.heading)) count++; - } - - final percentage = count / 1000; - expect(percentage, greaterThan(0.75), reason: "IMU should be accurate >75% of the time: $percentage"); - - final badData = Orientation(z: 10); - simulator.logger.info("Final orientation: ${realImu.heading}"); - simulator.logger.info("Bad orientation: ${badData.heading}"); - realImu.update(badData); - simulator.logger.info("Unaffected orientation: ${realImu.heading}"); - expect(realImu.isNear(orientation.heading), isTrue); - await simulator.dispose(); - }); + // test("IMU can handle values on the edge", () async { + // Logger.level = LogLevel.off; + // final simulator = AutonomySimulator(); + // final simulatedImu = ImuSimulator(collection: simulator, maxError: imuError); + // final realImu = RoverImu(collection: simulator); + // final orientation = Orientation(z: 360); + // simulatedImu.update(orientation); + + // var count = 0; + // for (var i = 0; i < 1000; i++) { + // final newData = simulatedImu.raw; + // realImu.update(newData); + // simulator.logger.trace("Got new value: ${newData.heading}"); + // simulator.logger.trace(" New heading: ${realImu.heading}"); + // simulator.logger.trace(" Real position: ${orientation.heading}"); + // if (i < 10) continue; + // simulator.logger.trace(" Values are similar: ${realImu.isNear(orientation.heading)}"); + // if (realImu.isNear(orientation.heading)) count++; + // } + + // final percentage = count / 1000; + // expect(percentage, greaterThan(0.75), reason: "IMU should be accurate >75% of the time: $percentage"); + + // final badData = Orientation(z: 10); + // simulator.logger.info("Final orientation: ${realImu.heading}"); + // simulator.logger.info("Bad orientation: ${badData.heading}"); + // realImu.update(badData); + // simulator.logger.info("Unaffected orientation: ${realImu.heading}"); + // expect(realImu.isNear(orientation.heading), isTrue); + // await simulator.dispose(); + // }); }); From 1ceb9bde4c485d56fee3c21ecf3ad5bd17ad45f5 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:28:42 -0500 Subject: [PATCH 015/110] Account for move length in heuristic --- lib/src/utils/gps_utils.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 983dcc5..9a5bb6e 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -42,13 +42,13 @@ extension GpsUtils on GpsCoordinates { final minimumDistance = min(deltaLat, deltaLong); if (minimumDistance >= moveLengthMeters) { - steps += minimumDistance; + steps += minimumDistance / moveLengthMeters; } if (deltaLat < deltaLong) { - steps += deltaLong - deltaLat; + steps += (deltaLong - deltaLat) / moveLengthMeters; } else if (deltaLong < deltaLat) { - steps += deltaLat - deltaLong; + steps += (deltaLat - deltaLong) / moveLengthMeters; } return steps; From d0301001b5fd1c5b4618332641bcd343ada7708a Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:45:52 -0500 Subject: [PATCH 016/110] Simplified heuristic calculation (again) --- lib/src/utils/gps_utils.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 9a5bb6e..92736bc 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -45,10 +45,10 @@ extension GpsUtils on GpsCoordinates { steps += minimumDistance / moveLengthMeters; } - if (deltaLat < deltaLong) { - steps += (deltaLong - deltaLat) / moveLengthMeters; - } else if (deltaLong < deltaLat) { - steps += (deltaLat - deltaLong) / moveLengthMeters; + final translationDelta = (deltaLat - deltaLong).abs(); + + if (translationDelta >= moveLengthMeters) { + steps += translationDelta / moveLengthMeters; } return steps; From 4a03f60378a318caf0e61da34537a411fc5f5d38 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:19:42 -0500 Subject: [PATCH 017/110] Fix path following not stopping after restart --- lib/src/orchestrator/rover_orchestrator.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 7d536c8..aecf35f 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -66,6 +66,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { for (final state in path) { collection.logger.debug(state.toString()); await collection.drive.driveState(state); + if (currentCommand == null || currentPath == null) { + collection.logger.debug("Aborting path, command was canceled"); + return; + } traversed.add(state.position); // if (state.direction != DriveDirection.forward) continue; if (count++ == 5) break; From 9657bb4b28305d2ad794fc3c61be1b8b2b627c74 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:20:23 -0500 Subject: [PATCH 018/110] Fixed heuristic math to use only distance --- lib/src/utils/gps_utils.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 92736bc..27710a9 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -35,23 +35,23 @@ extension GpsUtils on GpsCoordinates { ); double heuristicDistance(GpsCoordinates other) { - var steps = 0.0; + var distance = 0.0; final delta = (this - other).inMeters; final deltaLat = delta.lat.abs(); final deltaLong = delta.long.abs(); final minimumDistance = min(deltaLat, deltaLong); if (minimumDistance >= moveLengthMeters) { - steps += minimumDistance / moveLengthMeters; + distance += (minimumDistance / moveLengthMeters) * sqrt2; } final translationDelta = (deltaLat - deltaLong).abs(); if (translationDelta >= moveLengthMeters) { - steps += translationDelta / moveLengthMeters; + distance += translationDelta / moveLengthMeters; } - return steps; + return distance; } double manhattanDistance(GpsCoordinates other) => From 4b50d0af5d17a149b83ab2eb3277bee657264a0c Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:20:33 -0500 Subject: [PATCH 019/110] Add more path tests --- test/path_test.dart | 49 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/test/path_test.dart b/test/path_test.dart index b877658..246703c 100644 --- a/test/path_test.dart +++ b/test/path_test.dart @@ -97,7 +97,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { final oldError = GpsUtils.maxErrorMeters; final oldMoveLength = GpsUtils.moveLengthMeters; GpsUtils.maxErrorMeters = 1; - // GpsUtils.moveLengthMeters = 5; + GpsUtils.moveLengthMeters = 1; final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.logger.trace("Starting from ${simulator.gps.coordinates.prettyPrint()}"); @@ -127,4 +127,51 @@ void main() => group("[Pathfinding]", tags: ["path"], () { expect(path, isNull); await simulator.dispose(); }); + + group("diagonal turns", () { + test("path chooses to move diagonally", () async { + final simulator = AutonomySimulator(); + simulator.pathfinder = RoverPathfinder(collection: simulator); + final destination = (lat: 5, long: 5).toGps(); + final path = simulator.pathfinder.getPath(destination); + expect(path, isNotNull); + expect(path!.where((state) => state.instruction == DriveDirection.forward).length, 5); + expect(path[1].instruction, DriveDirection.quarterRight); + await simulator.dispose(); + }); + + test("doesn't drive through an obstacle", () async { + final simulator = AutonomySimulator(); + simulator.pathfinder = RoverPathfinder(collection: simulator); + final destination = (lat: 5, long: 5).toGps(); + final obstacles = { + (lat: 1, long: 0).toGps(), /* Destination */ + /* Rover */ (lat: 0, long: 1).toGps(), + }; + for (final obstacle in obstacles) { + simulator.pathfinder.recordObstacle(obstacle); + } + final path = simulator.pathfinder.getPath(destination); + expect(path, isNotNull); + expect(path!.where((state) => state.instruction == DriveDirection.forward).length, greaterThan(2)); + await simulator.dispose(); + }); + + test("doesn't drive through an obstacle", () async { + final simulator = AutonomySimulator(); + simulator.pathfinder = RoverPathfinder(collection: simulator); + final destination = (lat: 5, long: 5).toGps(); + final obstacles = { + (lat: 1, long: 0).toGps(), /* Destination */ + /* Rover */ + }; + for (final obstacle in obstacles) { + simulator.pathfinder.recordObstacle(obstacle); + } + final path = simulator.pathfinder.getPath(destination); + expect(path, isNotNull); + expect(path!.where((state) => state.instruction == DriveDirection.forward).length, greaterThan(1)); + await simulator.dispose(); + }); + }); }); From 3bfe8cb266b35608e0f570949e9c3a0e40fb31cc Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 20 Dec 2024 01:02:45 -0500 Subject: [PATCH 020/110] Fixed excessive turns (no idea why this fixes it but whatever) --- lib/src/utils/gps_utils.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 27710a9..8be55e2 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -42,7 +42,7 @@ extension GpsUtils on GpsCoordinates { final minimumDistance = min(deltaLat, deltaLong); if (minimumDistance >= moveLengthMeters) { - distance += (minimumDistance / moveLengthMeters) * sqrt2; + distance += (minimumDistance ~/ moveLengthMeters) * sqrt2; } final translationDelta = (deltaLat - deltaLong).abs(); From ede67745d48bc1a0ab925eb6ec12624759c0fc80 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:50:04 -0500 Subject: [PATCH 021/110] Fixed sensor drive --- lib/src/video/video_interface.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/video/video_interface.dart b/lib/src/video/video_interface.dart index e5923c2..77ea2ad 100644 --- a/lib/src/video/video_interface.dart +++ b/lib/src/video/video_interface.dart @@ -11,6 +11,6 @@ abstract class VideoInterface extends Service with Receiver { void updateFrame(VideoData newData); - // double get arucoSize => data.arucoSize; - // double get arucoPosition => data.arucoPosition; + double get arucoSize => 0; // data.arucoSize; + double get arucoPosition => 0; // data.arucoPosition; } From 0e31908f85888a73351711c223f3fbdedbe27901 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:17:02 -0500 Subject: [PATCH 022/110] Fixed heuristic and duplicate hashing --- lib/src/utils/a_star.dart | 2 +- lib/src/utils/gps_utils.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/utils/a_star.dart b/lib/src/utils/a_star.dart index 530d52d..08fb3d0 100644 --- a/lib/src/utils/a_star.dart +++ b/lib/src/utils/a_star.dart @@ -72,7 +72,7 @@ class AutonomyAStarState extends AStarState { double heuristic() => position.heuristicDistance(goal); @override - String hash() => "${position.prettyPrint()} ($orientation)"; + String hash() => "${position.prettyPrint()} ($orientation) ($instruction)"; @override bool isGoal() => position.isNear(goal, min(GpsUtils.moveLengthMeters, GpsUtils.maxErrorMeters)); diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 8be55e2..ef48939 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -48,7 +48,7 @@ extension GpsUtils on GpsCoordinates { final translationDelta = (deltaLat - deltaLong).abs(); if (translationDelta >= moveLengthMeters) { - distance += translationDelta / moveLengthMeters; + distance += translationDelta ~/ moveLengthMeters; } return distance; From 64bf1e31f0eb9d84f043a1f5a193e0332b91b62e Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 24 Jan 2025 10:09:04 -0500 Subject: [PATCH 023/110] Added path optimizer and more unit tests --- lib/src/drive/drive_interface.dart | 2 + lib/src/pathfinding/rover_pathfinding.dart | 32 ++++++++++- test/path_test.dart | 64 ++++++++++++++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index 495851e..7fc0ce4 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -11,6 +11,8 @@ enum DriveDirection { stop; bool get isTurn => this != forward && this != stop; + + bool get isQuarterTurn => this == quarterLeft || this == quarterRight; } abstract class DriveInterface extends Service { diff --git a/lib/src/pathfinding/rover_pathfinding.dart b/lib/src/pathfinding/rover_pathfinding.dart index a72d279..afe429d 100644 --- a/lib/src/pathfinding/rover_pathfinding.dart +++ b/lib/src/pathfinding/rover_pathfinding.dart @@ -8,13 +8,41 @@ class RoverPathfinder extends PathfindingInterface { @override Future init() async => true; + List optimizePath(Iterable original) { + final newPath = []; + AutonomyAStarState? previous; + for (final step in original) { + if (previous != null && + step.instruction.isQuarterTurn && + previous.instruction.isQuarterTurn && + step.instruction == previous.instruction) { + newPath.last = AutonomyAStarState( + position: previous.position, + goal: previous.goal, + collection: collection, + instruction: step.instruction == DriveDirection.quarterLeft + ? DriveDirection.left + : DriveDirection.right, + orientation: step.orientation, + depth: step.depth, + ); + } else { + newPath.add(step); + } + previous = newPath.last; + } + + return newPath; + } + @override List? getPath(GpsCoordinates destination, {bool verbose = false}) { if (isObstacle(destination)) return null; final state = AutonomyAStarState.start(collection: collection, goal: destination); final result = aStar(state, verbose: verbose, limit: 50000); if (result == null) return null; - final transitions = result.reconstructPath().toList(); - return transitions; + final transitions = result.reconstructPath(); + final optimized = optimizePath(transitions); + return optimized; } } diff --git a/test/path_test.dart b/test/path_test.dart index 246703c..a1dd043 100644 --- a/test/path_test.dart +++ b/test/path_test.dart @@ -1,3 +1,5 @@ +import "dart:math"; + import "package:burt_network/burt_network.dart"; import "package:test/test.dart"; @@ -174,4 +176,66 @@ void main() => group("[Pathfinding]", tags: ["path"], () { await simulator.dispose(); }); }); + + group("optimizer", () { + test("replaces equal and duplicate quarter turns", () async { + final simulator = AutonomySimulator(); + final pathfinder = RoverPathfinder(collection: simulator); + simulator.pathfinder = pathfinder; + + final originalPath = [ + AutonomyAStarState( + position: (lat: 0, long: 0).toGps(), + goal: (lat: 0, long: 0).toGps(), + collection: simulator, + instruction: DriveDirection.quarterLeft, + orientation: CardinalDirection.northEast, + depth: sqrt2, + ), + AutonomyAStarState( + position: (lat: 0, long: 0).toGps(), + goal: (lat: 0, long: 0).toGps(), + collection: simulator, + instruction: DriveDirection.quarterLeft, + orientation: CardinalDirection.east, + depth: sqrt2, + ), + ]; + final optimizedPath = pathfinder.optimizePath(originalPath); + expect(optimizedPath.length, 1); + expect(optimizedPath.first.instruction, DriveDirection.left); + expect(optimizedPath.first.orientation, CardinalDirection.east); + await simulator.dispose(); + }); + + test("does not replace non-equal turns", () async { + final simulator = AutonomySimulator(); + final pathfinder = RoverPathfinder(collection: simulator); + simulator.pathfinder = pathfinder; + + final originalPath = [ + AutonomyAStarState( + position: (lat: 0, long: 0).toGps(), + goal: (lat: 0, long: 0).toGps(), + collection: simulator, + instruction: DriveDirection.quarterLeft, + orientation: CardinalDirection.northEast, + depth: sqrt2, + ), + AutonomyAStarState( + position: (lat: 0, long: 0).toGps(), + goal: (lat: 0, long: 0).toGps(), + collection: simulator, + instruction: DriveDirection.quarterRight, + orientation: CardinalDirection.north, + depth: sqrt2, + ), + ]; + final optimizedPath = pathfinder.optimizePath(originalPath); + expect(optimizedPath.length, 2); + expect(optimizedPath.first.instruction, DriveDirection.quarterLeft); + expect(optimizedPath.first.orientation, CardinalDirection.northEast); + await simulator.dispose(); + }); + }); }); From 0dffb69ca7cc510b90547f23cbc0ae5f665a5a74 Mon Sep 17 00:00:00 2001 From: Binghamton Rover Date: Fri, 24 Jan 2025 06:10:45 -0500 Subject: [PATCH 024/110] Initial testing on rover --- lib/src/drive/drive_config.dart | 2 +- lib/src/drive/sensor_drive.dart | 33 +++++++++++++------- lib/src/imu/cardinal_direction.dart | 6 ++-- lib/src/orchestrator/rover_orchestrator.dart | 1 + lib/src/pathfinding/rover_pathfinding.dart | 4 +-- lib/src/utils/imu_utils.dart | 24 +++++++++----- 6 files changed, 45 insertions(+), 25 deletions(-) diff --git a/lib/src/drive/drive_config.dart b/lib/src/drive/drive_config.dart index dfb2919..24631af 100644 --- a/lib/src/drive/drive_config.dart +++ b/lib/src/drive/drive_config.dart @@ -25,7 +25,7 @@ class DriveConfig { const roverConfig = DriveConfig( forwardThrottle: 0.1, - turnThrottle: 0.1, + turnThrottle: 0.05, oneMeterDelay: Duration(milliseconds: 5500), turnDelay: Duration(milliseconds: 4500), subsystemsAddress: "192.168.1.20", diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index da5c316..5c3ba3a 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -43,19 +43,30 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { bool _tryToFace(CardinalDirection orientation) { final current = collection.imu.heading; final target = orientation.angle; - if ((current - target).abs() < 180) { - if (current < target) { - spinRight(); - } else { - spinLeft(); - } + var error = target - current; + if (error < -180) { + error += 360; + } else if (error > 180) { + error -= 360; + } + if (error < 0) { + spinLeft(); } else { - if (current < target) { - spinLeft(); - } else { - spinRight(); - } + spinRight(); } + // if (error.abs() < 180) { + // if (current < target) { + // spinRight(); + // } else { + // spinLeft(); + // } + // } else { + // if (current < target) { + // spinLeft(); + // } else { + // spinRight(); + // } + // } collection.logger.trace("Current heading: $current"); return collection.imu.isNear(orientation); } diff --git a/lib/src/imu/cardinal_direction.dart b/lib/src/imu/cardinal_direction.dart index 410e456..213d743 100644 --- a/lib/src/imu/cardinal_direction.dart +++ b/lib/src/imu/cardinal_direction.dart @@ -6,7 +6,7 @@ enum CardinalDirection { west(90), south(180), east(270), - northEast(0 - 45), + northEast(360 - 45), northWest(0 + 45), southWest(180 - 45), southEast(180 + 45); @@ -22,8 +22,8 @@ enum CardinalDirection { for (final value in values) { final diff = (value.angle - orientation.z).clampAngle(); - if (diff < smallestDiff) { - smallestDiff = diff; + if (diff.abs() < smallestDiff) { + smallestDiff = diff.abs(); closestOrientation = value; } } diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index aecf35f..5215d80 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -62,6 +62,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.logger.debug(step.toString()); } currentState = AutonomyState.DRIVING; + await collection.drive.faceDirection(path.first.orientation); var count = 0; for (final state in path) { collection.logger.debug(state.toString()); diff --git a/lib/src/pathfinding/rover_pathfinding.dart b/lib/src/pathfinding/rover_pathfinding.dart index afe429d..f1893e6 100644 --- a/lib/src/pathfinding/rover_pathfinding.dart +++ b/lib/src/pathfinding/rover_pathfinding.dart @@ -42,7 +42,7 @@ class RoverPathfinder extends PathfindingInterface { final result = aStar(state, verbose: verbose, limit: 50000); if (result == null) return null; final transitions = result.reconstructPath(); - final optimized = optimizePath(transitions); - return optimized; + // final optimized = optimizePath(transitions); + return transitions.toList(); } } diff --git a/lib/src/utils/imu_utils.dart b/lib/src/utils/imu_utils.dart index 5a45f9a..6825e38 100644 --- a/lib/src/utils/imu_utils.dart +++ b/lib/src/utils/imu_utils.dart @@ -1,9 +1,9 @@ import "package:burt_network/protobuf.dart"; extension OrientationUtils on Orientation { - static const double epsilon = 3.5; + static const double epsilon = 5; static const double orientationEpsilon = 10; - + static final north = Orientation(z: 0); static final west = Orientation(z: 90); static final south = Orientation(z: 180); @@ -14,13 +14,21 @@ extension OrientationUtils on Orientation { bool get isEmpty => x == 0 && y == 0 && z == 0; bool isNear(double value) { - if (value > 270 && z < 90) { - return (z + 360 - value).abs() < epsilon; - } else if (value < 90 && z > 270) { - return (z - value - 360).abs() < epsilon; - } else { - return (z - value).abs() < epsilon; + var error = value - z; + if (error > 180) { + error -= 360; + } else if (error < -180) { + error += 360; } + + return error.abs() < epsilon; + // if (value > 270 && z < 90) { + // return (z + 360 - value).abs() < epsilon; + // } else if (value < 90 && z > 270) { + // return (z - value - 360).abs() < epsilon; + // } else { + // return (z - value).abs() < epsilon; + // } } } From 5a5c1ff3e1774c965e5529d1176b2cf3af26d8ff Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:28:18 -0500 Subject: [PATCH 025/110] Fixes from testing - Clamp all angles from -180 to 180 - Face the correct direction before driving forward Tested with sim GPS and real IMU* --- bin/task.dart | 8 ++-- lib/src/drive/sensor_drive.dart | 17 ++++---- lib/src/imu/cardinal_direction.dart | 8 ++-- lib/src/imu/rover_imu.dart | 6 +-- lib/src/orchestrator/rover_orchestrator.dart | 7 +++- lib/src/utils/gps_utils.dart | 8 ++-- lib/src/utils/imu_utils.dart | 43 ++++++++++++-------- 7 files changed, 52 insertions(+), 45 deletions(-) diff --git a/bin/task.dart b/bin/task.dart index 628f823..8823873 100644 --- a/bin/task.dart +++ b/bin/task.dart @@ -12,13 +12,13 @@ final obstacles = [ void main() async { Logger.level = LogLevel.debug; final simulator = RoverAutonomy(); - simulator.detector = DetectorSimulator(collection: simulator, obstacles: obstacles); + simulator.detector = NetworkDetector(collection: simulator); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.orchestrator = RoverOrchestrator(collection: simulator); - // simulator.drive = RoverDrive(collection: simulator, useImu: true, useGps: false); + simulator.drive = RoverDrive(collection: simulator, useGps: false, useImu: false); simulator.gps = GpsSimulator(collection: simulator); - simulator.imu = ImuSimulator(collection: simulator); - simulator.drive = DriveSimulator(collection: simulator, shouldDelay: true); + simulator.video = VideoSimulator(collection: simulator); + await simulator.init(); await simulator.imu.waitForValue(); await simulator.server.waitForConnection(); diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 5c3ba3a..a90cb3b 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -27,8 +27,10 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { Future driveForward(GpsCoordinates position) async { collection.logger.info("Driving forward one meter"); setThrottle(config.forwardThrottle); - moveForward(); - await waitFor(() => collection.gps.isNear(position)); + await waitFor(() { + moveForward(); + return collection.gps.isNear(position); + }); await stop(); } @@ -43,16 +45,11 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { bool _tryToFace(CardinalDirection orientation) { final current = collection.imu.heading; final target = orientation.angle; - var error = target - current; - if (error < -180) { - error += 360; - } else if (error > 180) { - error -= 360; - } + final error = (target - current).clampHalfAngle(); if (error < 0) { - spinLeft(); - } else { spinRight(); + } else { + spinLeft(); } // if (error.abs() < 180) { // if (current < target) { diff --git a/lib/src/imu/cardinal_direction.dart b/lib/src/imu/cardinal_direction.dart index 213d743..68017c6 100644 --- a/lib/src/imu/cardinal_direction.dart +++ b/lib/src/imu/cardinal_direction.dart @@ -5,9 +5,9 @@ enum CardinalDirection { north(0), west(90), south(180), - east(270), - northEast(360 - 45), - northWest(0 + 45), + east(-90), + northEast(-45), + northWest(45), southWest(180 - 45), southEast(180 + 45); @@ -21,7 +21,7 @@ enum CardinalDirection { var closestOrientation = CardinalDirection.north; for (final value in values) { - final diff = (value.angle - orientation.z).clampAngle(); + final diff = (value.angle - orientation.z).clampHalfAngle(); if (diff.abs() < smallestDiff) { smallestDiff = diff.abs(); closestOrientation = value; diff --git a/lib/src/imu/rover_imu.dart b/lib/src/imu/rover_imu.dart index d9d6728..d16dbcb 100644 --- a/lib/src/imu/rover_imu.dart +++ b/lib/src/imu/rover_imu.dart @@ -40,8 +40,8 @@ class RoverImu extends ImuInterface { @override Orientation get raw => Orientation( - x: _xCorrector.calibratedValue.clampAngle(), - y: _yCorrector.calibratedValue.clampAngle(), - z: _zCorrector.calibratedValue.clampAngle(), + x: _xCorrector.calibratedValue.clampHalfAngle(), + y: _yCorrector.calibratedValue.clampHalfAngle(), + z: _zCorrector.calibratedValue.clampHalfAngle(), ); } diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 5215d80..c303e5d 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -62,10 +62,13 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.logger.debug(step.toString()); } currentState = AutonomyState.DRIVING; - await collection.drive.faceDirection(path.first.orientation); var count = 0; for (final state in path) { collection.logger.debug(state.toString()); + if (state.instruction == DriveDirection.forward && + !collection.imu.raw.isNear(state.orientation.angle, OrientationUtils.driveRealignmentEpsilon)) { + await collection.drive.faceDirection(state.orientation); + } await collection.drive.driveState(state); if (currentCommand == null || currentPath == null) { collection.logger.debug("Aborting path, command was canceled"); @@ -73,7 +76,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } traversed.add(state.position); // if (state.direction != DriveDirection.forward) continue; - if (count++ == 5) break; + if (++count == 5) break; final foundObstacle = collection.detector.findObstacles(); if (foundObstacle) { collection.logger.debug("Found an obstacle. Recalculating path..."); diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index ef48939..9831d2a 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -16,10 +16,10 @@ extension GpsUtils on GpsCoordinates { static GpsCoordinates get west => GpsCoordinates(longitude: -movementLongitude); static GpsCoordinates get north => GpsCoordinates(latitude: movementLatitude); static GpsCoordinates get south => GpsCoordinates(latitude: -movementLatitude); - static GpsCoordinates get northEast => GpsCoordinates(latitude: movementLatitude, longitude: movementLongitude); - static GpsCoordinates get northWest => GpsCoordinates(latitude: movementLatitude, longitude: -movementLongitude); - static GpsCoordinates get southEast => GpsCoordinates(latitude: -movementLatitude, longitude: movementLongitude); - static GpsCoordinates get southWest => GpsCoordinates(latitude: -movementLatitude, longitude: -movementLongitude); + static GpsCoordinates get northEast => north + east; + static GpsCoordinates get northWest => north + west; + static GpsCoordinates get southEast => south + east; + static GpsCoordinates get southWest => south + west; // Taken from https://stackoverflow.com/a/39540339/9392211 static const metersPerLatitude = 111.32 * 1000; // 111.32 km diff --git a/lib/src/utils/imu_utils.dart b/lib/src/utils/imu_utils.dart index 6825e38..e40073e 100644 --- a/lib/src/utils/imu_utils.dart +++ b/lib/src/utils/imu_utils.dart @@ -1,27 +1,30 @@ -import "package:burt_network/protobuf.dart"; +import "package:autonomy/autonomy.dart"; extension OrientationUtils on Orientation { - static const double epsilon = 5; - static const double orientationEpsilon = 10; - - static final north = Orientation(z: 0); - static final west = Orientation(z: 90); - static final south = Orientation(z: 180); - static final east = Orientation(z: 270); + /// The IMU angle tolerance for a turn during autonomy + static const double turnEpsilon = 5; + /// The IMU angle tolerance when turning to re-correct to the + /// proper orientation before driving forward + static const double driveRealignmentEpsilon = 8; - double get heading => z; + /// North orientation + static final north = Orientation(z: CardinalDirection.north.angle); + /// East orientation + static final west = Orientation(z: CardinalDirection.west.angle); + /// South Orientation + static final south = Orientation(z: CardinalDirection.south.angle); + /// East orientation + static final east = Orientation(z: CardinalDirection.east.angle); - bool get isEmpty => x == 0 && y == 0 && z == 0; + /// The heading of the orientation, or the compass direction we are facing + double get heading => z; - bool isNear(double value) { - var error = value - z; - if (error > 180) { - error -= 360; - } else if (error < -180) { - error += 360; - } + /// Whether or not this orientation is within [epsilon] degrees of [value] + bool isNear(double value, [double? epsilon]) { + epsilon ??= OrientationUtils.turnEpsilon; + final error = (value - z).clampHalfAngle(); - return error.abs() < epsilon; + return error.abs() <= epsilon; // if (value > 270 && z < 90) { // return (z + 360 - value).abs() < epsilon; // } else if (value < 90 && z > 270) { @@ -32,6 +35,10 @@ extension OrientationUtils on Orientation { } } +/// Utility methods for an angle extension AngleUtils on double { + /// The angle clamped between (-180, 180) + double clampHalfAngle() => ((this + 180) % 360) - 180; + /// The angle clamped between (0, 360) double clampAngle() => ((this % 360) + 360) % 360; } From eb14e611a6c2ceebeab2de1ba2f4decbacf9372e Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:04:52 -0500 Subject: [PATCH 026/110] Began documenting --- lib/src/drive/drive_commands.dart | 5 ++++ lib/src/drive/drive_config.dart | 17 ++++++++++++++ lib/src/drive/drive_interface.dart | 37 ++++++++++++++++++++++++++++-- lib/src/drive/rover_drive.dart | 13 +++++++++++ lib/src/drive/sensor_drive.dart | 11 +++++++++ lib/src/drive/sim_drive.dart | 12 +++++++++- lib/src/drive/timed_drive.dart | 26 +++++++++++++++++++++ 7 files changed, 118 insertions(+), 3 deletions(-) diff --git a/lib/src/drive/drive_commands.dart b/lib/src/drive/drive_commands.dart index 94f8889..3b149e7 100644 --- a/lib/src/drive/drive_commands.dart +++ b/lib/src/drive/drive_commands.dart @@ -1,5 +1,6 @@ import "package:autonomy/interfaces.dart"; +/// Utility methods for a [DriveInterface] to send motor commands directly to the Subsystems program mixin RoverDriveCommands on DriveInterface { /// Sets the max speed of the rover. /// @@ -21,13 +22,17 @@ mixin RoverDriveCommands on DriveInterface { sendCommand(DriveCommand(right: right, setRight: true)); } + /// Stops the motors, setting the throttle and speeds to 0 void stopMotors() { setThrottle(0); _setSpeeds(left: 0, right: 0); } + /// Sets the speeds of the wheels to spin the rover left void spinLeft() => _setSpeeds(left: -1, right: 1); + /// Sets the speeds of the wheels to spin the rover right void spinRight() => _setSpeeds(left: 1, right: -1); + /// Sets the speeds of the wheels to move the rover in a straight line void moveForward() => _setSpeeds(left: 1, right: 1); /// Sets the angle of the front camera. diff --git a/lib/src/drive/drive_config.dart b/lib/src/drive/drive_config.dart index 24631af..f11d1e3 100644 --- a/lib/src/drive/drive_config.dart +++ b/lib/src/drive/drive_config.dart @@ -2,13 +2,27 @@ import "dart:io"; import "package:burt_network/burt_network.dart"; +/// Configuration for the Rover's drive behavior +/// +/// Each rover or device that is tested will have different behaviors +/// depending on the mechanical and electrical setup, ranging from subsystems +/// program address, to drive speeds, or the time it takes to move forward +/// and turn. +/// +/// This class is to make testing on different devices easier class DriveConfig { + /// The throttle to set the drive to when moving forward final double forwardThrottle; + /// The throttle to set the drive to when turning final double turnThrottle; + /// The time it takes to turn 90 degrees final Duration turnDelay; + /// The time it takes to move one meter forward final Duration oneMeterDelay; + /// The IP address for the subsystems program final String subsystemsAddress; + /// Constructor for DriveConfig initializing all final fields const DriveConfig({ required this.forwardThrottle, required this.turnThrottle, @@ -17,12 +31,14 @@ class DriveConfig { required this.subsystemsAddress, }); + /// The [SocketInfo] for Subsystems, created using [subsystemsAddress] and port 8001 SocketInfo get subsystems => SocketInfo( address: InternetAddress(subsystemsAddress), port: 8001, ); } +/// The [DriveConfig] for the rover const roverConfig = DriveConfig( forwardThrottle: 0.1, turnThrottle: 0.05, @@ -31,6 +47,7 @@ const roverConfig = DriveConfig( subsystemsAddress: "192.168.1.20", ); +/// The [DriveConfig] for the tank const tankConfig = DriveConfig( forwardThrottle: 0.3, turnThrottle: 0.35, diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index 7fc0ce4..ab2d0cc 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -2,33 +2,57 @@ import "package:autonomy/interfaces.dart"; import "drive_config.dart"; +/// An enum for the drive direction or "instruction" for how the rover should drive enum DriveDirection { + /// Move forward forward, + /// Turn 90 degrees left left, + /// Turn 90 degrees right right, + /// Turn 45 degrees left quarterLeft, + /// Turn 45 degrees right quarterRight, + /// Stop moving stop; + /// Whether or not this is a turn bool get isTurn => this != forward && this != stop; + /// Whether or not this is a quarter turn of 45 degrees bool get isQuarterTurn => this == quarterLeft || this == quarterRight; } +/// An abstract class for driving. +/// +/// This allows for easy stubbing to simulate drive if certain sensors are not used. abstract class DriveInterface extends Service { + /// The autonomy collection of the rover's sensors, pathfinders, loggers, and UDP sockets AutonomyInterface collection; + + /// The drive configuration for the rover it is running on DriveConfig config; + + /// Constructor for Drive Interface DriveInterface({required this.collection, this.config = roverConfig}); + /// Stop the rover Future stop(); + /// Drive forward to [position] Future driveForward(GpsCoordinates position); + /// Turn to face [orientation] Future faceDirection(CardinalDirection orientation); - void sendCommand(Message message) => collection.server - .sendMessage(message, destination: config.subsystems); + /// Utility method to send a command to subsystems + void sendCommand(Message message) => collection.server.sendMessage(message, destination: config.subsystems); + /// Spins the rover to the nearest IMU rotation + /// + /// This exists so the rover can generate a path based on a known + /// orientation that aligns to the possible orientations defined by [CardinalDirection] Future resolveOrientation() => faceDirection(collection.imu.nearest); /// Turns to face the state's [AutonomyAStarState.orientation]. @@ -36,6 +60,9 @@ abstract class DriveInterface extends Service { /// Exists so that the TimedDrive can implement this in terms of [AutonomyAStarState.instruction]. Future turnState(AutonomyAStarState state) => faceDirection(state.orientation); + /// Drives the rover based on the instruction and desired positions in [state] + /// + /// This determines based on the [state] whether it should move forward, turn, or stop Future driveState(AutonomyAStarState state) { if (state.instruction == DriveDirection.stop) { return stop(); @@ -46,11 +73,17 @@ abstract class DriveInterface extends Service { } } + /// Utility method to send a command to change the color of the LED strip + /// + /// This is used to signal the state of the autonomous driving outside of the rover void setLedStrip(ProtoColor color, {bool blink = false}) { final command = DriveCommand(color: color, blink: blink ? BoolState.YES : BoolState.NO); sendCommand(command); } + /// Spin to face an Aruco tag, returns whether or not it was able to face the tag Future spinForAruco() async => false; + + /// Drive forward to approach an Aruco tag Future approachAruco() async { } } diff --git a/lib/src/drive/rover_drive.dart b/lib/src/drive/rover_drive.dart index 60df772..14358ef 100644 --- a/lib/src/drive/rover_drive.dart +++ b/lib/src/drive/rover_drive.dart @@ -7,13 +7,26 @@ import "sim_drive.dart"; /// A helper class to send drive commands to the rover with a simpler API. class RoverDrive extends DriveInterface { + /// Whether or not it should use the GPS while driving forward final bool useGps; + + /// Whether or not it should use the IMU while turning final bool useImu; + /// A [SensorDrive] used during movements that rely on sensors late final sensorDrive = SensorDrive(collection: collection, config: config); + + /// A [TimedDrive] used during movements that do not use sensors late final timedDrive = TimedDrive(collection: collection, config: config); + + /// A simulator used in conjunction with [timedDrive] to simulate the sensor + /// readings of the drive when driving without sensors late final simDrive = DriveSimulator(collection: collection, config: config); + /// Constructor for RoverDrive + /// + /// Takes in parameters for whether or not to use the GPS and imu + /// These will determine when to use [sensorDrive] or [timedDrive] RoverDrive({ required super.collection, this.useGps = true, diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index a90cb3b..c3c84bf 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -3,14 +3,25 @@ import "package:autonomy/interfaces.dart"; import "drive_commands.dart"; +/// An implementation of [DriveInterface] that uses the rover's sensors to +/// determine its direction to move in and whether or not it has moved in its +/// desired direction/orientation +/// +/// When this is driving, it assumes that the rover is constantly getting new sensor +/// readings, if not, this will continue moving indefinitely class SensorDrive extends DriveInterface with RoverDriveCommands { + /// The default period to check for a condition to become true static const predicateDelay = Duration(milliseconds: 10); + /// Default constructor for SensorDrive SensorDrive({required super.collection, super.config}); @override Future stop() async => stopMotors(); + /// Will periodically check for a condition to become true. This can be + /// thought of as a "wait until", where the rover will periodically check + /// if it has reached its desired position or orientation Future waitFor(bool Function() predicate) async { while (!predicate()) { await Future.delayed(predicateDelay); diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index a04418f..6507d9c 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -1,9 +1,19 @@ import "package:autonomy/interfaces.dart"; +/// An implementation of [DriveInterface] that will not move the rover, +/// and only update its sensor readings based on the desired values +/// +/// This assumes that the implementations for sensors are not expected to be updated from the rover, +/// otherwise, this can cause the rover to not follow its path properly class DriveSimulator extends DriveInterface { + /// The amount of time to wait before updating the virtual sensor readings static const delay = Duration(milliseconds: 500); - + + /// Whether or not to wait before updating virtual sensor readings, + /// this can be useful when simulating the individual steps of a path final bool shouldDelay; + + /// Constructor for DriveSimulator, initializing the default fields, and whether or not it should delay DriveSimulator({required super.collection, this.shouldDelay = false, super.config}); @override diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index bf5f2ab..9a937a2 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -1,9 +1,15 @@ import "dart:math"; import "package:autonomy/interfaces.dart"; +import "package:autonomy/src/drive/drive_config.dart"; import "drive_commands.dart"; +/// An implementation of [DriveInterface] that drives for a specified amount of time without using sensors +/// +/// The time to drive/turn for is defined by [DriveConfig.oneMeterDelay] and [DriveConfig.turnDelay] +/// +/// This should only be used if the rover is not using sensors for autonomous driving class TimedDrive extends DriveInterface with RoverDriveCommands { TimedDrive({required super.collection, super.config}); @@ -37,6 +43,10 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { @override Future stop() async => stopMotors(); + /// Moves forward for the amount of time it will take to drive the specified [distance] + /// + /// This will set the speeds to move forward, and wait for the amount of + /// time specified by the [DriveConfig.oneMeterDelay] Future goForward([double distance = 1]) async { collection.logger.info("Driving forward $distance meters"); setThrottle(config.forwardThrottle); @@ -45,6 +55,10 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { await stop(); } + /// Turns left for the amount of time it will take to spin left 90 degrees + /// + /// This will set the speeds to turn left, and wait for the amount of + /// time specified by the [DriveConfig.turnDelay] Future turnLeft() async { setThrottle(config.turnThrottle); spinLeft(); @@ -52,6 +66,10 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { await stop(); } + /// Turns right for the amount of time it will take to spin right 90 degrees + /// + /// This will set the speeds to turn right, and wait for the amount of + /// time defined by the [DriveConfig.turnDelay] Future turnRight() async { setThrottle(config.turnThrottle); spinRight(); @@ -59,6 +77,10 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { await stop(); } + /// Turns left for the amount of time it will take to spin left 45 degrees + /// + /// This will set the speeds to turn left, and wait for the amount of + /// time defined by the [DriveConfig.turnDelay] / 2 Future turnQuarterLeft() async { setThrottle(config.turnThrottle); spinLeft(); @@ -66,6 +88,10 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { await stop(); } + /// Turns right for the amount of time it will take to spin right 45 degrees + /// + /// This will set the speeds to turn right, and wait for the amount of + /// time defined by the [DriveConfig.turnDelay] / 2 Future turnQuarterRight() async { setThrottle(config.turnThrottle); spinRight(); From a7264a28a202266c75689f499c523004bb2dd8e4 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 31 Jan 2025 16:43:28 -0500 Subject: [PATCH 027/110] Use sim imu in task --- bin/task.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/task.dart b/bin/task.dart index 8823873..3736494 100644 --- a/bin/task.dart +++ b/bin/task.dart @@ -17,6 +17,7 @@ void main() async { simulator.orchestrator = RoverOrchestrator(collection: simulator); simulator.drive = RoverDrive(collection: simulator, useGps: false, useImu: false); simulator.gps = GpsSimulator(collection: simulator); + simulator.imu = ImuSimulator(collection: simulator); simulator.video = VideoSimulator(collection: simulator); await simulator.init(); From d55748ecf923ff134d0a6605199d2ffee5140bf4 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:44:35 -0500 Subject: [PATCH 028/110] Use meters for goForward() --- lib/src/utils/gps_utils.dart | 76 +++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 9831d2a..90bf807 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -3,6 +3,24 @@ import "dart:math"; import "package:autonomy/interfaces.dart"; +/// An alias for gps coordinates measured in meters +typedef GpsMeters = ({num lat, num long}); + +/// Utility math methods for GpsMeters +extension GpsMetersUtil on GpsMeters { + /// Add 2 [GpsMeters] together + GpsMeters operator +(GpsMeters other) => ( + lat: lat + other.lat, + long: long + other.long, + ); + + /// Subtract 2 [GpsMeters] from each other + GpsMeters operator -(GpsMeters other) => ( + lat: lat - other.lat, + long: long - other.long, + ); +} + extension GpsUtils on GpsCoordinates { static double maxErrorMeters = 0.5; static double moveLengthMeters = 1; @@ -21,6 +39,15 @@ extension GpsUtils on GpsCoordinates { static GpsCoordinates get southEast => south + east; static GpsCoordinates get southWest => south + west; + static GpsMeters get eastMeters => (lat: 0, long: moveLengthMeters); + static GpsMeters get westMeters => (lat: 0, long: -moveLengthMeters); + static GpsMeters get northMeters => (lat: moveLengthMeters, long: 0); + static GpsMeters get southMeters => (lat: -moveLengthMeters, long: 0); + static GpsMeters get northEastMeters => northMeters + eastMeters; + static GpsMeters get northWestMeters => northMeters + westMeters; + static GpsMeters get southEastMeters => southMeters + eastMeters; + static GpsMeters get southWestMeters => southMeters + westMeters; + // Taken from https://stackoverflow.com/a/39540339/9392211 static const metersPerLatitude = 111.32 * 1000; // 111.32 km static const radiansPerDegree = pi / 180; @@ -36,7 +63,7 @@ extension GpsUtils on GpsCoordinates { double heuristicDistance(GpsCoordinates other) { var distance = 0.0; - final delta = (this - other).inMeters; + final delta = inMeters - other.inMeters; final deltaLat = delta.lat.abs(); final deltaLong = delta.long.abs(); @@ -54,21 +81,19 @@ extension GpsUtils on GpsCoordinates { return distance; } - double manhattanDistance(GpsCoordinates other) => - (latitude - other.latitude).abs() * metersPerLatitude + - (longitude - other.longitude).abs() * metersPerLongitude; + double manhattanDistance(GpsCoordinates other) { + final delta = inMeters - other.inMeters; + return delta.lat.toDouble().abs() + delta.long.abs(); + } bool isNear(GpsCoordinates other, [double? tolerance]) { tolerance ??= maxErrorMeters; final currentMeters = inMeters; final otherMeters = other.inMeters; - final (deltaX, deltaY) = ( - currentMeters.lat - otherMeters.lat, - currentMeters.long - otherMeters.long - ); + final delta = currentMeters - otherMeters; - final distance = sqrt(pow(deltaX, 2) + pow(deltaY, 2)); + final distance = sqrt(pow(delta.long, 2) + pow(delta.lat, 2)); return distance < tolerance; } @@ -85,14 +110,27 @@ extension GpsUtils on GpsCoordinates { String prettyPrint() => toProto3Json().toString(); - GpsCoordinates goForward(CardinalDirection orientation) => this + switch(orientation) { - CardinalDirection.north => GpsUtils.north, - CardinalDirection.south => GpsUtils.south, - CardinalDirection.west => GpsUtils.west, - CardinalDirection.east => GpsUtils.east, - CardinalDirection.northEast => GpsUtils.northEast, - CardinalDirection.northWest => GpsUtils.northWest, - CardinalDirection.southEast => GpsUtils.southEast, - CardinalDirection.southWest => GpsUtils.southWest, - }; + // GpsCoordinates goForward(CardinalDirection orientation) => this + + // switch (orientation) { + // CardinalDirection.north => GpsUtils.north, + // CardinalDirection.south => GpsUtils.south, + // CardinalDirection.west => GpsUtils.west, + // CardinalDirection.east => GpsUtils.east, + // CardinalDirection.northEast => GpsUtils.northEast, + // CardinalDirection.northWest => GpsUtils.northWest, + // CardinalDirection.southEast => GpsUtils.southEast, + // CardinalDirection.southWest => GpsUtils.southWest, + // }; + + GpsCoordinates goForward(CardinalDirection orientation) => (inMeters + + switch (orientation) { + CardinalDirection.north => GpsUtils.northMeters, + CardinalDirection.south => GpsUtils.southMeters, + CardinalDirection.west => GpsUtils.westMeters, + CardinalDirection.east => GpsUtils.eastMeters, + CardinalDirection.northEast => GpsUtils.northEastMeters, + CardinalDirection.northWest => GpsUtils.northWestMeters, + CardinalDirection.southEast => GpsUtils.southEastMeters, + CardinalDirection.southWest => GpsUtils.southWestMeters, + }).toGps(); } From 758eb266b66b169b933e91c34e644bfff0617645 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:58:48 -0500 Subject: [PATCH 029/110] Use meters for distanceTo() --- lib/src/utils/gps_utils.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 90bf807..f668ed1 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -56,10 +56,11 @@ extension GpsUtils on GpsCoordinates { static double get latitudePerMeter => 1 / metersPerLatitude; static double get longitudePerMeter => 1 / metersPerLongitude; - double distanceTo(GpsCoordinates other) => sqrt( - pow(latitude - other.latitude, 2) + - pow(longitude - other.longitude, 2), - ); + double distanceTo(GpsCoordinates other) { + final deltaMeters = inMeters - other.inMeters; + + return sqrt(pow(deltaMeters.long, 2) + pow(deltaMeters.lat, 2)); + } double heuristicDistance(GpsCoordinates other) { var distance = 0.0; From 991c4262c726a3af37c96cd76002e35d7137b2bc Mon Sep 17 00:00:00 2001 From: Binghamton Rover Date: Fri, 24 Jan 2025 11:48:55 -0500 Subject: [PATCH 030/110] Additional testing on rover --- lib/src/orchestrator/rover_orchestrator.dart | 2 +- lib/src/utils/gps_utils.dart | 2 +- lib/src/utils/imu_utils.dart | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index c303e5d..7ecd52f 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -76,7 +76,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } traversed.add(state.position); // if (state.direction != DriveDirection.forward) continue; - if (++count == 5) break; + if (++count >= 5) break; final foundObstacle = collection.detector.findObstacles(); if (foundObstacle) { collection.logger.debug("Found an obstacle. Recalculating path..."); diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index f668ed1..6113a58 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -22,7 +22,7 @@ extension GpsMetersUtil on GpsMeters { } extension GpsUtils on GpsCoordinates { - static double maxErrorMeters = 0.5; + static double maxErrorMeters = 1; static double moveLengthMeters = 1; static double get epsilonLatitude => maxErrorMeters * latitudePerMeter; static double get epsilonLongitude => maxErrorMeters * longitudePerMeter; diff --git a/lib/src/utils/imu_utils.dart b/lib/src/utils/imu_utils.dart index e40073e..2009b98 100644 --- a/lib/src/utils/imu_utils.dart +++ b/lib/src/utils/imu_utils.dart @@ -2,10 +2,10 @@ import "package:autonomy/autonomy.dart"; extension OrientationUtils on Orientation { /// The IMU angle tolerance for a turn during autonomy - static const double turnEpsilon = 5; + static const double turnEpsilon = 3; /// The IMU angle tolerance when turning to re-correct to the /// proper orientation before driving forward - static const double driveRealignmentEpsilon = 8; + static const double driveRealignmentEpsilon = 5; /// North orientation static final north = Orientation(z: CardinalDirection.north.angle); From 60de9d28822a54c6a188fa3660fe1d94f48fbf1b Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 4 Feb 2025 00:42:07 -0500 Subject: [PATCH 031/110] Moved things into constants --- lib/constants.dart | 13 ++++++ lib/src/orchestrator/rover_orchestrator.dart | 3 +- lib/src/pathfinding/rover_pathfinding.dart | 4 +- lib/src/utils/a_star.dart | 3 +- lib/src/utils/gps_utils.dart | 44 +++++--------------- lib/src/utils/imu_utils.dart | 9 +--- test/path_test.dart | 5 ++- 7 files changed, 34 insertions(+), 47 deletions(-) create mode 100644 lib/constants.dart diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 0000000..b7f2ec7 --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,13 @@ +class Constants { + /// The maximum error or "tolerance" for reaching the end goal + static const double maxErrorMeters = 1; + /// The amount of meters to move per path step + static const double moveLengthMeters = 1; + + /// The IMU angle tolerance for a turn during autonomy + static const double turnEpsilon = 3; + + /// The IMU angle tolerance when turning to re-correct to the + /// proper orientation before driving forward + static const double driveRealignmentEpsilon = 5; +} diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 7ecd52f..b885c62 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -1,3 +1,4 @@ +import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; import "dart:async"; @@ -66,7 +67,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { for (final state in path) { collection.logger.debug(state.toString()); if (state.instruction == DriveDirection.forward && - !collection.imu.raw.isNear(state.orientation.angle, OrientationUtils.driveRealignmentEpsilon)) { + !collection.imu.raw.isNear(state.orientation.angle, Constants.driveRealignmentEpsilon)) { await collection.drive.faceDirection(state.orientation); } await collection.drive.driveState(state); diff --git a/lib/src/pathfinding/rover_pathfinding.dart b/lib/src/pathfinding/rover_pathfinding.dart index f1893e6..afe429d 100644 --- a/lib/src/pathfinding/rover_pathfinding.dart +++ b/lib/src/pathfinding/rover_pathfinding.dart @@ -42,7 +42,7 @@ class RoverPathfinder extends PathfindingInterface { final result = aStar(state, verbose: verbose, limit: 50000); if (result == null) return null; final transitions = result.reconstructPath(); - // final optimized = optimizePath(transitions); - return transitions.toList(); + final optimized = optimizePath(transitions); + return optimized; } } diff --git a/lib/src/utils/a_star.dart b/lib/src/utils/a_star.dart index 08fb3d0..7ea672e 100644 --- a/lib/src/utils/a_star.dart +++ b/lib/src/utils/a_star.dart @@ -1,6 +1,7 @@ import "dart:math"; import "package:a_star/a_star.dart"; +import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; @@ -75,7 +76,7 @@ class AutonomyAStarState extends AStarState { String hash() => "${position.prettyPrint()} ($orientation) ($instruction)"; @override - bool isGoal() => position.isNear(goal, min(GpsUtils.moveLengthMeters, GpsUtils.maxErrorMeters)); + bool isGoal() => position.isNear(goal, min(Constants.moveLengthMeters, Constants.maxErrorMeters)); /// Returns whether or not the rover will drive between or right next to an obstacle diagonally
///
diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 6113a58..570e9f4 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -1,6 +1,7 @@ import "dart:math"; +import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; /// An alias for gps coordinates measured in meters @@ -22,40 +23,15 @@ extension GpsMetersUtil on GpsMeters { } extension GpsUtils on GpsCoordinates { - static double maxErrorMeters = 1; - static double moveLengthMeters = 1; - static double get epsilonLatitude => maxErrorMeters * latitudePerMeter; - static double get epsilonLongitude => maxErrorMeters * longitudePerMeter; - - static double get movementLatitude => moveLengthMeters * latitudePerMeter; - static double get movementLongitude => moveLengthMeters * longitudePerMeter; - - static GpsCoordinates get east => GpsCoordinates(longitude: movementLongitude); - static GpsCoordinates get west => GpsCoordinates(longitude: -movementLongitude); - static GpsCoordinates get north => GpsCoordinates(latitude: movementLatitude); - static GpsCoordinates get south => GpsCoordinates(latitude: -movementLatitude); - static GpsCoordinates get northEast => north + east; - static GpsCoordinates get northWest => north + west; - static GpsCoordinates get southEast => south + east; - static GpsCoordinates get southWest => south + west; - - static GpsMeters get eastMeters => (lat: 0, long: moveLengthMeters); - static GpsMeters get westMeters => (lat: 0, long: -moveLengthMeters); - static GpsMeters get northMeters => (lat: moveLengthMeters, long: 0); - static GpsMeters get southMeters => (lat: -moveLengthMeters, long: 0); + static GpsMeters get eastMeters => (lat: 0, long: Constants.moveLengthMeters); + static GpsMeters get westMeters => (lat: 0, long: -Constants.moveLengthMeters); + static GpsMeters get northMeters => (lat: Constants.moveLengthMeters, long: 0); + static GpsMeters get southMeters => (lat: -Constants.moveLengthMeters, long: 0); static GpsMeters get northEastMeters => northMeters + eastMeters; static GpsMeters get northWestMeters => northMeters + westMeters; static GpsMeters get southEastMeters => southMeters + eastMeters; static GpsMeters get southWestMeters => southMeters + westMeters; - // Taken from https://stackoverflow.com/a/39540339/9392211 - static const metersPerLatitude = 111.32 * 1000; // 111.32 km - static const radiansPerDegree = pi / 180; - static double get metersPerLongitude => 40075 * cos(GpsInterface.currentLatitude * radiansPerDegree) / 360 * 1000.0; - - static double get latitudePerMeter => 1 / metersPerLatitude; - static double get longitudePerMeter => 1 / metersPerLongitude; - double distanceTo(GpsCoordinates other) { final deltaMeters = inMeters - other.inMeters; @@ -69,14 +45,14 @@ extension GpsUtils on GpsCoordinates { final deltaLong = delta.long.abs(); final minimumDistance = min(deltaLat, deltaLong); - if (minimumDistance >= moveLengthMeters) { - distance += (minimumDistance ~/ moveLengthMeters) * sqrt2; + if (minimumDistance >= Constants.moveLengthMeters) { + distance += (minimumDistance ~/ Constants.moveLengthMeters) * sqrt2; } final translationDelta = (deltaLat - deltaLong).abs(); - if (translationDelta >= moveLengthMeters) { - distance += translationDelta ~/ moveLengthMeters; + if (translationDelta >= Constants.moveLengthMeters) { + distance += translationDelta ~/ Constants.moveLengthMeters; } return distance; @@ -88,7 +64,7 @@ extension GpsUtils on GpsCoordinates { } bool isNear(GpsCoordinates other, [double? tolerance]) { - tolerance ??= maxErrorMeters; + tolerance ??= Constants.maxErrorMeters; final currentMeters = inMeters; final otherMeters = other.inMeters; diff --git a/lib/src/utils/imu_utils.dart b/lib/src/utils/imu_utils.dart index 2009b98..084bc29 100644 --- a/lib/src/utils/imu_utils.dart +++ b/lib/src/utils/imu_utils.dart @@ -1,12 +1,7 @@ import "package:autonomy/autonomy.dart"; +import "package:autonomy/constants.dart"; extension OrientationUtils on Orientation { - /// The IMU angle tolerance for a turn during autonomy - static const double turnEpsilon = 3; - /// The IMU angle tolerance when turning to re-correct to the - /// proper orientation before driving forward - static const double driveRealignmentEpsilon = 5; - /// North orientation static final north = Orientation(z: CardinalDirection.north.angle); /// East orientation @@ -21,7 +16,7 @@ extension OrientationUtils on Orientation { /// Whether or not this orientation is within [epsilon] degrees of [value] bool isNear(double value, [double? epsilon]) { - epsilon ??= OrientationUtils.turnEpsilon; + epsilon ??= Constants.turnEpsilon; final error = (value - z).clampHalfAngle(); return error.abs() <= epsilon; diff --git a/test/path_test.dart b/test/path_test.dart index a1dd043..c664735 100644 --- a/test/path_test.dart +++ b/test/path_test.dart @@ -1,5 +1,6 @@ import "dart:math"; +import "package:autonomy/constants.dart"; import "package:burt_network/burt_network.dart"; import "package:test/test.dart"; @@ -22,7 +23,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("Simple path from (0, 0) to (5, 5) exists", () { final simulator = AutonomySimulator(); final destination = (lat: 5, long: 5).toGps(); - simulator.logger.info("Each step is ${GpsUtils.north.latitude.toStringAsFixed(5)}"); + simulator.logger.info("Each step is ${GpsUtils.northMeters.toGps().latitude.toStringAsFixed(5)}"); simulator.logger.info("Going to ${destination.prettyPrint()}"); simulator.pathfinder = RoverPathfinder(collection: simulator); final path = simulator.pathfinder.getPath(destination); @@ -30,7 +31,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { }); test("Small paths are efficient", () { - final oldError = GpsUtils.maxErrorMeters; + const oldError = Constants.maxErrorMeters; GpsUtils.maxErrorMeters = 1; final simulator = AutonomySimulator(); From fd1180deb7c5f8a0d5321eb782465131c17eabcd Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:45:54 -0500 Subject: [PATCH 032/110] More safety replanning features: - Replan if the rover is too far from the path - Add a maximum time for the rover to drive forward 1 step --- lib/constants.dart | 9 +++++++++ lib/src/drive/sensor_drive.dart | 11 ++++++++++- lib/src/orchestrator/rover_orchestrator.dart | 12 ++++++++++-- lib/src/utils/a_star.dart | 8 ++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index b7f2ec7..56d579c 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,10 +4,19 @@ class Constants { /// The amount of meters to move per path step static const double moveLengthMeters = 1; + /// Replan the path if the rover's position is this far away from the path + static const double replanErrorMeters = 3; + /// The IMU angle tolerance for a turn during autonomy static const double turnEpsilon = 3; /// The IMU angle tolerance when turning to re-correct to the /// proper orientation before driving forward static const double driveRealignmentEpsilon = 5; + + /// The maximum time to spend waiting for the drive to reach a desired GPS coordinate. + /// + /// Only applies to individual "drive forward" steps, to prevent indefinite driving + /// if it never reaches within [maxErrorMeters] of its goal position + static const Duration driveGPSTimeout = Duration(seconds: 6, milliseconds: 500); } diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index c3c84bf..3e93e7b 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -1,4 +1,5 @@ import "package:autonomy/autonomy.dart"; +import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; import "drive_commands.dart"; @@ -41,7 +42,15 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { await waitFor(() { moveForward(); return collection.gps.isNear(position); - }); + }).timeout( + Constants.driveGPSTimeout, + onTimeout: () { + collection.logger.warning( + "GPS Drive timed out", + body: "Failed to reach ${position.prettyPrint()} after ${Constants.driveGPSTimeout}", + ); + }, + ); await stop(); } diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index b885c62..f96685b 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -36,7 +36,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { @override Future handleGpsTask(AutonomyCommand command) async { final destination = command.destination; - collection.logger.info("Got GPS Task", body: "Go to ${destination.prettyPrint()}"); + collection.logger.info("Received GPS Task", body: "Go to ${destination.prettyPrint()}"); collection.logger.debug("Currently at ${collection.gps.coordinates.prettyPrint()}"); traversed.clear(); collection.drive.setLedStrip(ProtoColor.RED); @@ -66,13 +66,21 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { var count = 0; for (final state in path) { collection.logger.debug(state.toString()); + // Replan if too far from start point + final distanceError = collection.gps.coordinates.distanceTo(state.startPostition); + if (distanceError >= Constants.replanErrorMeters) { + collection.logger.info("Replanning Path", body: "Rover is $distanceError meters off the path"); + break; + } + // Re-align to desired start orientation if angle is too far if (state.instruction == DriveDirection.forward && !collection.imu.raw.isNear(state.orientation.angle, Constants.driveRealignmentEpsilon)) { + collection.logger.info("Re-aligning IMU to start orientation"); await collection.drive.faceDirection(state.orientation); } await collection.drive.driveState(state); if (currentCommand == null || currentPath == null) { - collection.logger.debug("Aborting path, command was canceled"); + collection.logger.info("Aborting path, command was canceled"); return; } traversed.add(state.position); diff --git a/lib/src/utils/a_star.dart b/lib/src/utils/a_star.dart index 7ea672e..6306d00 100644 --- a/lib/src/utils/a_star.dart +++ b/lib/src/utils/a_star.dart @@ -25,6 +25,14 @@ class AutonomyAStarState extends AStarState { final GpsCoordinates goal; final AutonomyInterface collection; + GpsCoordinates get startPostition { + if (instruction != DriveDirection.forward) { + return position; + } + + return position.goForward(orientation.turnRight().turnRight()); + } + AutonomyAStarState({ required this.position, required this.goal, From 86bea082566b9abd905db8b22b577541962a87df Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:19:30 -0500 Subject: [PATCH 033/110] Replan paths if timeout is hit --- lib/constants.dart | 5 ++-- lib/src/drive/drive_interface.dart | 20 +++++++++------ lib/src/drive/rover_drive.dart | 26 +++++++++++--------- lib/src/drive/sensor_drive.dart | 13 +++++++--- lib/src/drive/sim_drive.dart | 11 ++++++--- lib/src/drive/timed_drive.dart | 16 ++++++++---- lib/src/orchestrator/rover_orchestrator.dart | 5 +++- 7 files changed, 63 insertions(+), 33 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index 56d579c..82bd84f 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,6 +1,7 @@ class Constants { /// The maximum error or "tolerance" for reaching the end goal static const double maxErrorMeters = 1; + /// The amount of meters to move per path step static const double moveLengthMeters = 1; @@ -15,8 +16,8 @@ class Constants { static const double driveRealignmentEpsilon = 5; /// The maximum time to spend waiting for the drive to reach a desired GPS coordinate. - /// + /// /// Only applies to individual "drive forward" steps, to prevent indefinite driving - /// if it never reaches within [maxErrorMeters] of its goal position + /// if it never reaches within [maxErrorMeters] of its desired position. static const Duration driveGPSTimeout = Duration(seconds: 6, milliseconds: 500); } diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index ab2d0cc..86b6084 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -38,13 +38,13 @@ abstract class DriveInterface extends Service { DriveInterface({required this.collection, this.config = roverConfig}); /// Stop the rover - Future stop(); + Future stop(); - /// Drive forward to [position] - Future driveForward(GpsCoordinates position); + /// Drive forward to [position], returns whether or not it successfully drove to the position + Future driveForward(GpsCoordinates position); - /// Turn to face [orientation] - Future faceDirection(CardinalDirection orientation); + /// Turn to face [orientation], returns whether or not it was able to turn + Future faceDirection(CardinalDirection orientation); /// Utility method to send a command to subsystems void sendCommand(Message message) => collection.server.sendMessage(message, destination: config.subsystems); @@ -53,17 +53,21 @@ abstract class DriveInterface extends Service { /// /// This exists so the rover can generate a path based on a known /// orientation that aligns to the possible orientations defined by [CardinalDirection] - Future resolveOrientation() => faceDirection(collection.imu.nearest); + Future resolveOrientation() => faceDirection(collection.imu.nearest); /// Turns to face the state's [AutonomyAStarState.orientation]. /// /// Exists so that the TimedDrive can implement this in terms of [AutonomyAStarState.instruction]. - Future turnState(AutonomyAStarState state) => faceDirection(state.orientation); + /// + /// Returns whether or not the turn was successful + Future turnState(AutonomyAStarState state) => faceDirection(state.orientation); /// Drives the rover based on the instruction and desired positions in [state] /// /// This determines based on the [state] whether it should move forward, turn, or stop - Future driveState(AutonomyAStarState state) { + /// + /// Returns whether or not the drive was successful + Future driveState(AutonomyAStarState state) { if (state.instruction == DriveDirection.stop) { return stop(); } else if (state.instruction == DriveDirection.forward) { diff --git a/lib/src/drive/rover_drive.dart b/lib/src/drive/rover_drive.dart index 14358ef..6db6a8a 100644 --- a/lib/src/drive/rover_drive.dart +++ b/lib/src/drive/rover_drive.dart @@ -68,10 +68,12 @@ class RoverDrive extends DriveInterface { } @override - Future stop() async { - await sensorDrive.stop(); - await timedDrive.stop(); - await simDrive.stop(); + Future stop() async { + var result = true; + result &= await sensorDrive.stop(); + result &= await timedDrive.stop(); + result &= await simDrive.stop(); + return result; } @override @@ -81,21 +83,23 @@ class RoverDrive extends DriveInterface { Future approachAruco() => sensorDrive.approachAruco(); @override - Future faceDirection(CardinalDirection orientation) async { + Future faceDirection(CardinalDirection orientation) async { if (useImu) { - await sensorDrive.faceDirection(orientation); + return sensorDrive.faceDirection(orientation); } else { - await simDrive.faceDirection(orientation); + return simDrive.faceDirection(orientation); } } @override - Future driveForward(GpsCoordinates position) async { + Future driveForward(GpsCoordinates position) async { if (useGps) { - await sensorDrive.driveForward(position); + return sensorDrive.driveForward(position); } else { - await timedDrive.driveForward(position); - await simDrive.driveForward(position); + var result = true; + result &= await timedDrive.driveForward(position); + result &= await simDrive.driveForward(position); + return result; } } } diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 3e93e7b..85d4349 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -18,7 +18,10 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { SensorDrive({required super.collection, super.config}); @override - Future stop() async => stopMotors(); + Future stop() async { + stopMotors(); + return true; + } /// Will periodically check for a condition to become true. This can be /// thought of as a "wait until", where the rover will periodically check @@ -36,9 +39,10 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { Future dispose() async { } @override - Future driveForward(GpsCoordinates position) async { + Future driveForward(GpsCoordinates position) async { collection.logger.info("Driving forward one meter"); setThrottle(config.forwardThrottle); + var timedOut = false; await waitFor(() { moveForward(); return collection.gps.isNear(position); @@ -49,17 +53,20 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { "GPS Drive timed out", body: "Failed to reach ${position.prettyPrint()} after ${Constants.driveGPSTimeout}", ); + timedOut = true; }, ); await stop(); + return !timedOut; } @override - Future faceDirection(CardinalDirection orientation) async { + Future faceDirection(CardinalDirection orientation) async { collection.logger.info("Turning to face $orientation..."); setThrottle(config.turnThrottle); await waitFor(() => _tryToFace(orientation)); await stop(); + return true; } bool _tryToFace(CardinalDirection orientation) { diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index 6507d9c..303cfb5 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -23,17 +23,22 @@ class DriveSimulator extends DriveInterface { Future dispose() async { } @override - Future driveForward(GpsCoordinates position) async { + Future driveForward(GpsCoordinates position) async { if (shouldDelay) await Future.delayed(delay); collection.gps.update(position); + return true; } @override - Future faceDirection(CardinalDirection orientation) async { + Future faceDirection(CardinalDirection orientation) async { if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); collection.imu.update(orientation.orientation); + return true; } @override - Future stop() async => collection.logger.debug("Stopping"); + Future stop() async { + collection.logger.debug("Stopping"); + return true; + } } diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index 9a937a2..d85eefe 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -20,12 +20,13 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { Future dispose() async { } @override - Future driveForward(GpsCoordinates position) async { + Future driveForward(GpsCoordinates position) async { await goForward(collection.imu.nearest.isPerpendicular ? 1 : sqrt2); + return true; } @override - Future turnState(AutonomyAStarState state) async { + Future turnState(AutonomyAStarState state) async { switch (state.instruction) { case DriveDirection.left: await turnLeft(); @@ -38,21 +39,26 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { case DriveDirection.forward || DriveDirection.stop: break; } + return true; } @override - Future stop() async => stopMotors(); + Future stop() async { + stopMotors(); + return true; + } /// Moves forward for the amount of time it will take to drive the specified [distance] /// /// This will set the speeds to move forward, and wait for the amount of /// time specified by the [DriveConfig.oneMeterDelay] - Future goForward([double distance = 1]) async { + Future goForward([double distance = 1]) async { collection.logger.info("Driving forward $distance meters"); setThrottle(config.forwardThrottle); moveForward(); await Future.delayed(config.oneMeterDelay * distance); await stop(); + return true; } /// Turns left for the amount of time it will take to spin left 90 degrees @@ -100,6 +106,6 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { } @override - Future faceDirection(CardinalDirection orientation) => + Future faceDirection(CardinalDirection orientation) => throw UnsupportedError("Cannot face any arbitrary direction using TimedDrive"); } diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index f96685b..75f58e7 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -78,7 +78,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.logger.info("Re-aligning IMU to start orientation"); await collection.drive.faceDirection(state.orientation); } - await collection.drive.driveState(state); + // If there was an error (usually a timeout) while driving, replan path + if (!await collection.drive.driveState(state)) { + break; + } if (currentCommand == null || currentPath == null) { collection.logger.info("Aborting path, command was canceled"); return; From e3f773371b941a8015182c1f8231e0449daf2718 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:38:28 -0500 Subject: [PATCH 034/110] GPSUtil cleanup - The directions don't need to be getters anymore --- lib/src/utils/gps_utils.dart | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 570e9f4..6303570 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -23,14 +23,14 @@ extension GpsMetersUtil on GpsMeters { } extension GpsUtils on GpsCoordinates { - static GpsMeters get eastMeters => (lat: 0, long: Constants.moveLengthMeters); - static GpsMeters get westMeters => (lat: 0, long: -Constants.moveLengthMeters); - static GpsMeters get northMeters => (lat: Constants.moveLengthMeters, long: 0); - static GpsMeters get southMeters => (lat: -Constants.moveLengthMeters, long: 0); - static GpsMeters get northEastMeters => northMeters + eastMeters; - static GpsMeters get northWestMeters => northMeters + westMeters; - static GpsMeters get southEastMeters => southMeters + eastMeters; - static GpsMeters get southWestMeters => southMeters + westMeters; + static const GpsMeters eastMeters = (lat: 0, long: Constants.moveLengthMeters); + static const GpsMeters westMeters = (lat: 0, long: -Constants.moveLengthMeters); + static const GpsMeters northMeters = (lat: Constants.moveLengthMeters, long: 0); + static const GpsMeters southMeters = (lat: -Constants.moveLengthMeters, long: 0); + static final GpsMeters northEastMeters = northMeters + eastMeters; + static final GpsMeters northWestMeters = northMeters + westMeters; + static final GpsMeters southEastMeters = southMeters + eastMeters; + static final GpsMeters southWestMeters = southMeters + westMeters; double distanceTo(GpsCoordinates other) { final deltaMeters = inMeters - other.inMeters; @@ -87,18 +87,6 @@ extension GpsUtils on GpsCoordinates { String prettyPrint() => toProto3Json().toString(); - // GpsCoordinates goForward(CardinalDirection orientation) => this + - // switch (orientation) { - // CardinalDirection.north => GpsUtils.north, - // CardinalDirection.south => GpsUtils.south, - // CardinalDirection.west => GpsUtils.west, - // CardinalDirection.east => GpsUtils.east, - // CardinalDirection.northEast => GpsUtils.northEast, - // CardinalDirection.northWest => GpsUtils.northWest, - // CardinalDirection.southEast => GpsUtils.southEast, - // CardinalDirection.southWest => GpsUtils.southWest, - // }; - GpsCoordinates goForward(CardinalDirection orientation) => (inMeters + switch (orientation) { CardinalDirection.north => GpsUtils.northMeters, From 2e24f0db501342941d4c6ac87e4b53b3754af8ec Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:23:06 -0500 Subject: [PATCH 035/110] Improved heuristic and sharpen intermediate drive tolerance --- lib/constants.dart | 4 ++++ lib/src/drive/sensor_drive.dart | 2 +- lib/src/gps/gps_interface.dart | 2 +- lib/src/orchestrator/rover_orchestrator.dart | 2 ++ lib/src/utils/a_star.dart | 2 +- lib/src/utils/gps_utils.dart | 8 ++++++++ 6 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index 82bd84f..c42c988 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -2,6 +2,10 @@ class Constants { /// The maximum error or "tolerance" for reaching the end goal static const double maxErrorMeters = 1; + /// How close the rover should get to a drive coordinate before + /// continuing with the path + static const double intermediateStepTolerance = 0.25; + /// The amount of meters to move per path step static const double moveLengthMeters = 1; diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 85d4349..0fffc56 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -45,7 +45,7 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { var timedOut = false; await waitFor(() { moveForward(); - return collection.gps.isNear(position); + return collection.gps.isNear(position, Constants.intermediateStepTolerance); }).timeout( Constants.driveGPSTimeout, onTimeout: () { diff --git a/lib/src/gps/gps_interface.dart b/lib/src/gps/gps_interface.dart index e4505b6..7ce52d1 100644 --- a/lib/src/gps/gps_interface.dart +++ b/lib/src/gps/gps_interface.dart @@ -17,7 +17,7 @@ abstract class GpsInterface extends Service with Receiver { GpsCoordinates get coordinates; - bool isNear(GpsCoordinates other) => coordinates.isNear(other); + bool isNear(GpsCoordinates other, [double? tolerance]) => coordinates.isNear(other, tolerance); @override Future waitForValue() async { diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 75f58e7..b225a0e 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -40,8 +40,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.logger.debug("Currently at ${collection.gps.coordinates.prettyPrint()}"); traversed.clear(); collection.drive.setLedStrip(ProtoColor.RED); + // detect obstacles before and after resolving orientation, as a "scan" collection.detector.findObstacles(); await collection.drive.resolveOrientation(); + collection.detector.findObstacles(); while (!collection.gps.coordinates.isNear(destination)) { // Calculate a path collection.logger.debug("Finding a path"); diff --git a/lib/src/utils/a_star.dart b/lib/src/utils/a_star.dart index 6306d00..41b132f 100644 --- a/lib/src/utils/a_star.dart +++ b/lib/src/utils/a_star.dart @@ -78,7 +78,7 @@ class AutonomyAStarState extends AStarState { }; @override - double heuristic() => position.heuristicDistance(goal); + double heuristic() => position.octileDistance(goal); @override String hash() => "${position.prettyPrint()} ($orientation) ($instruction)"; diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 6303570..23c0fb0 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -63,6 +63,14 @@ extension GpsUtils on GpsCoordinates { return delta.lat.toDouble().abs() + delta.long.abs(); } + double octileDistance(GpsCoordinates other) { + final delta = inMeters - other.inMeters; + final dx = delta.long.abs() ~/ Constants.moveLengthMeters; + final dy = delta.lat.abs() ~/ Constants.moveLengthMeters; + + return max(dx, dy) + (sqrt2 - 1) * min(dx, dy); + } + bool isNear(GpsCoordinates other, [double? tolerance]) { tolerance ??= Constants.maxErrorMeters; final currentMeters = inMeters; From d9a1cd1dfc69ab45d3a63cdc1cac1a6d2e065581 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:53:28 -0500 Subject: [PATCH 036/110] Point to next waypoint before driving (only with RTK) --- lib/src/drive/drive_interface.dart | 5 +++- lib/src/drive/rover_drive.dart | 6 ++--- lib/src/drive/sensor_drive.dart | 6 ++--- lib/src/drive/sim_drive.dart | 4 +-- lib/src/drive/timed_drive.dart | 2 +- lib/src/imu/imu_interface.dart | 2 +- lib/src/orchestrator/rover_orchestrator.dart | 26 +++++++++++++++++--- lib/src/utils/gps_utils.dart | 3 +++ 8 files changed, 39 insertions(+), 15 deletions(-) diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index 86b6084..8d250cf 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -44,7 +44,10 @@ abstract class DriveInterface extends Service { Future driveForward(GpsCoordinates position); /// Turn to face [orientation], returns whether or not it was able to turn - Future faceDirection(CardinalDirection orientation); + Future faceOrientation(Orientation orientation); + + /// Turn to face the orientation of [direction], returns whether or not it was able to turn + Future faceDirection(CardinalDirection direction) => faceOrientation(direction.orientation); /// Utility method to send a command to subsystems void sendCommand(Message message) => collection.server.sendMessage(message, destination: config.subsystems); diff --git a/lib/src/drive/rover_drive.dart b/lib/src/drive/rover_drive.dart index 6db6a8a..c43a5fb 100644 --- a/lib/src/drive/rover_drive.dart +++ b/lib/src/drive/rover_drive.dart @@ -83,11 +83,11 @@ class RoverDrive extends DriveInterface { Future approachAruco() => sensorDrive.approachAruco(); @override - Future faceDirection(CardinalDirection orientation) async { + Future faceOrientation(Orientation orientation) async { if (useImu) { - return sensorDrive.faceDirection(orientation); + return sensorDrive.faceOrientation(orientation); } else { - return simDrive.faceDirection(orientation); + return simDrive.faceOrientation(orientation); } } diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 0fffc56..0db25c3 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -61,7 +61,7 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { } @override - Future faceDirection(CardinalDirection orientation) async { + Future faceOrientation(Orientation orientation) async { collection.logger.info("Turning to face $orientation..."); setThrottle(config.turnThrottle); await waitFor(() => _tryToFace(orientation)); @@ -69,9 +69,9 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { return true; } - bool _tryToFace(CardinalDirection orientation) { + bool _tryToFace(Orientation orientation) { final current = collection.imu.heading; - final target = orientation.angle; + final target = orientation.heading; final error = (target - current).clampHalfAngle(); if (error < 0) { spinRight(); diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index 303cfb5..a9f0760 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -30,9 +30,9 @@ class DriveSimulator extends DriveInterface { } @override - Future faceDirection(CardinalDirection orientation) async { + Future faceOrientation(Orientation orientation) async { if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); - collection.imu.update(orientation.orientation); + collection.imu.update(orientation); return true; } diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index d85eefe..b71812c 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -106,6 +106,6 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { } @override - Future faceDirection(CardinalDirection orientation) => + Future faceOrientation(Orientation orientation) => throw UnsupportedError("Cannot face any arbitrary direction using TimedDrive"); } diff --git a/lib/src/imu/imu_interface.dart b/lib/src/imu/imu_interface.dart index 2bf1d1f..9ff199b 100644 --- a/lib/src/imu/imu_interface.dart +++ b/lib/src/imu/imu_interface.dart @@ -14,7 +14,7 @@ abstract class ImuInterface extends Service with Receiver { @visibleForTesting void forceUpdate(Orientation newValue) {} - bool isNear(CardinalDirection direction) => raw.isNear(direction.angle); + bool isNear(Orientation orientation, [double? tolerance]) => raw.isNear(orientation.heading, tolerance); @override Future init() async => true; diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index b225a0e..f3f417f 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -1,3 +1,5 @@ +import "dart:math"; + import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; import "dart:async"; @@ -75,10 +77,26 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { break; } // Re-align to desired start orientation if angle is too far - if (state.instruction == DriveDirection.forward && - !collection.imu.raw.isNear(state.orientation.angle, Constants.driveRealignmentEpsilon)) { - collection.logger.info("Re-aligning IMU to start orientation"); - await collection.drive.faceDirection(state.orientation); + if (state.instruction == DriveDirection.forward) { + Orientation targetOrientation; + // if it has RTK, point towards the next coordinate + if (collection.gps.coordinates.hasRTK) { + final difference = state.position.inMeters - collection.gps.coordinates.inMeters; + + final angle = atan2(difference.lat, difference.long) * 180 / pi; + + targetOrientation = Orientation(z: angle); + } else { + targetOrientation = state.orientation.orientation; + } + + if (!collection.imu.isNear( + targetOrientation, + Constants.driveRealignmentEpsilon, + )) { + collection.logger.info("Re-aligning IMU to correct orientation"); + await collection.drive.faceOrientation(targetOrientation); + } } // If there was an error (usually a timeout) while driving, replan path if (!await collection.drive.driveState(state)) { diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 23c0fb0..b91a0bf 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -32,6 +32,9 @@ extension GpsUtils on GpsCoordinates { static final GpsMeters southEastMeters = southMeters + eastMeters; static final GpsMeters southWestMeters = southMeters + westMeters; + /// Whether or not the coordinates is fused with the RTK algorithm + bool get hasRTK => rtkMode == RTKMode.RTK_FIXED || rtkMode == RTKMode.RTK_FLOAT; + double distanceTo(GpsCoordinates other) { final deltaMeters = inMeters - other.inMeters; From da21d7680372862ab3bfd2de15ae2dcdf11a04ff Mon Sep 17 00:00:00 2001 From: Binghamton Rover Date: Fri, 24 Jan 2025 18:15:15 -0500 Subject: [PATCH 037/110] Works on rover! --- lib/constants.dart | 2 +- lib/src/autonomy_interface.dart | 2 +- lib/src/drive/drive_config.dart | 4 ++-- lib/src/drive/sensor_drive.dart | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index c42c988..03e23ad 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -23,5 +23,5 @@ class Constants { /// /// Only applies to individual "drive forward" steps, to prevent indefinite driving /// if it never reaches within [maxErrorMeters] of its desired position. - static const Duration driveGPSTimeout = Duration(seconds: 6, milliseconds: 500); + static const Duration driveGPSTimeout = Duration(seconds: 3, milliseconds: 53); } diff --git a/lib/src/autonomy_interface.dart b/lib/src/autonomy_interface.dart index dbafe47..cb478b9 100644 --- a/lib/src/autonomy_interface.dart +++ b/lib/src/autonomy_interface.dart @@ -64,5 +64,5 @@ abstract class AutonomyInterface extends Service with Receiver { } @override - bool get hasValue => _receivers.every((r) => r.hasValue); + bool get hasValue => true || _receivers.every((r) => r.hasValue); } diff --git a/lib/src/drive/drive_config.dart b/lib/src/drive/drive_config.dart index f11d1e3..eed787e 100644 --- a/lib/src/drive/drive_config.dart +++ b/lib/src/drive/drive_config.dart @@ -40,8 +40,8 @@ class DriveConfig { /// The [DriveConfig] for the rover const roverConfig = DriveConfig( - forwardThrottle: 0.1, - turnThrottle: 0.05, + forwardThrottle: 0.2, + turnThrottle: 0.075, oneMeterDelay: Duration(milliseconds: 5500), turnDelay: Duration(milliseconds: 4500), subsystemsAddress: "192.168.1.20", diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 0db25c3..a865da4 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -45,7 +45,7 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { var timedOut = false; await waitFor(() { moveForward(); - return collection.gps.isNear(position, Constants.intermediateStepTolerance); + return collection.gps.isNear(position, Constants.intermediateStepTolerance) || timedOut; }).timeout( Constants.driveGPSTimeout, onTimeout: () { From d182fc1b9b2ed30b66bfd0ccd617f33a436f974b Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 7 Feb 2025 00:01:49 -0500 Subject: [PATCH 038/110] Remove unused imports --- lib/src/detector/sim_detector.dart | 1 - lib/src/gps/gps_interface.dart | 1 - lib/src/gps/sim_gps.dart | 1 - lib/src/imu/sim_imu.dart | 1 - lib/src/orchestrator/rover_orchestrator.dart | 1 - lib/src/orchestrator/sim_orchestrator.dart | 1 - lib/src/pathfinding/pathfinding_interface.dart | 1 - lib/src/utils/reporter.dart | 1 - lib/src/video/sim_video.dart | 1 - 9 files changed, 9 deletions(-) diff --git a/lib/src/detector/sim_detector.dart b/lib/src/detector/sim_detector.dart index 8234922..dfac538 100644 --- a/lib/src/detector/sim_detector.dart +++ b/lib/src/detector/sim_detector.dart @@ -1,5 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:burt_network/protobuf.dart"; class SimulatedObstacle { final GpsCoordinates coordinates; diff --git a/lib/src/gps/gps_interface.dart b/lib/src/gps/gps_interface.dart index 5cba6bb..7ce52d1 100644 --- a/lib/src/gps/gps_interface.dart +++ b/lib/src/gps/gps_interface.dart @@ -1,4 +1,3 @@ -import "package:burt_network/protobuf.dart"; import "package:autonomy/interfaces.dart"; import "package:meta/meta.dart"; diff --git a/lib/src/gps/sim_gps.dart b/lib/src/gps/sim_gps.dart index 0062db2..d90a7f5 100644 --- a/lib/src/gps/sim_gps.dart +++ b/lib/src/gps/sim_gps.dart @@ -1,4 +1,3 @@ -import "package:burt_network/protobuf.dart"; import "package:autonomy/interfaces.dart"; class GpsSimulator extends GpsInterface with ValueReporter { diff --git a/lib/src/imu/sim_imu.dart b/lib/src/imu/sim_imu.dart index fb4846b..8798446 100644 --- a/lib/src/imu/sim_imu.dart +++ b/lib/src/imu/sim_imu.dart @@ -1,4 +1,3 @@ -import "package:burt_network/protobuf.dart"; import "package:autonomy/interfaces.dart"; class ImuSimulator extends ImuInterface with ValueReporter { diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 26ae4ba..f3f417f 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -2,7 +2,6 @@ import "dart:math"; import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; -import "package:burt_network/protobuf.dart"; import "dart:async"; class RoverOrchestrator extends OrchestratorInterface with ValueReporter { diff --git a/lib/src/orchestrator/sim_orchestrator.dart b/lib/src/orchestrator/sim_orchestrator.dart index 56420d6..ae2dd75 100644 --- a/lib/src/orchestrator/sim_orchestrator.dart +++ b/lib/src/orchestrator/sim_orchestrator.dart @@ -1,5 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:burt_network/protobuf.dart"; class OrchestratorSimulator extends OrchestratorInterface { OrchestratorSimulator({required super.collection}); diff --git a/lib/src/pathfinding/pathfinding_interface.dart b/lib/src/pathfinding/pathfinding_interface.dart index a3b4c73..25bce8e 100644 --- a/lib/src/pathfinding/pathfinding_interface.dart +++ b/lib/src/pathfinding/pathfinding_interface.dart @@ -1,5 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:burt_network/protobuf.dart"; abstract class PathfindingInterface extends Service { final AutonomyInterface collection; diff --git a/lib/src/utils/reporter.dart b/lib/src/utils/reporter.dart index 4524b60..744eb80 100644 --- a/lib/src/utils/reporter.dart +++ b/lib/src/utils/reporter.dart @@ -1,5 +1,4 @@ import "dart:async"; -import "package:burt_network/protobuf.dart"; import "package:autonomy/interfaces.dart"; diff --git a/lib/src/video/sim_video.dart b/lib/src/video/sim_video.dart index f425102..c7d5f1c 100644 --- a/lib/src/video/sim_video.dart +++ b/lib/src/video/sim_video.dart @@ -1,7 +1,6 @@ import "dart:typed_data"; import "package:autonomy/interfaces.dart"; -import "package:burt_network/protobuf.dart"; class VideoSimulator extends VideoInterface { VideoSimulator({required super.collection}); From 67a52fb5db0d39d84ac06550e51bf285ade07314 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 7 Feb 2025 00:04:19 -0500 Subject: [PATCH 039/110] Update CI dependency overrides --- .github/pubspec_overrides.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/pubspec_overrides.yaml b/.github/pubspec_overrides.yaml index b7c9d12..e25d215 100644 --- a/.github/pubspec_overrides.yaml +++ b/.github/pubspec_overrides.yaml @@ -3,5 +3,5 @@ resolution: dependency_overrides: burt_network: git: - url: https://github.com/BinghamtonRover/Networking - ref: 2.3.1 \ No newline at end of file + url: https://github.com/BinghamtonRover/Rover-Code.git + path: burt_network \ No newline at end of file From 61ca97b62e38511e9dd149f8b6cdebbc94fb1dec Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:24:34 -0500 Subject: [PATCH 040/110] Return method early when timing out --- lib/src/drive/sensor_drive.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index a865da4..7a9654f 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -44,8 +44,9 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { setThrottle(config.forwardThrottle); var timedOut = false; await waitFor(() { + if (timedOut) return true; moveForward(); - return collection.gps.isNear(position, Constants.intermediateStepTolerance) || timedOut; + return collection.gps.isNear(position, Constants.intermediateStepTolerance); }).timeout( Constants.driveGPSTimeout, onTimeout: () { From 6073948f158a54a4f08b6ff453c0b75e2959cc2a Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:43:30 -0500 Subject: [PATCH 041/110] Fix hasValue --- lib/src/autonomy_interface.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/autonomy_interface.dart b/lib/src/autonomy_interface.dart index cb478b9..dbafe47 100644 --- a/lib/src/autonomy_interface.dart +++ b/lib/src/autonomy_interface.dart @@ -64,5 +64,5 @@ abstract class AutonomyInterface extends Service with Receiver { } @override - bool get hasValue => true || _receivers.every((r) => r.hasValue); + bool get hasValue => _receivers.every((r) => r.hasValue); } From 296823611b3ce268d32590950494353ad257646b Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:09:17 -0500 Subject: [PATCH 042/110] Added aruco tracking --- lib/constants.dart | 10 +- lib/src/detector/detector_interface.dart | 1 - lib/src/detector/network_detector.dart | 3 - lib/src/detector/rover_detector.dart | 4 - lib/src/detector/sim_detector.dart | 3 - lib/src/drive/drive_interface.dart | 2 +- lib/src/drive/rover_drive.dart | 5 +- lib/src/drive/sensor_drive.dart | 41 ++++--- lib/src/drive/sim_drive.dart | 3 + lib/src/orchestrator/rover_orchestrator.dart | 113 +++++++++++++++---- lib/src/video/rover_video.dart | 68 +++++++++-- lib/src/video/sim_video.dart | 2 +- lib/src/video/video_interface.dart | 13 ++- pubspec.yaml | 1 + 14 files changed, 201 insertions(+), 68 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index 03e23ad..ce6fbdf 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,3 +1,5 @@ +import "package:autonomy/autonomy.dart"; + class Constants { /// The maximum error or "tolerance" for reaching the end goal static const double maxErrorMeters = 1; @@ -23,5 +25,11 @@ class Constants { /// /// Only applies to individual "drive forward" steps, to prevent indefinite driving /// if it never reaches within [maxErrorMeters] of its desired position. - static const Duration driveGPSTimeout = Duration(seconds: 3, milliseconds: 53); + static const Duration driveGPSTimeout = Duration(seconds: 4, milliseconds: 500); + + /// The maximum time to spend searching for an aruco tag + static const Duration arucoSearchTimeout = Duration(seconds: 10); + + /// The camera that should be used to detect Aruco tags + static const CameraName arucoDetectionCamera = CameraName.ROVER_FRONT; } diff --git a/lib/src/detector/detector_interface.dart b/lib/src/detector/detector_interface.dart index c42a694..8d5909e 100644 --- a/lib/src/detector/detector_interface.dart +++ b/lib/src/detector/detector_interface.dart @@ -5,6 +5,5 @@ abstract class DetectorInterface extends Service { DetectorInterface({required this.collection}); bool findObstacles(); - bool canSeeAruco(); bool isOnSlope(); } diff --git a/lib/src/detector/network_detector.dart b/lib/src/detector/network_detector.dart index c91934d..37e7796 100644 --- a/lib/src/detector/network_detector.dart +++ b/lib/src/detector/network_detector.dart @@ -6,9 +6,6 @@ class NetworkDetector extends DetectorInterface { NetworkDetector({required super.collection}); - @override - bool canSeeAruco() => false; - @override Future dispose() async {} diff --git a/lib/src/detector/rover_detector.dart b/lib/src/detector/rover_detector.dart index 3ed0161..a7d7e16 100644 --- a/lib/src/detector/rover_detector.dart +++ b/lib/src/detector/rover_detector.dart @@ -9,10 +9,6 @@ class RoverDetector extends DetectorInterface { @override bool findObstacles() => false; - @override -// bool canSeeAruco() => collection.video.data.arucoDetected == BoolState.YES; - bool canSeeAruco() => collection.video.flag; - @override Future init() async => true; diff --git a/lib/src/detector/sim_detector.dart b/lib/src/detector/sim_detector.dart index dfac538..4b8e57a 100644 --- a/lib/src/detector/sim_detector.dart +++ b/lib/src/detector/sim_detector.dart @@ -37,9 +37,6 @@ class DetectorSimulator extends DetectorInterface { return result; } - @override - bool canSeeAruco() => false; // if can see [arucoPosition] - @override bool isOnSlope() => false; // if on [slopedLatitude] } diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index 8d250cf..267ad07 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -89,7 +89,7 @@ abstract class DriveInterface extends Service { } /// Spin to face an Aruco tag, returns whether or not it was able to face the tag - Future spinForAruco() async => false; + Future spinForAruco(int arucoId, {CameraName? desiredCamera}) async => false; /// Drive forward to approach an Aruco tag Future approachAruco() async { } diff --git a/lib/src/drive/rover_drive.dart b/lib/src/drive/rover_drive.dart index c43a5fb..70c9cec 100644 --- a/lib/src/drive/rover_drive.dart +++ b/lib/src/drive/rover_drive.dart @@ -77,7 +77,10 @@ class RoverDrive extends DriveInterface { } @override - Future spinForAruco() => sensorDrive.spinForAruco(); + Future spinForAruco( + int arucoId, { + CameraName? desiredCamera, + }) => sensorDrive.spinForAruco(arucoId, desiredCamera: desiredCamera); @override Future approachAruco() => sensorDrive.approachAruco(); diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 7a9654f..44b6534 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -97,27 +97,38 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { } @override - Future spinForAruco() async { + Future spinForAruco( + int arucoId, { + CameraName? desiredCamera, + }) async { setThrottle(config.turnThrottle); - spinLeft(); - final result = await waitFor(() => collection.detector.canSeeAruco()) - .then((_) => true) - .timeout(config.turnDelay * 4, onTimeout: () => false); + var foundAruco = true; + await waitFor(() { + if (!foundAruco) { + return true; + } + spinLeft(); + return collection.video.getArucoDetection(arucoId, desiredCamera: desiredCamera) != null; + }).timeout( + Constants.arucoSearchTimeout, + onTimeout: () => foundAruco = false, + ); await stop(); - return result; + return foundAruco; } @override Future approachAruco() async { - const sizeThreshold = 0.2; - const epsilon = 0.00001; - setThrottle(config.forwardThrottle); - moveForward(); - await waitFor(() { - final size = collection.video.arucoSize; - collection.logger.trace("The Aruco tag is at $size percent"); - return (size.abs() < epsilon && !collection.detector.canSeeAruco()) || size >= sizeThreshold; - }).timeout(config.oneMeterDelay * 5); + // const sizeThreshold = 0.2; + // const epsilon = 0.00001; + // setThrottle(config.forwardThrottle); + // moveForward(); + // await waitFor(() { + // final size = collection.video.arucoSize; + // collection.logger.trace("The Aruco tag is at $size percent"); + // return true; + // return (size.abs() < epsilon && !collection.detector.canSeeAruco()) || size >= sizeThreshold; + // }).timeout(config.oneMeterDelay * 5); await stop(); } } diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index a9f0760..67c2450 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -36,6 +36,9 @@ class DriveSimulator extends DriveInterface { return true; } + @override + Future spinForAruco(int arucoId, {CameraName? desiredCamera}) async => true; + @override Future stop() async { collection.logger.debug("Stopping"); diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index f3f417f..bd4b7a5 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -35,33 +35,27 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { @override Message getMessage() => statusMessage; - @override - Future handleGpsTask(AutonomyCommand command) async { - final destination = command.destination; - collection.logger.info("Received GPS Task", body: "Go to ${destination.prettyPrint()}"); - collection.logger.debug("Currently at ${collection.gps.coordinates.prettyPrint()}"); - traversed.clear(); - collection.drive.setLedStrip(ProtoColor.RED); - // detect obstacles before and after resolving orientation, as a "scan" - collection.detector.findObstacles(); + Future calculateAndFollowPath(GpsCoordinates goal, {bool abortOnError = true}) async { await collection.drive.resolveOrientation(); collection.detector.findObstacles(); - while (!collection.gps.coordinates.isNear(destination)) { + while (!collection.gps.coordinates.isNear(goal)) { // Calculate a path collection.logger.debug("Finding a path"); currentState = AutonomyState.PATHING; - final path = collection.pathfinder.getPath(destination); + final path = collection.pathfinder.getPath(goal); currentPath = path; // also use local variable path for promotion if (path == null) { final current = collection.gps.coordinates; - collection.logger.error("Could not find a path", body: "No path found from ${current.prettyPrint()} to ${destination.prettyPrint()}"); - currentState = AutonomyState.NO_SOLUTION; - currentCommand = null; - return; + collection.logger.error("Could not find a path", body: "No path found from ${current.prettyPrint()} to ${goal.prettyPrint()}"); + if (abortOnError) { + currentState = AutonomyState.NO_SOLUTION; + currentCommand = null; + } + return false; } // Try to take that path final current = collection.gps.coordinates; - collection.logger.debug("Found a path from ${current.prettyPrint()} to ${destination.prettyPrint()}: ${path.length} steps"); + collection.logger.debug("Found a path from ${current.prettyPrint()} to ${goal.prettyPrint()}: ${path.length} steps"); collection.logger.debug("Here is a summary of the path"); for (final step in path) { collection.logger.debug(step.toString()); @@ -104,7 +98,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } if (currentCommand == null || currentPath == null) { collection.logger.info("Aborting path, command was canceled"); - return; + return false; } traversed.add(state.position); // if (state.direction != DriveDirection.forward) continue; @@ -116,6 +110,25 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } } } + return true; + } + + @override + Future handleGpsTask(AutonomyCommand command) async { + final destination = command.destination; + collection.logger.info("Received GPS Task", body: "Go to ${destination.prettyPrint()}"); + collection.logger.debug("Currently at ${collection.gps.coordinates.prettyPrint()}"); + traversed.clear(); + collection.drive.setLedStrip(ProtoColor.RED); + // detect obstacles before and after resolving orientation, as a "scan" + collection.detector.findObstacles(); + await collection.drive.resolveOrientation(); + collection.detector.findObstacles(); + + if (!await calculateAndFollowPath(command.destination)) { + return; + } + collection.logger.info("Task complete"); collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); currentState = AutonomyState.AT_DESTINATION; @@ -127,18 +140,72 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.drive.setLedStrip(ProtoColor.RED); // Go to GPS coordinates - // await handleGpsTask(command); collection.logger.info("Got ArUco Task"); + if (command.destination != GpsCoordinates(latitude: 0, longitude: 0)) { + if (!await calculateAndFollowPath(command.destination, abortOnError: false)) { + collection.logger.error("Failed to follow path towards initial destination"); + currentState = AutonomyState.NO_SOLUTION; + currentCommand = null; + return; + } + } currentState = AutonomyState.SEARCHING; collection.logger.info("Searching for ArUco tag"); - final didSeeAruco = await collection.drive.spinForAruco(); - if (didSeeAruco) { + final didSeeAruco = await collection.drive.spinForAruco( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ); + var detectedAruco = collection.video.getArucoDetection( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ); + + if (didSeeAruco && detectedAruco != null) { collection.logger.info("Found aruco"); currentState = AutonomyState.APPROACHING; - await collection.drive.approachAruco(); - collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); - currentState = AutonomyState.AT_DESTINATION; + final arucoOrientation = Orientation(z: collection.imu.heading - detectedAruco.yaw); + await collection.drive.faceOrientation(arucoOrientation); + detectedAruco = await collection.video.waitForAruco( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + timeout: const Duration(seconds: 3), + ); + + if (detectedAruco == null || !detectedAruco.hasBestPnpResult()) { + // TODO: handle this condition properly + collection.logger.error("Could not find desired Aruco tag"); + return; + } + + collection.logger.debug( + "Planning path to Aruco ID ${command.arucoId}", + body: "Detection: ${detectedAruco.toProto3Json()}", + ); + + final distanceToTag = detectedAruco.bestPnpResult.cameraToTarget.translation.z.abs() - 0.5; // Don't drive *into* the tag + + if (distanceToTag < 1) { + // well that was easy + collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); + currentState = AutonomyState.AT_DESTINATION; + return; + } + + final relativeX = distanceToTag * sin((collection.imu.heading - detectedAruco.yaw) * pi / 180); + final relativeY = distanceToTag * cos((collection.imu.heading - detectedAruco.yaw) * pi / 180); + + final destinationCoordinates = (collection.gps.coordinates.inMeters + (lat: relativeY, long: relativeX)).toGps(); + + if (await calculateAndFollowPath(destinationCoordinates, abortOnError: false)) { + collection.logger.info("Successfully reached within ${Constants.maxErrorMeters} meters of the Aruco tag"); + collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); + currentState = AutonomyState.AT_DESTINATION; + } + currentCommand = null; + } else { + collection.logger.error("Could not spin towards ArUco tag"); + currentCommand = null; } } diff --git a/lib/src/video/rover_video.dart b/lib/src/video/rover_video.dart index b9442d2..1170108 100644 --- a/lib/src/video/rover_video.dart +++ b/lib/src/video/rover_video.dart @@ -1,31 +1,77 @@ import "dart:async"; +import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; +import "package:collection/collection.dart"; class RoverVideo extends VideoInterface { + final List _cachedResults = []; + RoverVideo({required super.collection}); @override Future init() async { collection.server.messages.onMessage( - name: VideoData().messageName, - constructor: VideoData.fromBuffer, + name: VisionResult().messageName, + constructor: VisionResult.fromBuffer, callback: updateFrame, ); return true; } @override - Future dispose() async { } + Future dispose() async {} + + @override + void updateFrame(VisionResult result) { + hasValue = true; + if (result.objects.isEmpty) return; + + _cachedResults.removeWhere((e) => e.name == result.name); + + _cachedResults.add(result); + } + + @override + DetectedObject? getArucoDetection(int id, {CameraName? desiredCamera}) { + for (final result in _cachedResults.where((e) => e.name == (desiredCamera ?? e.name))) { + for (final object in result.objects) { + if (object.arucoTagId == id) { + return object; + } + } + } + return null; + } @override - void updateFrame(VideoData newData) { - data = newData; - // if (data.arucoDetected == BoolState.YES) { - // flag = true; - // Timer(const Duration(seconds: 3), () => flag = false); - // collection.logger.info("Is ArUco detected: ${data.arucoDetected}"); - // } - hasValue = true; + Future waitForAruco( + int id, { + CameraName? desiredCamera, + Duration timeout = Constants.arucoSearchTimeout, + }) async { + final completer = Completer(); + + late final StreamSubscription resultSubscription; + + resultSubscription = collection.server.messages.onMessage( + name: VisionResult().messageName, + constructor: VisionResult.fromBuffer, + callback: (result) async { + if (result.name != (desiredCamera ?? result.name)) return; + final object = result.objects.firstWhereOrNull((e) => e.arucoTagId == id); + if (object != null) { + await resultSubscription.cancel(); + completer.complete(object); + } + }, + ); + + try { + return await completer.future.timeout(timeout); + } on TimeoutException { + await resultSubscription.cancel(); + return null; + } } } diff --git a/lib/src/video/sim_video.dart b/lib/src/video/sim_video.dart index c7d5f1c..a2eb73d 100644 --- a/lib/src/video/sim_video.dart +++ b/lib/src/video/sim_video.dart @@ -17,5 +17,5 @@ class VideoSimulator extends VideoInterface { Uint16List depthFrame = Uint16List.fromList([]); @override - void updateFrame(VideoData newData) { } + void updateFrame(VisionResult result) {} } diff --git a/lib/src/video/video_interface.dart b/lib/src/video/video_interface.dart index 77ea2ad..4731f97 100644 --- a/lib/src/video/video_interface.dart +++ b/lib/src/video/video_interface.dart @@ -1,3 +1,4 @@ +import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; /// Handles obstacle detection data and ArUco data from video @@ -7,10 +8,14 @@ abstract class VideoInterface extends Service with Receiver { final AutonomyInterface collection; VideoInterface({required this.collection}); - VideoData data = VideoData(); + void updateFrame(VisionResult result); - void updateFrame(VideoData newData); + DetectedObject? getArucoDetection(int id, {CameraName? desiredCamera}) => null; - double get arucoSize => 0; // data.arucoSize; - double get arucoPosition => 0; // data.arucoPosition; + Future waitForAruco( + int id, { + CameraName? desiredCamera, + Duration timeout = Constants.arucoSearchTimeout, + }) => + Future.value(); } diff --git a/pubspec.yaml b/pubspec.yaml index 9dfa7b7..7c5058d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: burt_network: ^2.3.1 a_star: ^3.0.0 meta: ^1.11.0 + collection: ^1.19.1 dev_dependencies: test: ^1.21.0 From 77f2ce6f3ab68c996cfddbd2ef28f234ffb69de4 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 9 Feb 2025 10:49:28 -0500 Subject: [PATCH 043/110] Add lidar obstacle detection --- lib/src/detector/rover_detector.dart | 91 ++++++++++++++++++++++++++-- lib/src/drive/drive_config.dart | 2 +- lib/src/utils/a_star.dart | 3 +- 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/lib/src/detector/rover_detector.dart b/lib/src/detector/rover_detector.dart index 3ed0161..de83e67 100644 --- a/lib/src/detector/rover_detector.dart +++ b/lib/src/detector/rover_detector.dart @@ -1,21 +1,100 @@ +import "dart:async"; +import "dart:math"; + import "package:autonomy/interfaces.dart"; class RoverDetector extends DetectorInterface { + StreamSubscription? _subscription; + + LidarPointCloud cloudCache = LidarPointCloud(); + + List queuedObstacles = []; + RoverDetector({required super.collection}); + void _handleLidarCloud(LidarPointCloud cloud) { + if (cloud.cartesian.isNotEmpty) { + cloudCache.cartesian.clear(); + cloudCache.cartesian.addAll(cloud.cartesian); + } + if (cloud.polar.isNotEmpty) { + cloudCache.polar.clear(); + cloudCache.polar.addAll(cloud.polar); + } + + _queueObstacles(); + } + + void _queueObstacles() { + final cartesian = cloudCache.cartesian; + final polar = cloudCache.polar; + + if (cartesian.isEmpty || polar.isEmpty) { + return; + } + + for (final point in cartesian) { + final angle = atan2(point.y, point.x) * 180 / pi; + final magnitude = sqrt(pow(point.x, 2) + pow(point.y, 2)); + + final matchingPolar = polar.where((e) => + (e.angle - angle.roundToDouble()).abs() <= 1 && + (e.angle - angle.roundToDouble()).abs() != 0, + ); + + // no polar coordinates are near the cartesian coordinate + if (matchingPolar.isEmpty) { + continue; + } + // nearby polar coordinates do not match the cartesian distance, likely a false speck + if (!matchingPolar.any((e) => (e.distance - magnitude).abs() < 0.05)) { + continue; + } + + final imuAngleRad = collection.imu.heading * pi / 180; + + final roverToPoint = ( + long: point.x * cos(imuAngleRad) - (-point.y) * sin(imuAngleRad), + lat: (-point.y) * cos(imuAngleRad) + point.x * sin(imuAngleRad) + ); + + queuedObstacles.add( + (collection.gps.coordinates.inMeters + roverToPoint).toGps(), + ); + } + + cloudCache.cartesian.clear(); + cloudCache.polar.clear(); + } + @override bool isOnSlope() => false; @override - bool findObstacles() => false; + bool findObstacles() { + if (queuedObstacles.isEmpty) return false; - @override -// bool canSeeAruco() => collection.video.data.arucoDetected == BoolState.YES; - bool canSeeAruco() => collection.video.flag; + for (final obstacle in queuedObstacles) { + collection.pathfinder.recordObstacle(obstacle); + } + + queuedObstacles.clear(); + + return true; + } @override - Future init() async => true; + Future init() async { + _subscription = collection.server.messages.onMessage( + name: LidarPointCloud().messageName, + constructor: LidarPointCloud.fromBuffer, + callback: _handleLidarCloud, + ); + return true; + } @override - Future dispose() async { } + Future dispose() async { + await _subscription?.cancel(); + } } diff --git a/lib/src/drive/drive_config.dart b/lib/src/drive/drive_config.dart index eed787e..c0cd43e 100644 --- a/lib/src/drive/drive_config.dart +++ b/lib/src/drive/drive_config.dart @@ -42,7 +42,7 @@ class DriveConfig { const roverConfig = DriveConfig( forwardThrottle: 0.2, turnThrottle: 0.075, - oneMeterDelay: Duration(milliseconds: 5500), + oneMeterDelay: Duration(milliseconds: 2250), turnDelay: Duration(milliseconds: 4500), subsystemsAddress: "192.168.1.20", ); diff --git a/lib/src/utils/a_star.dart b/lib/src/utils/a_star.dart index 41b132f..dd0de0c 100644 --- a/lib/src/utils/a_star.dart +++ b/lib/src/utils/a_star.dart @@ -155,7 +155,8 @@ class AutonomyAStarState extends AStarState { } bool isValidState(AutonomyAStarState state) => - !collection.pathfinder.isObstacle(state.position) + !(state.instruction == DriveDirection.forward && + collection.pathfinder.isObstacle(state.position)) && !willDriveThroughObstacle(state); Iterable _allNeighbors() => [ From e7b0f4e0f2769c6294fe6c453c38b2f9607acca9 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 9 Feb 2025 10:52:19 -0500 Subject: [PATCH 044/110] Fixed build errors --- lib/src/detector/detector_interface.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/detector/detector_interface.dart b/lib/src/detector/detector_interface.dart index c42a694..8d5909e 100644 --- a/lib/src/detector/detector_interface.dart +++ b/lib/src/detector/detector_interface.dart @@ -5,6 +5,5 @@ abstract class DetectorInterface extends Service { DetectorInterface({required this.collection}); bool findObstacles(); - bool canSeeAruco(); bool isOnSlope(); } From 5c1b5894c452e9f23ff8b299118a86c0a51c256e Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 9 Feb 2025 10:52:55 -0500 Subject: [PATCH 045/110] Remove all implementations of canSeeAruco --- lib/src/detector/network_detector.dart | 3 --- lib/src/detector/sim_detector.dart | 3 --- lib/src/drive/sensor_drive.dart | 18 +++++++++--------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/lib/src/detector/network_detector.dart b/lib/src/detector/network_detector.dart index c91934d..37e7796 100644 --- a/lib/src/detector/network_detector.dart +++ b/lib/src/detector/network_detector.dart @@ -6,9 +6,6 @@ class NetworkDetector extends DetectorInterface { NetworkDetector({required super.collection}); - @override - bool canSeeAruco() => false; - @override Future dispose() async {} diff --git a/lib/src/detector/sim_detector.dart b/lib/src/detector/sim_detector.dart index dfac538..4b8e57a 100644 --- a/lib/src/detector/sim_detector.dart +++ b/lib/src/detector/sim_detector.dart @@ -37,9 +37,6 @@ class DetectorSimulator extends DetectorInterface { return result; } - @override - bool canSeeAruco() => false; // if can see [arucoPosition] - @override bool isOnSlope() => false; // if on [slopedLatitude] } diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 7a9654f..7d62634 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -109,15 +109,15 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { @override Future approachAruco() async { - const sizeThreshold = 0.2; - const epsilon = 0.00001; - setThrottle(config.forwardThrottle); - moveForward(); - await waitFor(() { - final size = collection.video.arucoSize; - collection.logger.trace("The Aruco tag is at $size percent"); - return (size.abs() < epsilon && !collection.detector.canSeeAruco()) || size >= sizeThreshold; - }).timeout(config.oneMeterDelay * 5); + // const sizeThreshold = 0.2; + // const epsilon = 0.00001; + // setThrottle(config.forwardThrottle); + // moveForward(); + // await waitFor(() { + // final size = collection.video.arucoSize; + // collection.logger.trace("The Aruco tag is at $size percent"); + // return (size.abs() < epsilon && !collection.detector.canSeeAruco()) || size >= sizeThreshold; + // }).timeout(config.oneMeterDelay * 5); await stop(); } } From ed065cbe3c35ba71d5fa4260413e686167d882c7 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 9 Feb 2025 10:54:09 -0500 Subject: [PATCH 046/110] Remove aruco methods (i think) --- lib/src/drive/sensor_drive.dart | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 7d62634..c2fcead 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -95,29 +95,4 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { collection.logger.trace("Current heading: $current"); return collection.imu.isNear(orientation); } - - @override - Future spinForAruco() async { - setThrottle(config.turnThrottle); - spinLeft(); - final result = await waitFor(() => collection.detector.canSeeAruco()) - .then((_) => true) - .timeout(config.turnDelay * 4, onTimeout: () => false); - await stop(); - return result; - } - - @override - Future approachAruco() async { - // const sizeThreshold = 0.2; - // const epsilon = 0.00001; - // setThrottle(config.forwardThrottle); - // moveForward(); - // await waitFor(() { - // final size = collection.video.arucoSize; - // collection.logger.trace("The Aruco tag is at $size percent"); - // return (size.abs() < epsilon && !collection.detector.canSeeAruco()) || size >= sizeThreshold; - // }).timeout(config.oneMeterDelay * 5); - await stop(); - } } From 2f8becfb555d827fb947378432e9faf164dce2de Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 9 Feb 2025 15:40:56 -0500 Subject: [PATCH 047/110] Update protobuf messages --- lib/src/video/rover_video.dart | 39 ++++++++++++------------------ lib/src/video/sim_video.dart | 11 ++------- lib/src/video/video_interface.dart | 26 ++++++++++++++++---- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/lib/src/video/rover_video.dart b/lib/src/video/rover_video.dart index 1170108..b0d7167 100644 --- a/lib/src/video/rover_video.dart +++ b/lib/src/video/rover_video.dart @@ -5,37 +5,27 @@ import "package:autonomy/interfaces.dart"; import "package:collection/collection.dart"; class RoverVideo extends VideoInterface { - final List _cachedResults = []; + final List _cachedResults = []; RoverVideo({required super.collection}); @override - Future init() async { - collection.server.messages.onMessage( - name: VisionResult().messageName, - constructor: VisionResult.fromBuffer, - callback: updateFrame, - ); - return true; - } - - @override - Future dispose() async {} - - @override - void updateFrame(VisionResult result) { + void updateFrame(VideoData result) { hasValue = true; - if (result.objects.isEmpty) return; + if (result.hasFrame()) return; - _cachedResults.removeWhere((e) => e.name == result.name); + _cachedResults.removeWhere((e) => e.details.name == result.details.name); + if (result.detectedObjects.isEmpty) return; _cachedResults.add(result); } @override DetectedObject? getArucoDetection(int id, {CameraName? desiredCamera}) { - for (final result in _cachedResults.where((e) => e.name == (desiredCamera ?? e.name))) { - for (final object in result.objects) { + for (final result in _cachedResults.where( + (e) => e.details.name == (desiredCamera ?? e.details.name), + )) { + for (final object in result.detectedObjects) { if (object.arucoTagId == id) { return object; } @@ -52,14 +42,15 @@ class RoverVideo extends VideoInterface { }) async { final completer = Completer(); - late final StreamSubscription resultSubscription; + late final StreamSubscription resultSubscription; resultSubscription = collection.server.messages.onMessage( - name: VisionResult().messageName, - constructor: VisionResult.fromBuffer, + name: VideoData().messageName, + constructor: VideoData.fromBuffer, callback: (result) async { - if (result.name != (desiredCamera ?? result.name)) return; - final object = result.objects.firstWhereOrNull((e) => e.arucoTagId == id); + if (result.hasFrame()) return; + if (result.details.name != (desiredCamera ?? result.details.name)) return; + final object = result.detectedObjects.firstWhereOrNull((e) => e.arucoTagId == id); if (object != null) { await resultSubscription.cancel(); completer.complete(object); diff --git a/lib/src/video/sim_video.dart b/lib/src/video/sim_video.dart index a2eb73d..ca6b17b 100644 --- a/lib/src/video/sim_video.dart +++ b/lib/src/video/sim_video.dart @@ -1,5 +1,3 @@ -import "dart:typed_data"; - import "package:autonomy/interfaces.dart"; class VideoSimulator extends VideoInterface { @@ -8,14 +6,9 @@ class VideoSimulator extends VideoInterface { @override Future init() async { hasValue = true; - return true; + return super.init(); } @override - Future dispose() async => depthFrame = Uint16List.fromList([]); - - Uint16List depthFrame = Uint16List.fromList([]); - - @override - void updateFrame(VisionResult result) {} + void updateFrame(VideoData result) {} } diff --git a/lib/src/video/video_interface.dart b/lib/src/video/video_interface.dart index 4731f97..a9d095d 100644 --- a/lib/src/video/video_interface.dart +++ b/lib/src/video/video_interface.dart @@ -1,14 +1,31 @@ +import "dart:async"; + import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; /// Handles obstacle detection data and ArUco data from video abstract class VideoInterface extends Service with Receiver { - bool flag = false; - final AutonomyInterface collection; + StreamSubscription? _dataSubscription; + VideoInterface({required this.collection}); - void updateFrame(VisionResult result); + @override + Future init() async { + _dataSubscription = collection.server.messages.onMessage( + name: VideoData().messageName, + constructor: VideoData.fromBuffer, + callback: updateFrame, + ); + return true; + } + + @override + Future dispose() async { + await _dataSubscription?.cancel(); + } + + void updateFrame(VideoData result); DetectedObject? getArucoDetection(int id, {CameraName? desiredCamera}) => null; @@ -16,6 +33,5 @@ abstract class VideoInterface extends Service with Receiver { int id, { CameraName? desiredCamera, Duration timeout = Constants.arucoSearchTimeout, - }) => - Future.value(); + }) => Future.value(); } From 78765209959b04dec38bead2e23e152c36033c0c Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 9 Feb 2025 15:41:27 -0500 Subject: [PATCH 048/110] Bump mandatory burt_network version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9dfa7b7..c1f41ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,7 +9,7 @@ environment: # Try not to edit this section by hand. Use `dart pub add my_package` dependencies: - burt_network: ^2.3.1 + burt_network: ^2.4.0 a_star: ^3.0.0 meta: ^1.11.0 From afc808232d07f557408155cd9ecf862da500aced Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 11 Feb 2025 21:33:49 -0500 Subject: [PATCH 049/110] Tested with actual lidar --- lib/src/detector/rover_detector.dart | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/src/detector/rover_detector.dart b/lib/src/detector/rover_detector.dart index de83e67..b795740 100644 --- a/lib/src/detector/rover_detector.dart +++ b/lib/src/detector/rover_detector.dart @@ -9,6 +9,7 @@ class RoverDetector extends DetectorInterface { LidarPointCloud cloudCache = LidarPointCloud(); List queuedObstacles = []; + List previousObstacles = []; RoverDetector({required super.collection}); @@ -37,6 +38,10 @@ class RoverDetector extends DetectorInterface { final angle = atan2(point.y, point.x) * 180 / pi; final magnitude = sqrt(pow(point.x, 2) + pow(point.y, 2)); + if (magnitude <= 0.1) { + continue; + } + final matchingPolar = polar.where((e) => (e.angle - angle.roundToDouble()).abs() <= 1 && (e.angle - angle.roundToDouble()).abs() != 0, @@ -46,16 +51,17 @@ class RoverDetector extends DetectorInterface { if (matchingPolar.isEmpty) { continue; } + // nearby polar coordinates do not match the cartesian distance, likely a false speck - if (!matchingPolar.any((e) => (e.distance - magnitude).abs() < 0.05)) { + if (!matchingPolar.any((e) => (e.distance - magnitude).abs() < 0.1)) { continue; } - final imuAngleRad = collection.imu.heading * pi / 180; + final imuAngleRad = collection.imu.heading * pi / 180 + pi / 2; final roverToPoint = ( - long: point.x * cos(imuAngleRad) - (-point.y) * sin(imuAngleRad), - lat: (-point.y) * cos(imuAngleRad) + point.x * sin(imuAngleRad) + long: point.x * cos(imuAngleRad) - point.y * sin(imuAngleRad), + lat: point.y * cos(imuAngleRad) + point.x * sin(imuAngleRad) ); queuedObstacles.add( @@ -74,10 +80,15 @@ class RoverDetector extends DetectorInterface { bool findObstacles() { if (queuedObstacles.isEmpty) return false; + collection.pathfinder.obstacles.removeAll(previousObstacles); + previousObstacles.clear(); + for (final obstacle in queuedObstacles) { collection.pathfinder.recordObstacle(obstacle); } + previousObstacles.addAll(queuedObstacles); + queuedObstacles.clear(); return true; From d79f7900726722b274b6023e3cc5711762c3b094 Mon Sep 17 00:00:00 2001 From: Binghamton Rover Date: Wed, 12 Feb 2025 21:48:51 -0500 Subject: [PATCH 050/110] Testing on pi --- bin/autonomy.dart | 2 ++ lib/constants.dart | 4 ++-- lib/src/orchestrator/rover_orchestrator.dart | 6 +++--- lib/src/video/rover_video.dart | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bin/autonomy.dart b/bin/autonomy.dart index 31bd0b6..5bf6d3d 100644 --- a/bin/autonomy.dart +++ b/bin/autonomy.dart @@ -1,6 +1,8 @@ import "package:autonomy/rover.dart"; +import "package:burt_network/logging.dart"; void main() async { + Logger.level = Level.debug; final rover = RoverAutonomy(); await rover.init(); } diff --git a/lib/constants.dart b/lib/constants.dart index ce6fbdf..cbfcb1b 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -28,8 +28,8 @@ class Constants { static const Duration driveGPSTimeout = Duration(seconds: 4, milliseconds: 500); /// The maximum time to spend searching for an aruco tag - static const Duration arucoSearchTimeout = Duration(seconds: 10); + static const Duration arucoSearchTimeout = Duration(seconds: 20); /// The camera that should be used to detect Aruco tags - static const CameraName arucoDetectionCamera = CameraName.ROVER_FRONT; + static const CameraName arucoDetectionCamera = CameraName.AUTONOMY_DEPTH; } diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index bd4b7a5..ffa7b46 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -183,7 +183,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { body: "Detection: ${detectedAruco.toProto3Json()}", ); - final distanceToTag = detectedAruco.bestPnpResult.cameraToTarget.translation.z.abs() - 0.5; // Don't drive *into* the tag + final distanceToTag = detectedAruco.bestPnpResult.cameraToTarget.translation.z.abs() - 1; // Don't drive *into* the tag if (distanceToTag < 1) { // well that was easy @@ -192,8 +192,8 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { return; } - final relativeX = distanceToTag * sin((collection.imu.heading - detectedAruco.yaw) * pi / 180); - final relativeY = distanceToTag * cos((collection.imu.heading - detectedAruco.yaw) * pi / 180); + final relativeX = distanceToTag * sin((collection.imu.heading - detectedAruco.yaw - 90) * pi / 180); + final relativeY = distanceToTag * cos((collection.imu.heading - detectedAruco.yaw - 90) * pi / 180); final destinationCoordinates = (collection.gps.coordinates.inMeters + (lat: relativeY, long: relativeX)).toGps(); diff --git a/lib/src/video/rover_video.dart b/lib/src/video/rover_video.dart index b0d7167..491342d 100644 --- a/lib/src/video/rover_video.dart +++ b/lib/src/video/rover_video.dart @@ -12,7 +12,7 @@ class RoverVideo extends VideoInterface { @override void updateFrame(VideoData result) { hasValue = true; - if (result.hasFrame()) return; + if (result.hasFrame() && result.frame.isNotEmpty) return; _cachedResults.removeWhere((e) => e.details.name == result.details.name); if (result.detectedObjects.isEmpty) return; From 8a711fd23a00ec603f7d83724d8ecfa4765cf753 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Wed, 12 Feb 2025 22:10:35 -0500 Subject: [PATCH 051/110] Ignore obstacles 90 degrees off --- lib/src/detector/rover_detector.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/src/detector/rover_detector.dart b/lib/src/detector/rover_detector.dart index b795740..8026148 100644 --- a/lib/src/detector/rover_detector.dart +++ b/lib/src/detector/rover_detector.dart @@ -34,15 +34,22 @@ class RoverDetector extends DetectorInterface { return; } + queuedObstacles.clear(); + for (final point in cartesian) { final angle = atan2(point.y, point.x) * 180 / pi; final magnitude = sqrt(pow(point.x, 2) + pow(point.y, 2)); + if (angle >= pi / 2) { + continue; + } + if (magnitude <= 0.1) { continue; } - final matchingPolar = polar.where((e) => + final matchingPolar = polar.where( + (e) => (e.angle - angle.roundToDouble()).abs() <= 1 && (e.angle - angle.roundToDouble()).abs() != 0, ); @@ -61,7 +68,7 @@ class RoverDetector extends DetectorInterface { final roverToPoint = ( long: point.x * cos(imuAngleRad) - point.y * sin(imuAngleRad), - lat: point.y * cos(imuAngleRad) + point.x * sin(imuAngleRad) + lat: point.y * cos(imuAngleRad) + point.x * sin(imuAngleRad), ); queuedObstacles.add( From 5a7ecc75f483e65c18134f99ef4465ded2cbc46c Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:42:16 -0500 Subject: [PATCH 052/110] Fixed distance and angle errors --- lib/constants.dart | 4 ++-- lib/src/orchestrator/rover_orchestrator.dart | 14 +++++++++++--- lib/src/video/rover_video.dart | 9 ++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index cbfcb1b..f93986e 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -28,8 +28,8 @@ class Constants { static const Duration driveGPSTimeout = Duration(seconds: 4, milliseconds: 500); /// The maximum time to spend searching for an aruco tag - static const Duration arucoSearchTimeout = Duration(seconds: 20); + static const Duration arucoSearchTimeout = Duration(seconds: 25); /// The camera that should be used to detect Aruco tags - static const CameraName arucoDetectionCamera = CameraName.AUTONOMY_DEPTH; + static const CameraName arucoDetectionCamera = CameraName.ROVER_FRONT; } diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index ffa7b46..f49f0b6 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -183,7 +183,15 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { body: "Detection: ${detectedAruco.toProto3Json()}", ); - final distanceToTag = detectedAruco.bestPnpResult.cameraToTarget.translation.z.abs() - 1; // Don't drive *into* the tag + // In theory we could just find the relative position with the translation x and z, + // however if the tag's rotation relative to itself is off (which can be common + // when facing it head on), then it will be extremely innacurate. Since the SolvePnP's + // distance is always extremely accurate, it is more reliable to use the distance + // hypotenuse to the camera combined with trig of the tag's angle relative to the camera. + final cameraToTag = detectedAruco.bestPnpResult.cameraToTarget; + final distanceToTag = sqrt( + pow(cameraToTag.translation.z, 2) + pow(cameraToTag.translation.x, 2), + ) - 1; // don't drive *into* the tag if (distanceToTag < 1) { // well that was easy @@ -192,8 +200,8 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { return; } - final relativeX = distanceToTag * sin((collection.imu.heading - detectedAruco.yaw - 90) * pi / 180); - final relativeY = distanceToTag * cos((collection.imu.heading - detectedAruco.yaw - 90) * pi / 180); + final relativeX = -distanceToTag * sin((collection.imu.heading - detectedAruco.yaw) * pi / 180); + final relativeY = distanceToTag * cos((collection.imu.heading - detectedAruco.yaw) * pi / 180); final destinationCoordinates = (collection.gps.coordinates.inMeters + (lat: relativeY, long: relativeX)).toGps(); diff --git a/lib/src/video/rover_video.dart b/lib/src/video/rover_video.dart index 491342d..3698d82 100644 --- a/lib/src/video/rover_video.dart +++ b/lib/src/video/rover_video.dart @@ -26,7 +26,8 @@ class RoverVideo extends VideoInterface { (e) => e.details.name == (desiredCamera ?? e.details.name), )) { for (final object in result.detectedObjects) { - if (object.arucoTagId == id) { + if (object.objectType == DetectedObjectType.ARUCO && + object.arucoTagId == id) { return object; } } @@ -48,9 +49,11 @@ class RoverVideo extends VideoInterface { name: VideoData().messageName, constructor: VideoData.fromBuffer, callback: (result) async { - if (result.hasFrame()) return; + if (result.hasFrame() && result.frame.isNotEmpty) return; if (result.details.name != (desiredCamera ?? result.details.name)) return; - final object = result.detectedObjects.firstWhereOrNull((e) => e.arucoTagId == id); + final object = result.detectedObjects.firstWhereOrNull( + (e) => e.objectType == DetectedObjectType.ARUCO && e.arucoTagId == id, + ); if (object != null) { await resultSubscription.cancel(); completer.complete(object); From ab40fdde50296e942db72e643956ca649c7fbe98 Mon Sep 17 00:00:00 2001 From: Binghamton Rover Date: Thu, 13 Feb 2025 21:40:20 -0500 Subject: [PATCH 053/110] Works on rover! --- lib/constants.dart | 2 +- lib/src/drive/drive_config.dart | 2 +- lib/src/orchestrator/rover_orchestrator.dart | 56 ++++++++++++++++++-- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index f93986e..a712176 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -28,7 +28,7 @@ class Constants { static const Duration driveGPSTimeout = Duration(seconds: 4, milliseconds: 500); /// The maximum time to spend searching for an aruco tag - static const Duration arucoSearchTimeout = Duration(seconds: 25); + static const Duration arucoSearchTimeout = Duration(seconds: 20); /// The camera that should be used to detect Aruco tags static const CameraName arucoDetectionCamera = CameraName.ROVER_FRONT; diff --git a/lib/src/drive/drive_config.dart b/lib/src/drive/drive_config.dart index eed787e..d5f4419 100644 --- a/lib/src/drive/drive_config.dart +++ b/lib/src/drive/drive_config.dart @@ -42,7 +42,7 @@ class DriveConfig { const roverConfig = DriveConfig( forwardThrottle: 0.2, turnThrottle: 0.075, - oneMeterDelay: Duration(milliseconds: 5500), + oneMeterDelay: Duration(milliseconds: 5500 ~/ 2), turnDelay: Duration(milliseconds: 4500), subsystemsAddress: "192.168.1.20", ); diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index f49f0b6..65d508b 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -35,10 +35,14 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { @override Message getMessage() => statusMessage; - Future calculateAndFollowPath(GpsCoordinates goal, {bool abortOnError = true}) async { + Future calculateAndFollowPath( + GpsCoordinates goal, { + bool abortOnError = true, + bool Function()? alternateEndCondition, + }) async { await collection.drive.resolveOrientation(); collection.detector.findObstacles(); - while (!collection.gps.coordinates.isNear(goal)) { + while (!collection.gps.coordinates.isNear(goal) && !(alternateEndCondition?.call() ?? false)) { // Calculate a path collection.logger.debug("Finding a path"); currentState = AutonomyState.PATHING; @@ -64,6 +68,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { var count = 0; for (final state in path) { collection.logger.debug(state.toString()); + // Alternate end condition may have hit between steps + if (alternateEndCondition?.call() ?? false) { + break; + } // Replan if too far from start point final distanceError = collection.gps.coordinates.distanceTo(state.startPostition); if (distanceError >= Constants.replanErrorMeters) { @@ -205,7 +213,49 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { final destinationCoordinates = (collection.gps.coordinates.inMeters + (lat: relativeY, long: relativeX)).toGps(); - if (await calculateAndFollowPath(destinationCoordinates, abortOnError: false)) { + if (await calculateAndFollowPath( + destinationCoordinates, + abortOnError: false, + alternateEndCondition: () { + detectedAruco = collection.video.getArucoDetection( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ); + if (detectedAruco == null) { + return false; + } + final cameraToTag = detectedAruco!.bestPnpResult.cameraToTarget; + final distanceToTag = sqrt( + pow(cameraToTag.translation.z, 2) + + pow(cameraToTag.translation.x, 2), + ); + return distanceToTag < 0.75; + }, + )) { + detectedAruco = collection.video.getArucoDetection( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ); + if (detectedAruco == null) { + await collection.drive.spinForAruco( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ); + } + + detectedAruco = collection.video.getArucoDetection( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ); + + if (detectedAruco != null) { + await collection.drive.faceOrientation( + Orientation( + z: collection.imu.heading - detectedAruco!.yaw, + ), + ); + } + collection.logger.info("Successfully reached within ${Constants.maxErrorMeters} meters of the Aruco tag"); collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); currentState = AutonomyState.AT_DESTINATION; From 227c309a94023bf5f409397735c87bb6a9b561af Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 14 Feb 2025 09:41:49 -0500 Subject: [PATCH 054/110] More log messages for aruco re-searching --- lib/src/orchestrator/rover_orchestrator.dart | 60 +++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 65d508b..bd1cb50 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -213,7 +213,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { final destinationCoordinates = (collection.gps.coordinates.inMeters + (lat: relativeY, long: relativeX)).toGps(); - if (await calculateAndFollowPath( + if (!await calculateAndFollowPath( destinationCoordinates, abortOnError: false, alternateEndCondition: () { @@ -229,42 +229,46 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { pow(cameraToTag.translation.z, 2) + pow(cameraToTag.translation.x, 2), ); - return distanceToTag < 0.75; + return distanceToTag < 1; }, )) { - detectedAruco = collection.video.getArucoDetection( + collection.logger.error("Could not spin towards ArUco tag"); + currentCommand = null; + return; + } + collection.logger.info("Arrived at estimated Aruco position"); + detectedAruco = collection.video.getArucoDetection( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ); + if (detectedAruco == null) { + collection.logger.info("Re-spinning to find Aruco"); + await collection.drive.spinForAruco( command.arucoId, desiredCamera: Constants.arucoDetectionCamera, ); - if (detectedAruco == null) { - await collection.drive.spinForAruco( - command.arucoId, - desiredCamera: Constants.arucoDetectionCamera, - ); - } + } - detectedAruco = collection.video.getArucoDetection( - command.arucoId, - desiredCamera: Constants.arucoDetectionCamera, + detectedAruco = collection.video.getArucoDetection( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ); + if (detectedAruco != null) { + collection.logger.info("Rotating towards Aruco"); + await collection.drive.faceOrientation( + Orientation( + z: collection.imu.heading - detectedAruco!.yaw, + ), ); - - if (detectedAruco != null) { - await collection.drive.faceOrientation( - Orientation( - z: collection.imu.heading - detectedAruco!.yaw, - ), - ); - } - - collection.logger.info("Successfully reached within ${Constants.maxErrorMeters} meters of the Aruco tag"); - collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); - currentState = AutonomyState.AT_DESTINATION; + } else { + collection.logger.warning("Could not find Aruco after following path"); } - currentCommand = null; - } else { - collection.logger.error("Could not spin towards ArUco tag"); - currentCommand = null; + + collection.logger.info("Successfully reached within ${Constants.maxErrorMeters} meters of the Aruco tag"); + collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); + currentState = AutonomyState.AT_DESTINATION; } + currentCommand = null; } @override From 18ee0c805386c8e5bd659b837c59744c3af18a68 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:02:04 -0500 Subject: [PATCH 055/110] Use UTM coordinates for translating 1 meter --- lib/src/detector/rover_detector.dart | 11 ++-- lib/src/orchestrator/rover_orchestrator.dart | 11 +++- lib/src/utils/gps_utils.dart | 63 +++++++++++--------- pubspec.yaml | 1 + 4 files changed, 51 insertions(+), 35 deletions(-) diff --git a/lib/src/detector/rover_detector.dart b/lib/src/detector/rover_detector.dart index 8026148..da60e26 100644 --- a/lib/src/detector/rover_detector.dart +++ b/lib/src/detector/rover_detector.dart @@ -2,6 +2,7 @@ import "dart:async"; import "dart:math"; import "package:autonomy/interfaces.dart"; +import "package:coordinate_converter/coordinate_converter.dart"; class RoverDetector extends DetectorInterface { StreamSubscription? _subscription; @@ -66,13 +67,15 @@ class RoverDetector extends DetectorInterface { final imuAngleRad = collection.imu.heading * pi / 180 + pi / 2; - final roverToPoint = ( - long: point.x * cos(imuAngleRad) - point.y * sin(imuAngleRad), - lat: point.y * cos(imuAngleRad) + point.x * sin(imuAngleRad), + final roverToPoint = UTMCoordinates( + x: point.x * cos(imuAngleRad) - point.y * sin(imuAngleRad), + y: point.y * cos(imuAngleRad) + point.x * sin(imuAngleRad), + zoneNumber: 1, ); queuedObstacles.add( - (collection.gps.coordinates.inMeters + roverToPoint).toGps(), + (collection.gps.coordinates.asUtmCoordinates + roverToPoint) + .asGpsCoordinates, ); } diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index bd1cb50..3646938 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -4,6 +4,8 @@ import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; import "dart:async"; +import "package:coordinate_converter/coordinate_converter.dart"; + class RoverOrchestrator extends OrchestratorInterface with ValueReporter { final List traversed = []; List? currentPath; @@ -83,9 +85,9 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { Orientation targetOrientation; // if it has RTK, point towards the next coordinate if (collection.gps.coordinates.hasRTK) { - final difference = state.position.inMeters - collection.gps.coordinates.inMeters; + final difference = state.position.asUtmCoordinates - collection.gps.coordinates.asUtmCoordinates; - final angle = atan2(difference.lat, difference.long) * 180 / pi; + final angle = atan2(difference.y, difference.x) * 180 / pi; targetOrientation = Orientation(z: angle); } else { @@ -211,7 +213,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { final relativeX = -distanceToTag * sin((collection.imu.heading - detectedAruco.yaw) * pi / 180); final relativeY = distanceToTag * cos((collection.imu.heading - detectedAruco.yaw) * pi / 180); - final destinationCoordinates = (collection.gps.coordinates.inMeters + (lat: relativeY, long: relativeX)).toGps(); + final destinationCoordinates = + (collection.gps.coordinates.asUtmCoordinates + + UTMCoordinates(y: relativeY, x: relativeX, zoneNumber: 1)) + .asGpsCoordinates; if (!await calculateAndFollowPath( destinationCoordinates, diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index b91a0bf..9c85d83 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -3,6 +3,7 @@ import "dart:math"; import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; +import "package:coordinate_converter/coordinate_converter.dart"; /// An alias for gps coordinates measured in meters typedef GpsMeters = ({num lat, num long}); @@ -23,29 +24,41 @@ extension GpsMetersUtil on GpsMeters { } extension GpsUtils on GpsCoordinates { - static const GpsMeters eastMeters = (lat: 0, long: Constants.moveLengthMeters); - static const GpsMeters westMeters = (lat: 0, long: -Constants.moveLengthMeters); - static const GpsMeters northMeters = (lat: Constants.moveLengthMeters, long: 0); - static const GpsMeters southMeters = (lat: -Constants.moveLengthMeters, long: 0); - static final GpsMeters northEastMeters = northMeters + eastMeters; - static final GpsMeters northWestMeters = northMeters + westMeters; - static final GpsMeters southEastMeters = southMeters + eastMeters; - static final GpsMeters southWestMeters = southMeters + westMeters; + static UTMCoordinates eastMeters = UTMCoordinates(y: 0, x: Constants.moveLengthMeters, zoneNumber: 1); + static UTMCoordinates westMeters = UTMCoordinates(y: 0, x: -Constants.moveLengthMeters, zoneNumber: 1); + static UTMCoordinates northMeters = UTMCoordinates(y: Constants.moveLengthMeters, x: 0, zoneNumber: 1); + static UTMCoordinates southMeters = UTMCoordinates(y: -Constants.moveLengthMeters, x: 0, zoneNumber: 1); + static final UTMCoordinates northEastMeters = northMeters + eastMeters; + static final UTMCoordinates northWestMeters = northMeters + westMeters; + static final UTMCoordinates southEastMeters = southMeters + eastMeters; + static final UTMCoordinates southWestMeters = southMeters + westMeters; /// Whether or not the coordinates is fused with the RTK algorithm bool get hasRTK => rtkMode == RTKMode.RTK_FIXED || rtkMode == RTKMode.RTK_FLOAT; + /// The distance to [other] using the haversine formula double distanceTo(GpsCoordinates other) { - final deltaMeters = inMeters - other.inMeters; - - return sqrt(pow(deltaMeters.long, 2) + pow(deltaMeters.lat, 2)); + // Radius of the earth in meters + const double earthRadius = 6371000; + // Calculate the differences between the coordinates + final deltaLat = (latitude - other.latitude) * pi / 180; + final deltaLong = (longitude - other.longitude) * pi / 180; + + // Apply the Haversine formula + final a = pow(sin(deltaLat / 2), 2) + + cos(other.latitude * pi / 180) * + cos(latitude * pi / 180) * + pow(sin(deltaLong / 2), 2); + final c = 2 * atan2(sqrt(a), sqrt(1 - a)); + + return earthRadius * c; } double heuristicDistance(GpsCoordinates other) { var distance = 0.0; - final delta = inMeters - other.inMeters; - final deltaLat = delta.lat.abs(); - final deltaLong = delta.long.abs(); + final delta = asUtmCoordinates - other.asUtmCoordinates; + final deltaLat = delta.y.abs(); + final deltaLong = delta.x.abs(); final minimumDistance = min(deltaLat, deltaLong); if (minimumDistance >= Constants.moveLengthMeters) { @@ -62,28 +75,22 @@ extension GpsUtils on GpsCoordinates { } double manhattanDistance(GpsCoordinates other) { - final delta = inMeters - other.inMeters; - return delta.lat.toDouble().abs() + delta.long.abs(); + final delta = asUtmCoordinates - other.asUtmCoordinates; + return delta.x.abs() + delta.y.abs(); } double octileDistance(GpsCoordinates other) { - final delta = inMeters - other.inMeters; - final dx = delta.long.abs() ~/ Constants.moveLengthMeters; - final dy = delta.lat.abs() ~/ Constants.moveLengthMeters; + final delta = asUtmCoordinates - other.asUtmCoordinates; + final dx = delta.x.abs() ~/ Constants.moveLengthMeters; + final dy = delta.y.abs() ~/ Constants.moveLengthMeters; return max(dx, dy) + (sqrt2 - 1) * min(dx, dy); } bool isNear(GpsCoordinates other, [double? tolerance]) { tolerance ??= Constants.maxErrorMeters; - final currentMeters = inMeters; - final otherMeters = other.inMeters; - - final delta = currentMeters - otherMeters; - - final distance = sqrt(pow(delta.long, 2) + pow(delta.lat, 2)); - return distance < tolerance; + return distanceTo(other) < tolerance; } GpsCoordinates operator +(GpsCoordinates other) => GpsCoordinates( @@ -98,7 +105,7 @@ extension GpsUtils on GpsCoordinates { String prettyPrint() => toProto3Json().toString(); - GpsCoordinates goForward(CardinalDirection orientation) => (inMeters + + GpsCoordinates goForward(CardinalDirection orientation) => (asUtmCoordinates + switch (orientation) { CardinalDirection.north => GpsUtils.northMeters, CardinalDirection.south => GpsUtils.southMeters, @@ -108,5 +115,5 @@ extension GpsUtils on GpsCoordinates { CardinalDirection.northWest => GpsUtils.northWestMeters, CardinalDirection.southEast => GpsUtils.southEastMeters, CardinalDirection.southWest => GpsUtils.southWestMeters, - }).toGps(); + }).asGpsCoordinates; } diff --git a/pubspec.yaml b/pubspec.yaml index d32e8ee..3ea0d73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: a_star: ^3.0.0 meta: ^1.11.0 collection: ^1.19.1 + coordinate_converter: ^1.1.1 dev_dependencies: test: ^1.21.0 From 9917143aca378c9d19916e55d7265411222ef563 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:04:20 -0500 Subject: [PATCH 056/110] Find obstacles every time the path is replanned --- lib/src/orchestrator/rover_orchestrator.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index bd1cb50..b3b7233 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -76,6 +76,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { final distanceError = collection.gps.coordinates.distanceTo(state.startPostition); if (distanceError >= Constants.replanErrorMeters) { collection.logger.info("Replanning Path", body: "Rover is $distanceError meters off the path"); + collection.detector.findObstacles(); break; } // Re-align to desired start orientation if angle is too far @@ -102,6 +103,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } // If there was an error (usually a timeout) while driving, replan path if (!await collection.drive.driveState(state)) { + collection.detector.findObstacles(); break; } if (currentCommand == null || currentPath == null) { @@ -110,7 +112,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } traversed.add(state.position); // if (state.direction != DriveDirection.forward) continue; - if (++count >= 5) break; + if (++count >= 5) { + collection.detector.findObstacles(); + break; + } final foundObstacle = collection.detector.findObstacles(); if (foundObstacle) { collection.logger.debug("Found an obstacle. Recalculating path..."); From 643406d0a1e7f515c332f8155adad8adf7b375ca Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:52:54 -0500 Subject: [PATCH 057/110] Ignore invalid IMU updates --- lib/src/imu/rover_imu.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/imu/rover_imu.dart b/lib/src/imu/rover_imu.dart index d16dbcb..5a3cf03 100644 --- a/lib/src/imu/rover_imu.dart +++ b/lib/src/imu/rover_imu.dart @@ -32,6 +32,12 @@ class RoverImu extends ImuInterface { void _internalUpdate(RoverPosition newValue) { if (!newValue.hasOrientation()) return; + // Angles are always between -180 and +180 + if (newValue.orientation.x.abs() > 180 || + newValue.orientation.y.abs() > 180 || + newValue.orientation.z.abs() > 180) { + return; + } _xCorrector.addValue(newValue.orientation.x); _yCorrector.addValue(newValue.orientation.y); _zCorrector.addValue(newValue.orientation.z); From 9671b69e44d0748e02f94886bcc9fe002e8a7228 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:11:01 -0500 Subject: [PATCH 058/110] Update coordinate converter --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 3ea0d73..6d60ec4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: a_star: ^3.0.0 meta: ^1.11.0 collection: ^1.19.1 - coordinate_converter: ^1.1.1 + coordinate_converter: ^1.1.2 dev_dependencies: test: ^1.21.0 From cad45fabd1845fc2744d98e0c0c318fdd4e12727 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:22:29 -0500 Subject: [PATCH 059/110] Keep obstacles that intersect with path --- lib/src/orchestrator/rover_orchestrator.dart | 23 +++++++++++++++---- .../pathfinding/pathfinding_interface.dart | 12 +++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 15f146d..e364272 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -37,6 +37,21 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { @override Message getMessage() => statusMessage; + bool findAndLockObstacles() { + if (!collection.detector.findObstacles()) { + return false; + } + + if (currentPath == null) return true; + + currentPath! + .map((state) => state.position) + .where((position) => collection.pathfinder.isObstacle(position)) + .forEach(collection.pathfinder.lockObstacle); + + return true; + } + Future calculateAndFollowPath( GpsCoordinates goal, { bool abortOnError = true, @@ -78,7 +93,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { final distanceError = collection.gps.coordinates.distanceTo(state.startPostition); if (distanceError >= Constants.replanErrorMeters) { collection.logger.info("Replanning Path", body: "Rover is $distanceError meters off the path"); - collection.detector.findObstacles(); + findAndLockObstacles(); break; } // Re-align to desired start orientation if angle is too far @@ -105,7 +120,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } // If there was an error (usually a timeout) while driving, replan path if (!await collection.drive.driveState(state)) { - collection.detector.findObstacles(); + findAndLockObstacles(); break; } if (currentCommand == null || currentPath == null) { @@ -115,10 +130,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { traversed.add(state.position); // if (state.direction != DriveDirection.forward) continue; if (++count >= 5) { - collection.detector.findObstacles(); + findAndLockObstacles(); break; } - final foundObstacle = collection.detector.findObstacles(); + final foundObstacle = findAndLockObstacles(); if (foundObstacle) { collection.logger.debug("Found an obstacle. Recalculating path..."); break; // calculate a new path diff --git a/lib/src/pathfinding/pathfinding_interface.dart b/lib/src/pathfinding/pathfinding_interface.dart index 25bce8e..202fd4d 100644 --- a/lib/src/pathfinding/pathfinding_interface.dart +++ b/lib/src/pathfinding/pathfinding_interface.dart @@ -7,8 +7,18 @@ abstract class PathfindingInterface extends Service { List? getPath(GpsCoordinates destination, {bool verbose = false}); Set obstacles = {}; + final Set _lockedObstacles = {}; + void recordObstacle(GpsCoordinates coordinates) => obstacles.add(coordinates); - bool isObstacle(GpsCoordinates coordinates) => obstacles.any((obstacle) => obstacle.isNear(coordinates)); + + void lockObstacle(GpsCoordinates coordinates) { + _lockedObstacles.add(coordinates); + obstacles.remove(coordinates); + } + + bool isObstacle(GpsCoordinates coordinates) => + obstacles.any((obstacle) => obstacle.isNear(coordinates)) || + _lockedObstacles.any((obstacle) => obstacle.isNear(coordinates)); @override Future dispose() async { From b14559458666f3402ff59cded08412772c941b20 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:58:00 -0500 Subject: [PATCH 060/110] Improve waitUntil() safety - Stop if near obstacle - Stop if the current command is null Should allow the program to be aborted without exiting --- lib/src/drive/sensor_drive.dart | 62 +++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index b1f9d3e..3e6259f 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -25,9 +25,31 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { /// Will periodically check for a condition to become true. This can be /// thought of as a "wait until", where the rover will periodically check - /// if it has reached its desired position or orientation - Future waitFor(bool Function() predicate) async { - while (!predicate()) { + /// if it has reached its desired position or orientation. + /// + /// The [predicate] method is intended to manuever the rover until its condition + /// becomes true. To prevent infinite driving, this will return false if either + /// the command is canceled, or [stopNearObstacle] is true and the rover becomes + /// too close to an obstacle. + /// + /// Returns whether or not the feedback loop reached its desired state. + Future runFeedback( + bool Function() predicate, { + bool stopNearObstacle = false, + }) async { + while (true) { + if (collection.orchestrator.currentCommand == null) { + return false; + } + if (stopNearObstacle && + collection.pathfinder.isObstacle(collection.gps.coordinates)) { + return false; + } + + if (predicate()) { + return true; + } + await Future.delayed(predicateDelay); } } @@ -42,32 +64,36 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { Future driveForward(GpsCoordinates position) async { collection.logger.info("Driving forward one meter"); setThrottle(config.forwardThrottle); - var timedOut = false; - await waitFor(() { - if (timedOut) return true; + var succeeded = true; + succeeded = await runFeedback(() { + if (!succeeded) return true; moveForward(); - return collection.gps.isNear(position, Constants.intermediateStepTolerance); - }).timeout( + return collection.gps.isNear( + position, + Constants.intermediateStepTolerance, + ); + // ignore: require_trailing_commas + }, stopNearObstacle: true).timeout( Constants.driveGPSTimeout, onTimeout: () { collection.logger.warning( "GPS Drive timed out", body: "Failed to reach ${position.prettyPrint()} after ${Constants.driveGPSTimeout}", ); - timedOut = true; + return false; }, ); await stop(); - return !timedOut; + return succeeded; } @override Future faceOrientation(Orientation orientation) async { collection.logger.info("Turning to face $orientation..."); setThrottle(config.turnThrottle); - await waitFor(() => _tryToFace(orientation)); + final result = await runFeedback(() => _tryToFace(orientation)); await stop(); - return true; + return result; } bool _tryToFace(Orientation orientation) { @@ -103,15 +129,21 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { }) async { setThrottle(config.turnThrottle); var foundAruco = true; - await waitFor(() { + foundAruco = await runFeedback(() { if (!foundAruco) { return true; } spinLeft(); - return collection.video.getArucoDetection(arucoId, desiredCamera: desiredCamera) != null; + return collection.video.getArucoDetection( + arucoId, + desiredCamera: desiredCamera, + ) != null; }).timeout( Constants.arucoSearchTimeout, - onTimeout: () => foundAruco = false, + onTimeout: () { + foundAruco = false; + return false; + }, ); await stop(); return foundAruco; From 2546368f4824a4647dff1d58e72b0c6139db7e94 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:27:38 -0500 Subject: [PATCH 061/110] Send normal and locked obstacles --- lib/src/orchestrator/rover_orchestrator.dart | 5 ++++- lib/src/pathfinding/pathfinding_interface.dart | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index e364272..5ce6456 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -24,7 +24,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { AutonomyData get statusMessage => AutonomyData( destination: currentCommand?.destination, state: currentState, - obstacles: collection.pathfinder.obstacles, + obstacles: [ + ...collection.pathfinder.obstacles, + ...collection.pathfinder.lockedObstacles, + ], path: [ for (final transition in currentPath ?? []) transition.position, diff --git a/lib/src/pathfinding/pathfinding_interface.dart b/lib/src/pathfinding/pathfinding_interface.dart index 202fd4d..42671a2 100644 --- a/lib/src/pathfinding/pathfinding_interface.dart +++ b/lib/src/pathfinding/pathfinding_interface.dart @@ -9,6 +9,8 @@ abstract class PathfindingInterface extends Service { Set obstacles = {}; final Set _lockedObstacles = {}; + Set get lockedObstacles => _lockedObstacles; + void recordObstacle(GpsCoordinates coordinates) => obstacles.add(coordinates); void lockObstacle(GpsCoordinates coordinates) { From d071f1f183dfd1a605717aa03258dc49a6d88dd6 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 23 Feb 2025 00:50:57 -0500 Subject: [PATCH 062/110] Cancel command when aruco task fails --- lib/src/orchestrator/rover_orchestrator.dart | 194 ++++++++++--------- lib/src/utils/gps_utils.dart | 10 +- 2 files changed, 108 insertions(+), 96 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 5ce6456..e546e59 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -104,7 +104,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { Orientation targetOrientation; // if it has RTK, point towards the next coordinate if (collection.gps.coordinates.hasRTK) { - final difference = state.position.asUtmCoordinates - collection.gps.coordinates.asUtmCoordinates; + final difference = state.position.toUTM() - collection.gps.coordinates.toUTM(); final angle = atan2(difference.y, difference.x) * 180 / pi; @@ -194,108 +194,120 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { desiredCamera: Constants.arucoDetectionCamera, ); - if (didSeeAruco && detectedAruco != null) { - collection.logger.info("Found aruco"); - currentState = AutonomyState.APPROACHING; - final arucoOrientation = Orientation(z: collection.imu.heading - detectedAruco.yaw); - await collection.drive.faceOrientation(arucoOrientation); - detectedAruco = await collection.video.waitForAruco( - command.arucoId, - desiredCamera: Constants.arucoDetectionCamera, - timeout: const Duration(seconds: 3), - ); + if (!didSeeAruco || detectedAruco == null) { + collection.logger.error("Could not find desired Aruco tag"); + currentState = AutonomyState.NO_SOLUTION; + currentCommand = null; + return; + } - if (detectedAruco == null || !detectedAruco.hasBestPnpResult()) { - // TODO: handle this condition properly - collection.logger.error("Could not find desired Aruco tag"); - return; - } + collection.logger.info("Found aruco"); + currentState = AutonomyState.APPROACHING; + final arucoOrientation = Orientation( + z: collection.imu.heading - detectedAruco.yaw, + ); + await collection.drive.faceOrientation(arucoOrientation); + detectedAruco = await collection.video.waitForAruco( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + timeout: const Duration(seconds: 3), + ); - collection.logger.debug( - "Planning path to Aruco ID ${command.arucoId}", - body: "Detection: ${detectedAruco.toProto3Json()}", - ); + if (detectedAruco == null || !detectedAruco.hasBestPnpResult()) { + // TODO: handle this condition properly + collection.logger.error("Could not find desired Aruco tag after rotating towards it"); + currentState = AutonomyState.NO_SOLUTION; + currentCommand = null; + return; + } - // In theory we could just find the relative position with the translation x and z, - // however if the tag's rotation relative to itself is off (which can be common - // when facing it head on), then it will be extremely innacurate. Since the SolvePnP's - // distance is always extremely accurate, it is more reliable to use the distance - // hypotenuse to the camera combined with trig of the tag's angle relative to the camera. - final cameraToTag = detectedAruco.bestPnpResult.cameraToTarget; - final distanceToTag = sqrt( - pow(cameraToTag.translation.z, 2) + pow(cameraToTag.translation.x, 2), - ) - 1; // don't drive *into* the tag - - if (distanceToTag < 1) { - // well that was easy - collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); - currentState = AutonomyState.AT_DESTINATION; - return; - } + collection.logger.debug( + "Planning path to Aruco ID ${command.arucoId}", + body: "Detection: ${detectedAruco.toProto3Json()}", + ); - final relativeX = -distanceToTag * sin((collection.imu.heading - detectedAruco.yaw) * pi / 180); - final relativeY = distanceToTag * cos((collection.imu.heading - detectedAruco.yaw) * pi / 180); - - final destinationCoordinates = - (collection.gps.coordinates.asUtmCoordinates + - UTMCoordinates(y: relativeY, x: relativeX, zoneNumber: 1)) - .asGpsCoordinates; - - if (!await calculateAndFollowPath( - destinationCoordinates, - abortOnError: false, - alternateEndCondition: () { - detectedAruco = collection.video.getArucoDetection( - command.arucoId, - desiredCamera: Constants.arucoDetectionCamera, - ); - if (detectedAruco == null) { - return false; - } - final cameraToTag = detectedAruco!.bestPnpResult.cameraToTarget; - final distanceToTag = sqrt( - pow(cameraToTag.translation.z, 2) + - pow(cameraToTag.translation.x, 2), - ); - return distanceToTag < 1; - }, - )) { - collection.logger.error("Could not spin towards ArUco tag"); - currentCommand = null; - return; - } - collection.logger.info("Arrived at estimated Aruco position"); - detectedAruco = collection.video.getArucoDetection( - command.arucoId, - desiredCamera: Constants.arucoDetectionCamera, - ); - if (detectedAruco == null) { - collection.logger.info("Re-spinning to find Aruco"); - await collection.drive.spinForAruco( + // In theory we could just find the relative position with the translation x and z, + // however if the tag's rotation relative to itself is off (which can be common + // when facing it head on), then it will be extremely innacurate. Since the SolvePnP's + // distance is always extremely accurate, it is more reliable to use the distance + // hypotenuse to the camera combined with trig of the tag's angle relative to the camera. + final cameraToTag = detectedAruco.bestPnpResult.cameraToTarget; + final distanceToTag = + sqrt( + pow(cameraToTag.translation.z, 2) + pow(cameraToTag.translation.x, 2), + ) - 1; // don't drive *into* the tag + + if (distanceToTag < 1) { + // well that was easy + collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); + currentState = AutonomyState.AT_DESTINATION; + currentCommand = null; + return; + } + + final relativeX = -distanceToTag * sin((collection.imu.heading - detectedAruco.yaw) * pi / 180); + final relativeY = distanceToTag * cos((collection.imu.heading - detectedAruco.yaw) * pi / 180); + + final destinationCoordinates = + (collection.gps.coordinates.toUTM() + + UTMCoordinates(y: relativeY, x: relativeX, zoneNumber: 1)) + .toGps(); + + if (!await calculateAndFollowPath( + destinationCoordinates, + abortOnError: false, + alternateEndCondition: () { + detectedAruco = collection.video.getArucoDetection( command.arucoId, desiredCamera: Constants.arucoDetectionCamera, ); - } - - detectedAruco = collection.video.getArucoDetection( + if (detectedAruco == null) { + return false; + } + final cameraToTag = detectedAruco!.bestPnpResult.cameraToTarget; + final distanceToTag = sqrt( + pow(cameraToTag.translation.z, 2) + pow(cameraToTag.translation.x, 2), + ); + return distanceToTag < 1; + }, + )) { + collection.logger.error("Could not spin towards ArUco tag"); + currentState = AutonomyState.NO_SOLUTION; + currentCommand = null; + return; + } + collection.logger.info("Arrived at estimated Aruco position"); + detectedAruco = collection.video.getArucoDetection( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ); + if (detectedAruco == null) { + collection.logger.info("Re-spinning to find Aruco"); + await collection.drive.spinForAruco( command.arucoId, desiredCamera: Constants.arucoDetectionCamera, ); - if (detectedAruco != null) { - collection.logger.info("Rotating towards Aruco"); - await collection.drive.faceOrientation( - Orientation( - z: collection.imu.heading - detectedAruco!.yaw, - ), - ); - } else { - collection.logger.warning("Could not find Aruco after following path"); - } + } - collection.logger.info("Successfully reached within ${Constants.maxErrorMeters} meters of the Aruco tag"); - collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); - currentState = AutonomyState.AT_DESTINATION; + detectedAruco = collection.video.getArucoDetection( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ); + if (detectedAruco != null) { + collection.logger.info("Rotating towards Aruco"); + await collection.drive.faceOrientation( + Orientation(z: collection.imu.heading - detectedAruco!.yaw), + ); + } else { + collection.logger.warning("Could not find Aruco after following path"); } + + collection.logger.info( + "Successfully reached within ${Constants.maxErrorMeters} meters of the Aruco tag", + ); + collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); + currentState = AutonomyState.AT_DESTINATION; + currentCommand = null; } diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 9c85d83..bb7a2d4 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -56,7 +56,7 @@ extension GpsUtils on GpsCoordinates { double heuristicDistance(GpsCoordinates other) { var distance = 0.0; - final delta = asUtmCoordinates - other.asUtmCoordinates; + final delta = toUTM() - other.toUTM(); final deltaLat = delta.y.abs(); final deltaLong = delta.x.abs(); @@ -75,12 +75,12 @@ extension GpsUtils on GpsCoordinates { } double manhattanDistance(GpsCoordinates other) { - final delta = asUtmCoordinates - other.asUtmCoordinates; + final delta = toUTM() - other.toUTM(); return delta.x.abs() + delta.y.abs(); } double octileDistance(GpsCoordinates other) { - final delta = asUtmCoordinates - other.asUtmCoordinates; + final delta = toUTM() - other.toUTM(); final dx = delta.x.abs() ~/ Constants.moveLengthMeters; final dy = delta.y.abs() ~/ Constants.moveLengthMeters; @@ -105,7 +105,7 @@ extension GpsUtils on GpsCoordinates { String prettyPrint() => toProto3Json().toString(); - GpsCoordinates goForward(CardinalDirection orientation) => (asUtmCoordinates + + GpsCoordinates goForward(CardinalDirection orientation) => (toUTM() + switch (orientation) { CardinalDirection.north => GpsUtils.northMeters, CardinalDirection.south => GpsUtils.southMeters, @@ -115,5 +115,5 @@ extension GpsUtils on GpsCoordinates { CardinalDirection.northWest => GpsUtils.northWestMeters, CardinalDirection.southEast => GpsUtils.southEastMeters, CardinalDirection.southWest => GpsUtils.southWestMeters, - }).asGpsCoordinates; + }).toGps(); } From 7d1774123e6878780dbac0a5cb7dd13b7beda4d1 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 23 Feb 2025 00:57:08 -0500 Subject: [PATCH 063/110] Only remove obstacles that are in lidar view range --- lib/src/detector/rover_detector.dart | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/src/detector/rover_detector.dart b/lib/src/detector/rover_detector.dart index da60e26..a839e96 100644 --- a/lib/src/detector/rover_detector.dart +++ b/lib/src/detector/rover_detector.dart @@ -9,8 +9,8 @@ class RoverDetector extends DetectorInterface { LidarPointCloud cloudCache = LidarPointCloud(); - List queuedObstacles = []; - List previousObstacles = []; + Set queuedObstacles = {}; + Set temporaryObstacles = {}; RoverDetector({required super.collection}); @@ -74,8 +74,8 @@ class RoverDetector extends DetectorInterface { ); queuedObstacles.add( - (collection.gps.coordinates.asUtmCoordinates + roverToPoint) - .asGpsCoordinates, + (collection.gps.coordinates.toUTM() + roverToPoint) + .toGps(), ); } @@ -90,14 +90,23 @@ class RoverDetector extends DetectorInterface { bool findObstacles() { if (queuedObstacles.isEmpty) return false; - collection.pathfinder.obstacles.removeAll(previousObstacles); - previousObstacles.clear(); + final roverUtm = collection.gps.coordinates.toUTM(); + + final toRemove = temporaryObstacles.where((coordinates) { + final delta = coordinates.toUTM() - roverUtm; + final roverToPoint = (atan2(delta.y, delta.x) - pi / 2) * 180 / pi; + final relativeAngle = (collection.imu.heading + roverToPoint).clampHalfAngle(); + + return relativeAngle > -135 && relativeAngle < 135; + }); + collection.pathfinder.obstacles.removeAll(toRemove); + temporaryObstacles.removeAll(toRemove); for (final obstacle in queuedObstacles) { collection.pathfinder.recordObstacle(obstacle); } - previousObstacles.addAll(queuedObstacles); + temporaryObstacles.addAll(queuedObstacles); queuedObstacles.clear(); From 576f4a1bd5bb5fb6c303f952eb7ff0e42d6ece55 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 20 Mar 2025 20:23:54 -0400 Subject: [PATCH 064/110] Initial behavior tree implementation for A -> B pathfinding --- lib/src/drive/drive_interface.dart | 30 ++ lib/src/drive/rover_drive.dart | 42 +++ lib/src/drive/sensor_drive.dart | 56 +++ lib/src/drive/sim_drive.dart | 33 ++ lib/src/drive/timed_drive.dart | 86 +++++ .../orchestrator/orchestrator_interface.dart | 42 ++- lib/src/orchestrator/rover_orchestrator.dart | 354 ++++++++++++++++-- lib/src/utils/behavior_util.dart | 153 ++++++++ pubspec.yaml | 1 + 9 files changed, 762 insertions(+), 35 deletions(-) create mode 100644 lib/src/utils/behavior_util.dart diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index 267ad07..c315e26 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -1,4 +1,5 @@ import "package:autonomy/interfaces.dart"; +import "package:behavior_tree/behavior_tree.dart"; import "drive_config.dart"; @@ -37,6 +38,35 @@ abstract class DriveInterface extends Service { /// Constructor for Drive Interface DriveInterface({required this.collection, this.config = roverConfig}); + BaseNode driveStateNode(AutonomyAStarState state) { + if (state.instruction == DriveDirection.stop) { + return Task(() { + stop(); + return NodeStatus.success; + }); + } else if (state.instruction == DriveDirection.forward) { + return driveForwardNode(state.position); + } else { + return turnStateNode(state); + } + } + + BaseNode resolveOrientationNode() => + faceDirectionNode(collection.imu.nearest); + + BaseNode driveForwardNode(GpsCoordinates coordinates); + + BaseNode faceDirectionNode(CardinalDirection direction) => + faceOrientationNode(direction.orientation); + + BaseNode faceOrientationNode(Orientation orientation); + + BaseNode turnStateNode(AutonomyAStarState state) => + faceOrientationNode(state.orientation.orientation); + + BaseNode spinForArucoNode(int arucoId, {CameraName? desiredCamera}) => + Condition(() => false); + /// Stop the rover Future stop(); diff --git a/lib/src/drive/rover_drive.dart b/lib/src/drive/rover_drive.dart index 70c9cec..624e54d 100644 --- a/lib/src/drive/rover_drive.dart +++ b/lib/src/drive/rover_drive.dart @@ -1,5 +1,6 @@ import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; +import "package:behavior_tree/behavior_tree.dart"; import "sensor_drive.dart"; import "timed_drive.dart"; @@ -105,4 +106,45 @@ class RoverDrive extends DriveInterface { return result; } } + + @override + BaseNode driveForwardNode(GpsCoordinates coordinates) { + if (useGps) { + return sensorDrive.driveForwardNode(coordinates); + } else { + return Sequence( + children: [ + timedDrive.driveForwardNode(coordinates), + simDrive.driveForwardNode(coordinates), + ], + ); + } + } + + @override + BaseNode turnStateNode(AutonomyAStarState state) { + if (useImu) { + return sensorDrive.turnStateNode(state); + } else { + return Sequence( + children: [ + timedDrive.turnStateNode(state), + simDrive.turnStateNode(state), + ], + ); + } + } + + @override + BaseNode faceOrientationNode(Orientation orientation) { + if (useImu) { + return sensorDrive.faceOrientationNode(orientation); + } else { + return simDrive.faceOrientationNode(orientation); + } + } + + @override + BaseNode spinForArucoNode(int arucoId, {CameraName? desiredCamera}) => + sensorDrive.spinForArucoNode(arucoId, desiredCamera: desiredCamera); } diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 3e6259f..f267a4d 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -1,6 +1,8 @@ import "package:autonomy/autonomy.dart"; import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; +import "package:autonomy/src/utils/behavior_util.dart"; +import "package:behavior_tree/behavior_tree.dart"; import "drive_commands.dart"; @@ -17,6 +19,60 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { /// Default constructor for SensorDrive SensorDrive({required super.collection, super.config}); + @override + BaseNode driveForwardNode(GpsCoordinates coordinates) => Selector( + children: [ + Condition( + () => collection.gps.isNear( + coordinates, + Constants.intermediateStepTolerance, + ), + ), + Task(() { + if (collection.pathfinder.isObstacle(collection.gps.coordinates)) { + return NodeStatus.failure; + } + setThrottle(config.forwardThrottle); + moveForward(); + return NodeStatus.running; + }), + ], + ).withTimeout(Constants.driveGPSTimeout); + + @override + BaseNode faceOrientationNode(Orientation orientation) => Selector( + children: [ + Condition( + () => collection.imu.isNear(orientation, Constants.turnEpsilon), + ), + Task(() { + setThrottle(config.turnThrottle); + _tryToFace(orientation); + return NodeStatus.running; + }), + ], + ); + + @override + BaseNode spinForArucoNode(int arucoId, {CameraName? desiredCamera}) => + Selector( + children: [ + Condition( + () => + collection.video.getArucoDetection( + arucoId, + desiredCamera: desiredCamera, + ) != + null, + ), + Task(() { + setThrottle(config.turnThrottle); + spinLeft(); + return NodeStatus.running; + }), + ], + ).withTimeout(Constants.arucoSearchTimeout); + @override Future stop() async { stopMotors(); diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index 67c2450..205bec5 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -1,4 +1,6 @@ import "package:autonomy/interfaces.dart"; +import "package:autonomy/src/utils/behavior_util.dart"; +import "package:behavior_tree/behavior_tree.dart"; /// An implementation of [DriveInterface] that will not move the rover, /// and only update its sensor readings based on the desired values @@ -16,6 +18,37 @@ class DriveSimulator extends DriveInterface { /// Constructor for DriveSimulator, initializing the default fields, and whether or not it should delay DriveSimulator({required super.collection, this.shouldDelay = false, super.config}); + BaseNode _delayAndExecute({ + required Duration delay, + required BaseNode child, + }) => Sequence(children: [DelayedNode(delay), child]); + + @override + BaseNode driveForwardNode(GpsCoordinates coordinates) { + final updateGps = Task(() { + collection.gps.update(coordinates); + return NodeStatus.success; + }); + return ConditionalNode( + condition: () => shouldDelay, + onTrue: _delayAndExecute(delay: delay, child: updateGps), + onFalse: updateGps, + ); + } + + @override + BaseNode faceOrientationNode(Orientation orientation) { + final updateImu = Task(() { + collection.imu.update(orientation); + return NodeStatus.success; + }); + return ConditionalNode( + condition: () => shouldDelay, + onTrue: _delayAndExecute(delay: delay, child: updateImu), + onFalse: updateImu, + ); + } + @override Future init() async => true; diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index b71812c..5b7aaa4 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -2,9 +2,37 @@ import "dart:math"; import "package:autonomy/interfaces.dart"; import "package:autonomy/src/drive/drive_config.dart"; +import "package:behavior_tree/behavior_tree.dart"; import "drive_commands.dart"; +class _TimedOperation extends BaseNode { + DateTime? _start; + + final Duration time; + final void Function() operation; + + _TimedOperation({required this.time, required this.operation}); + + @override + void tick() { + _start ??= DateTime.now(); + status = NodeStatus.running; + + operation(); + + if (DateTime.now().difference(_start!) >= time) { + status = NodeStatus.success; + } + } + + @override + void reset() { + _start = null; + super.reset(); + } +} + /// An implementation of [DriveInterface] that drives for a specified amount of time without using sensors /// /// The time to drive/turn for is defined by [DriveConfig.oneMeterDelay] and [DriveConfig.turnDelay] @@ -13,6 +41,64 @@ import "drive_commands.dart"; class TimedDrive extends DriveInterface with RoverDriveCommands { TimedDrive({required super.collection, super.config}); + @override + BaseNode faceOrientationNode(Orientation orientation) { + throw UnsupportedError( + "Cannot face any arbitrary direction using TimedDrive", + ); + } + + @override + BaseNode driveForwardNode(GpsCoordinates coordinates) => _TimedOperation( + time: + config.oneMeterDelay * + (collection.imu.nearest.isPerpendicular ? 1 : sqrt2), + operation: () { + setThrottle(config.forwardThrottle); + moveForward(); + }, + ); + + @override + BaseNode turnStateNode(AutonomyAStarState state) => switch (state + .instruction) { + DriveDirection.forward => throw UnimplementedError(), + + DriveDirection.left => _TimedOperation( + time: config.turnDelay, + operation: () { + setThrottle(config.turnThrottle); + spinLeft(); + }, + ), + + DriveDirection.right => _TimedOperation( + time: config.turnDelay, + operation: () { + setThrottle(config.turnThrottle); + spinRight(); + }, + ), + + DriveDirection.quarterLeft => _TimedOperation( + time: config.turnDelay * 0.5, + operation: () { + setThrottle(config.turnThrottle); + spinLeft(); + }, + ), + + DriveDirection.quarterRight => _TimedOperation( + time: config.turnDelay * 0.5, + operation: () { + setThrottle(config.turnThrottle); + spinRight(); + }, + ), + + DriveDirection.stop => throw UnimplementedError(), + }; + @override Future init() async => true; diff --git a/lib/src/orchestrator/orchestrator_interface.dart b/lib/src/orchestrator/orchestrator_interface.dart index ce8fcfe..676d570 100644 --- a/lib/src/orchestrator/orchestrator_interface.dart +++ b/lib/src/orchestrator/orchestrator_interface.dart @@ -1,6 +1,7 @@ -import "dart:io"; +import "dart:async"; import "package:autonomy/interfaces.dart"; +import "package:behavior_tree/behavior_tree.dart"; import "package:meta/meta.dart"; abstract class OrchestratorInterface extends Service { @@ -9,11 +10,18 @@ abstract class OrchestratorInterface extends Service { AutonomyCommand? currentCommand; AutonomyState currentState = AutonomyState.AUTONOMY_STATE_UNDEFINED; + Timer? behaviorTreeTimer; + + BaseNode behaviorRoot = Condition(() => true); + Future onCommand(AutonomyCommand command) async { collection.server.sendMessage(command); if (command.abort) return abort(); if (currentCommand != null) { - collection.logger.error("Already executing a command", body: "Abort first if you want to switch tasks"); + collection.logger.error( + "Already executing a command", + body: "Abort first if you want to switch tasks", + ); return; } @@ -24,13 +32,18 @@ abstract class OrchestratorInterface extends Service { currentState = AutonomyState.NO_SOLUTION; return; } - await collection.drive.resolveOrientation(); + + // await collection.drive.resolveOrientation(); currentCommand = command; switch (command.task) { - case AutonomyTask.BETWEEN_GATES: break; // TODO - case AutonomyTask.AUTONOMY_TASK_UNDEFINED: break; - case AutonomyTask.GPS_ONLY: await handleGpsTask(command); - case AutonomyTask.VISUAL_MARKER: await handleArucoTask(command); + case AutonomyTask.BETWEEN_GATES: + break; // TODO + case AutonomyTask.AUTONOMY_TASK_UNDEFINED: + break; + case AutonomyTask.GPS_ONLY: + handleGpsTask(command); + case AutonomyTask.VISUAL_MARKER: + handleArucoTask(command); } } @@ -48,15 +61,18 @@ abstract class OrchestratorInterface extends Service { Future abort() async { currentCommand = null; collection.logger.warning("Aborting task!"); + behaviorTreeTimer?.cancel(); + behaviorRoot.reset(); currentState = AutonomyState.ABORTING; await collection.drive.stop(); - await collection.dispose(); - exit(1); + // await collection.dispose(); + // await collection.init(); + // exit(1); } - Future handleGpsTask(AutonomyCommand command); - Future handleArucoTask(AutonomyCommand command); - Future handleHammerTask(AutonomyCommand command); - Future handleBottleTask(AutonomyCommand command); + void handleGpsTask(AutonomyCommand command); + void handleArucoTask(AutonomyCommand command); + void handleHammerTask(AutonomyCommand command); + void handleBottleTask(AutonomyCommand command); AutonomyData get statusMessage; } diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index e546e59..66e3ba4 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -2,6 +2,8 @@ import "dart:math"; import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; +import "package:autonomy/src/utils/behavior_util.dart"; +import "package:behavior_tree/behavior_tree.dart"; import "dart:async"; import "package:coordinate_converter/coordinate_converter.dart"; @@ -9,6 +11,12 @@ import "package:coordinate_converter/coordinate_converter.dart"; class RoverOrchestrator extends OrchestratorInterface with ValueReporter { final List traversed = []; List? currentPath; + + bool replanPath = true; + int waypointIndex = 0; + bool hasCheckedWaypointOrientation = false; + bool hasCheckedWaypointError = false; + RoverOrchestrator({required super.collection}); @override @@ -20,6 +28,12 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { await super.dispose(); } + @override + Future abort() async { + currentPath = null; + return super.abort(); + } + @override AutonomyData get statusMessage => AutonomyData( destination: currentCommand?.destination, @@ -55,6 +69,207 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { return true; } + BaseNode replanOnCondition(bool Function() condition) => Task(() { + if (condition()) { + replanPath = true; + return NodeStatus.failure; + } + return NodeStatus.success; + }); + + BaseNode planPath(GpsCoordinates destination) => Sequence( + children: [ + Condition( + () => + replanPath && + currentCommand != null && + collection.gps.hasValue && + collection.imu.hasValue, + ), + Task(() { + collection.logger.debug("Finding any new obstacles"); + findAndLockObstacles(); + return NodeStatus.success; + }), + Task(() { + collection.logger.debug("Finding a path"); + currentState = AutonomyState.PATHING; + replanPath = false; + return NodeStatus.success; + }), + Condition(() { + if (currentCommand == null) { + return false; + } + final current = collection.gps.coordinates; + currentPath = collection.pathfinder.getPath( + currentCommand!.destination, + ); + if (currentPath == null) { + collection.logger.error( + "Could not find a path", + body: + "No path found from ${current.prettyPrint()} to ${destination.prettyPrint()}", + ); + } else { + waypointIndex = 0; + hasCheckedWaypointError = false; + hasCheckedWaypointOrientation = false; + collection.logger.debug( + "Found a path from ${current.prettyPrint()} to ${destination.prettyPrint()}: ${currentPath!.length} steps", + ); + collection.logger.debug("Here is a summary of the path"); + for (final step in currentPath!) { + collection.logger.debug(step.toString()); + } + } + return currentPath != null; + }), + ], + ); + + BaseNode followPath(GpsCoordinates destination) { + late AutonomyAStarState currentWaypoint; + // Orientation the rover should be facing before driving forward + var targetOrientation = collection.imu.nearest.orientation; + + return Sequence( + children: [ + Task(() { + if (currentPath == null) { + return NodeStatus.failure; + } + if (waypointIndex >= currentPath!.length) { + return NodeStatus.failure; + } + currentWaypoint = currentPath![waypointIndex]; + currentState = AutonomyState.DRIVING; + + if (!hasCheckedWaypointOrientation) { + // if it has RTK, point towards the next coordinate + if (collection.gps.coordinates.hasRTK) { + final difference = + currentWaypoint.position.toUTM() - + collection.gps.coordinates.toUTM(); + + final angle = atan2(difference.y, difference.x) * 180 / pi; + + targetOrientation = Orientation(z: angle); + } else { + targetOrientation = currentWaypoint.orientation.orientation; + } + } + + return NodeStatus.success; + }), + Selector( + children: [ + Condition(() { + if (!hasCheckedWaypointOrientation) { + hasCheckedWaypointOrientation = true; + return currentWaypoint.instruction == DriveDirection.forward && + (collection.imu.heading - targetOrientation.z) + .clampHalfAngle() + .abs() >= + Constants.driveRealignmentEpsilon; + } + return false; + }).inverted, + SuppliedNode( + key: () => targetOrientation, + () => collection.drive.faceOrientationNode(targetOrientation), + ), + ], + ), + replanOnCondition(() { + if (!hasCheckedWaypointError) { + hasCheckedWaypointError = true; + return collection.gps.coordinates.distanceTo( + currentWaypoint.startPostition, + ) >= + Constants.replanErrorMeters; + } + return false; + }), + // ConditionalNode( + // condition: () { + // if (currentWaypoint.instruction != DriveDirection.forward) { + // return false; + // } + // return (collection.imu.heading - targetOrientation.z) + // .clampHalfAngle() > + // Constants.driveRealignmentEpsilon; + // }, + // onTrue: SuppliedNode( + // key: () => targetOrientation, + // () => collection.drive.faceOrientationNode(targetOrientation), + // ), + // ), + SuppliedNode( + key: () => waypointIndex, + () => collection.drive.driveStateNode(currentWaypoint), + ), + Task(() { + traversed.add(currentWaypoint.position); + waypointIndex++; + hasCheckedWaypointOrientation = false; + hasCheckedWaypointError = false; + return NodeStatus.success; + }), + replanOnCondition(() => findAndLockObstacles() || waypointIndex >= 5), + Condition(() => collection.gps.isNear(destination, Constants.maxErrorMeters)), + ], + ); + } + + BaseNode pathToDestination(GpsCoordinates destination) { + var resolvedOrientation = false; + return Sequence( + children: [ + Selector( + children: [ + Condition(() { + if (!resolvedOrientation) { + resolvedOrientation = true; + return true; + } + return false; + }).inverted, + SuppliedNode(() => collection.drive.resolveOrientationNode()), + ], + ), + Selector( + children: [ + Condition( + () => + collection.gps.isNear(destination, Constants.maxErrorMeters), + ), + planPath(destination), + followPath(destination), + // Only runs if plan path failed, and follow path failed, indicating 2 scenarios: + // 1. Couldn't find a path at all + // 2. Couldn't follow a specific step of the path + Task(() { + // Failed to find a path (Scenario 1) + if (!replanPath && currentPath == null) { + return NodeStatus.failure; + } else { + // Either a timeout or new obstacle was found, replan path and continue (Scenario 2) + return NodeStatus.running; + } + }), + ], + ), + Task(() { + if (collection.gps.isNear(destination, Constants.maxErrorMeters)) { + return NodeStatus.success; + } + return NodeStatus.running; + }), + ], + ); + } + Future calculateAndFollowPath( GpsCoordinates goal, { bool abortOnError = true, @@ -147,33 +362,128 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } @override - Future handleGpsTask(AutonomyCommand command) async { + void handleGpsTask(AutonomyCommand command) { final destination = command.destination; collection.logger.info("Received GPS Task", body: "Go to ${destination.prettyPrint()}"); collection.logger.debug("Currently at ${collection.gps.coordinates.prettyPrint()}"); traversed.clear(); collection.drive.setLedStrip(ProtoColor.RED); + waypointIndex = 0; + hasCheckedWaypointError = false; + hasCheckedWaypointOrientation = false; + replanPath = true; + behaviorRoot = pathToDestination(destination); + behaviorTreeTimer = Timer.periodic(const Duration(milliseconds: 10), ( + timer, + ) { + if (currentCommand == null) { + return; + } + behaviorRoot.tick(); + if (behaviorRoot.status == NodeStatus.failure) { + behaviorRoot.reset(); + currentState = AutonomyState.NO_SOLUTION; + currentCommand = null; + timer.cancel(); + } else if (behaviorRoot.status == NodeStatus.success) { + behaviorRoot.reset(); + timer.cancel(); + collection.logger.info("Task complete"); + currentState = AutonomyState.AT_DESTINATION; + collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); + currentCommand = null; + } + }); // detect obstacles before and after resolving orientation, as a "scan" - collection.detector.findObstacles(); - await collection.drive.resolveOrientation(); - collection.detector.findObstacles(); - - if (!await calculateAndFollowPath(command.destination)) { - return; - } + // collection.detector.findObstacles(); + // await collection.drive.resolveOrientation(); + // collection.detector.findObstacles(); - collection.logger.info("Task complete"); - collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); - currentState = AutonomyState.AT_DESTINATION; - currentCommand = null; + // if (!await calculateAndFollowPath(command.destination)) { + // return; + // } } @override - Future handleArucoTask(AutonomyCommand command) async { + void handleArucoTask(AutonomyCommand command) async { collection.drive.setLedStrip(ProtoColor.RED); // Go to GPS coordinates collection.logger.info("Got ArUco Task"); + + DetectedObject? detectedAruco; + + behaviorRoot = Sequence( + children: [ + // Go to initial coordinates given + Selector( + children: [ + Condition( + () => + command.destination != + GpsCoordinates(latitude: 0, longitude: 0), + ).inverted, + pathToDestination(command.destination), + // If failed to reach + Task(() { + collection.logger.error( + "Failed to follow path towards initial destination", + ); + currentState = AutonomyState.NO_SOLUTION; + currentCommand = null; + return NodeStatus.failure; + }), + ], + ), + Task(() { + currentState = AutonomyState.SEARCHING; + collection.logger.info("Searching for ArUco tag"); + return NodeStatus.success; + }), + // Try to spin and find a tag + Selector( + children: [ + collection.drive.spinForArucoNode( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ), + Task(() { + collection.logger.error("Could not find desired Aruco tag"); + currentState = AutonomyState.NO_SOLUTION; + currentCommand = null; + return NodeStatus.failure; + }), + ], + ), + Condition(() { + detectedAruco = collection.video.getArucoDetection( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ); + return detectedAruco != null; + }), + // Face towards aruco tag + SuppliedNode( + () => collection.drive.faceOrientationNode( + Orientation(z: collection.imu.heading - detectedAruco!.yaw), + ), + ), + Selector( + children: [ + Condition( + () => + collection.video.getArucoDetection( + command.arucoId, + desiredCamera: Constants.arucoDetectionCamera, + ) != + null, + ), + Task(() => NodeStatus.running), + ], + ).withTimeout(const Duration(seconds: 3)), + ], + ); + if (command.destination != GpsCoordinates(latitude: 0, longitude: 0)) { if (!await calculateAndFollowPath(command.destination, abortOnError: false)) { collection.logger.error("Failed to follow path towards initial destination"); @@ -189,7 +499,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { command.arucoId, desiredCamera: Constants.arucoDetectionCamera, ); - var detectedAruco = collection.video.getArucoDetection( + detectedAruco = collection.video.getArucoDetection( command.arucoId, desiredCamera: Constants.arucoDetectionCamera, ); @@ -204,7 +514,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.logger.info("Found aruco"); currentState = AutonomyState.APPROACHING; final arucoOrientation = Orientation( - z: collection.imu.heading - detectedAruco.yaw, + z: collection.imu.heading - detectedAruco!.yaw, ); await collection.drive.faceOrientation(arucoOrientation); detectedAruco = await collection.video.waitForAruco( @@ -213,7 +523,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { timeout: const Duration(seconds: 3), ); - if (detectedAruco == null || !detectedAruco.hasBestPnpResult()) { + if (detectedAruco == null || !detectedAruco!.hasBestPnpResult()) { // TODO: handle this condition properly collection.logger.error("Could not find desired Aruco tag after rotating towards it"); currentState = AutonomyState.NO_SOLUTION; @@ -223,7 +533,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.logger.debug( "Planning path to Aruco ID ${command.arucoId}", - body: "Detection: ${detectedAruco.toProto3Json()}", + body: "Detection: ${detectedAruco!.toProto3Json()}", ); // In theory we could just find the relative position with the translation x and z, @@ -231,7 +541,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { // when facing it head on), then it will be extremely innacurate. Since the SolvePnP's // distance is always extremely accurate, it is more reliable to use the distance // hypotenuse to the camera combined with trig of the tag's angle relative to the camera. - final cameraToTag = detectedAruco.bestPnpResult.cameraToTarget; + final cameraToTag = detectedAruco!.bestPnpResult.cameraToTarget; final distanceToTag = sqrt( pow(cameraToTag.translation.z, 2) + pow(cameraToTag.translation.x, 2), @@ -245,8 +555,8 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { return; } - final relativeX = -distanceToTag * sin((collection.imu.heading - detectedAruco.yaw) * pi / 180); - final relativeY = distanceToTag * cos((collection.imu.heading - detectedAruco.yaw) * pi / 180); + final relativeX = -distanceToTag * sin((collection.imu.heading - detectedAruco!.yaw) * pi / 180); + final relativeY = distanceToTag * cos((collection.imu.heading - detectedAruco!.yaw) * pi / 180); final destinationCoordinates = (collection.gps.coordinates.toUTM() + @@ -312,12 +622,12 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } @override - Future handleHammerTask(AutonomyCommand command) async { + void handleHammerTask(AutonomyCommand command) async { } @override - Future handleBottleTask(AutonomyCommand command) async { + void handleBottleTask(AutonomyCommand command) async { } } diff --git a/lib/src/utils/behavior_util.dart b/lib/src/utils/behavior_util.dart new file mode 100644 index 0000000..4db780f --- /dev/null +++ b/lib/src/utils/behavior_util.dart @@ -0,0 +1,153 @@ +import "package:behavior_tree/behavior_tree.dart"; + +class TimeLimit extends BaseNode { + DateTime? _start; + + final BaseNode child; + final Duration timeout; + final NodeStatus statusAfterTimeout; + + TimeLimit({ + required this.child, + required this.timeout, + this.statusAfterTimeout = NodeStatus.failure, + }); + + @override + void tick() { + _start ??= DateTime.now(); + + if (DateTime.now().difference(_start!) >= timeout) { + status = statusAfterTimeout; + return; + } + + child.tick(); + status = child.status; + } + + @override + void reset() { + _start = null; + child.reset(); + + super.reset(); + } +} + +class DelayedNode extends BaseNode { + DateTime? _start; + + final Duration delay; + + DelayedNode(this.delay); + + @override + void tick() { + _start ??= DateTime.now(); + + if (DateTime.now().difference(_start!) >= delay) { + status = NodeStatus.success; + } else { + status = NodeStatus.running; + } + } + + @override + void reset() { + _start = null; + super.reset(); + } +} + +class ConditionalNode extends BaseNode { + final bool Function() condition; + + final BaseNode? onTrue; + final BaseNode? onFalse; + + bool? _initialConditionState; + + ConditionalNode({required this.condition, this.onTrue, this.onFalse}) + : assert( + onTrue != null || onFalse != null, + "onTrue() or onFalse() must be non-null, if there is no intended child node, use a Condition() instead!", + ); + + @override + void tick() { + _initialConditionState ??= condition(); + + if (_initialConditionState!) { + if (onTrue != null) { + onTrue!.tick(); + status = onTrue!.status; + } else if (onFalse != null) { + status = NodeStatus.success; + } + } else { + if (onFalse != null) { + onFalse!.tick(); + status = onFalse!.status; + } else if (onTrue != null) { + status = NodeStatus.success; + } + } + } + + @override + void reset() { + _initialConditionState = null; + onTrue?.reset(); + onFalse?.reset(); + super.reset(); + } +} + +class SuppliedNode extends BaseNode { + BaseNode? _child; + + final BaseNode Function() supplier; + final Object? Function()? key; + + Object? _lastKey; + + SuppliedNode(this.supplier, {this.key}) : _lastKey = key?.call(); + + @override + void tick() { + if (_child == null) { + _child = supplier(); + } else if (key != null) { + final currentKey = key!.call(); + if (currentKey != _lastKey) { + _child?.reset(); + _child = supplier(); + } + _lastKey = currentKey; + } + + _child!.tick(); + status = _child!.status; + } + + @override + void reset() { + _child?.reset(); + _child = null; + super.reset(); + } +} + +extension BehaviorDecorators on BaseNode { + BaseNode get inverted => Inverter(this); + + BaseNode withTimeout( + Duration timeout, { + NodeStatus statusAfterTimeout = NodeStatus.failure, + }) => TimeLimit( + child: this, + timeout: timeout, + statusAfterTimeout: statusAfterTimeout, + ); +} diff --git a/pubspec.yaml b/pubspec.yaml index 6d60ec4..71c866d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: meta: ^1.11.0 collection: ^1.19.1 coordinate_converter: ^1.1.2 + behavior_tree: ^0.1.3+2 dev_dependencies: test: ^1.21.0 From 69ce71613843c1c23898720c0d17010c5c2868a3 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:33:24 -0400 Subject: [PATCH 065/110] Add more docs to behavior tree - Also fix issue with waypoint correction --- lib/src/orchestrator/rover_orchestrator.dart | 62 +++++++++++++++++--- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 66e3ba4..27f5450 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -15,6 +15,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { bool replanPath = true; int waypointIndex = 0; bool hasCheckedWaypointOrientation = false; + bool isCorrectingWaypointOrientation = false; bool hasCheckedWaypointError = false; RoverOrchestrator({required super.collection}); @@ -115,6 +116,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { waypointIndex = 0; hasCheckedWaypointError = false; hasCheckedWaypointOrientation = false; + isCorrectingWaypointOrientation = false; collection.logger.debug( "Found a path from ${current.prettyPrint()} to ${destination.prettyPrint()}: ${currentPath!.length} steps", ); @@ -145,7 +147,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { currentWaypoint = currentPath![waypointIndex]; currentState = AutonomyState.DRIVING; - if (!hasCheckedWaypointOrientation) { + if (!hasCheckedWaypointOrientation && !isCorrectingWaypointOrientation) { // if it has RTK, point towards the next coordinate if (collection.gps.coordinates.hasRTK) { final difference = @@ -165,8 +167,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { Selector( children: [ Condition(() { + if (isCorrectingWaypointOrientation) { + return true; + } if (!hasCheckedWaypointOrientation) { - hasCheckedWaypointOrientation = true; return currentWaypoint.instruction == DriveDirection.forward && (collection.imu.heading - targetOrientation.z) .clampHalfAngle() @@ -175,12 +179,32 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } return false; }).inverted, + + // If the previous one fails, then it has to recorrect, run a + // task to set the state of recorrecting, this is inverted so + // it will continue with the selection + Task(() { + isCorrectingWaypointOrientation = true; + return NodeStatus.success; + }).inverted, + + // Face the desired orientation SuppliedNode( key: () => targetOrientation, () => collection.drive.faceOrientationNode(targetOrientation), ), ], ), + + // If it makes it through here, it either has corrected, or doesn't need to correct. + // Either way, assume that it has corrected its orientation + Task(() { + hasCheckedWaypointOrientation = true; + isCorrectingWaypointOrientation = false; + return NodeStatus.success; + }), + + // If the distance to the start of our waypoint is too large, replan replanOnCondition(() { if (!hasCheckedWaypointError) { hasCheckedWaypointError = true; @@ -209,14 +233,26 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { key: () => waypointIndex, () => collection.drive.driveStateNode(currentWaypoint), ), + + // If the waypoint state has been driven, increase our waypoint index, + // and reset our correction state Task(() { traversed.add(currentWaypoint.position); waypointIndex++; hasCheckedWaypointOrientation = false; + isCorrectingWaypointOrientation = false; hasCheckedWaypointError = false; return NodeStatus.success; }), + + // Replan if there are new obstacles or we've traversed 5 waypoints, + // we want to periodically replan the path to ensure we're on track replanOnCondition(() => findAndLockObstacles() || waypointIndex >= 5), + + // This technically isn't needed for following the path, however, the tree root + // relies on the tree not being "success" until we have reached our destination, + // adding this here guarantees that this node will only be successful if we are + // done following Condition(() => collection.gps.isNear(destination, Constants.maxErrorMeters)), ], ); @@ -226,26 +262,33 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { var resolvedOrientation = false; return Sequence( children: [ + // If we haven't initially resolved our orientation, resolve the orientation Selector( children: [ - Condition(() { - if (!resolvedOrientation) { - resolvedOrientation = true; - return true; - } - return false; - }).inverted, + Condition(() => resolvedOrientation), SuppliedNode(() => collection.drive.resolveOrientationNode()), ], ), + Task(() { + resolvedOrientation = true; + return NodeStatus.success; + }), Selector( children: [ + // Success if we are near the destination Condition( () => collection.gps.isNear(destination, Constants.maxErrorMeters), ), + + // Plan the path, if there is already a path, + // this will fail and the selection will continue planPath(destination), + + // Follow the path, this will fail if it hasn't reached the destination, + // this failure scenario is "caught" in the next step followPath(destination), + // Only runs if plan path failed, and follow path failed, indicating 2 scenarios: // 1. Couldn't find a path at all // 2. Couldn't follow a specific step of the path @@ -371,6 +414,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { waypointIndex = 0; hasCheckedWaypointError = false; hasCheckedWaypointOrientation = false; + isCorrectingWaypointOrientation = false; replanPath = true; behaviorRoot = pathToDestination(destination); behaviorTreeTimer = Timer.periodic(const Duration(milliseconds: 10), ( From fb4410b2683a6844e2333883de28d86b4ab3043a Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:35:00 -0400 Subject: [PATCH 066/110] Fix obstacle locking --- lib/src/orchestrator/rover_orchestrator.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index e546e59..8868ee9 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -47,10 +47,16 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { if (currentPath == null) return true; - currentPath! - .map((state) => state.position) - .where((position) => collection.pathfinder.isObstacle(position)) - .forEach(collection.pathfinder.lockObstacle); + for (final step in currentPath!.map((state) => state.position)) { + // Since we're iterating over the obstacles that we also want to lock, + // we have to create a copy of the ones we want to lock, otherwise we'll + // be modifying the array while iterating over it + final toLock = collection.pathfinder.obstacles + .where((obstacle) => collection.pathfinder.isObstacle(step)) + .toSet(); + + toLock.forEach(collection.pathfinder.lockObstacle); + } return true; } From afb14ffd60abb405ef1b7ff24dce902e6f2dbf70 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 1 May 2025 14:08:04 -0400 Subject: [PATCH 067/110] Fix obstacle locking and add more comments --- lib/src/orchestrator/rover_orchestrator.dart | 75 ++++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 50d12d4..6e47daa 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -9,13 +9,25 @@ import "dart:async"; import "package:coordinate_converter/coordinate_converter.dart"; class RoverOrchestrator extends OrchestratorInterface with ValueReporter { + /// The GPS coordinates that the rover has traversed during the task final List traversed = []; + + /// The current path that the rover is following List? currentPath; + /// Whether or not the rover should replan the path, this is managed by the behavior tree bool replanPath = true; + + /// The current waypoint index of the path that the rover is following int waypointIndex = 0; + + /// Whether or not the rover has checked the waypoint orientation of the current path step bool hasCheckedWaypointOrientation = false; + + /// Whether or not the rover is currently correction waypoint orientation bool isCorrectingWaypointOrientation = false; + + /// Whether or not the rover has checked the waypoint error for the current step in the path bool hasCheckedWaypointError = false; RoverOrchestrator({required super.collection}); @@ -55,6 +67,11 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { @override Message getMessage() => statusMessage; + /// Finds new obstacles and locks them if any intersect with the current path + /// + /// If the implementation of the obstacle detector has detected any obstacles, + /// it will "lock" any obstacles that intersect with the current path, to prevent + /// future paths from being planned in that area. bool findAndLockObstacles() { if (!collection.detector.findObstacles()) { return false; @@ -62,20 +79,28 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { if (currentPath == null) return true; + final toLock = {}; + for (final step in currentPath!.map((state) => state.position)) { // Since we're iterating over the obstacles that we also want to lock, // we have to create a copy of the ones we want to lock, otherwise we'll // be modifying the array while iterating over it - final toLock = collection.pathfinder.obstacles - .where((obstacle) => collection.pathfinder.isObstacle(step)) - .toSet(); - - toLock.forEach(collection.pathfinder.lockObstacle); + toLock.addAll( + collection.pathfinder.obstacles.where( + (obstacle) => collection.pathfinder.isObstacle(step), + ), + ); } + toLock.forEach(collection.pathfinder.lockObstacle); + return true; } + /// A node that will trigger a path replan when [condition] is true + /// + /// If [condition] is true, [replanPath] will be set to true, and the + /// node will fail. Otherwise, it will be successful. BaseNode replanOnCondition(bool Function() condition) => Task(() { if (condition()) { replanPath = true; @@ -84,6 +109,21 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { return NodeStatus.success; }); + /// A node to plan a path towards [destination] + /// + /// This node will only create a new path towards [destination], and not follow it. + /// + /// This node will fail if either: + /// 1. There is a current path already planned + /// 2. There is no command currently running + /// 3. The GPS hasn't received a value + /// 4. The IMU hasn't received a value + /// 5. A path could not be planned + /// Otherwise, this node will be successful + /// + /// Since this node will fail if a path is already planned, + /// this should be wrapped in a decorator such as a selector + /// to prevent the entire tree from failing. BaseNode planPath(GpsCoordinates destination) => Sequence( children: [ Condition( @@ -136,6 +176,23 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { ], ); + /// Creates a node to follow a path towards [destination] + /// + /// This node will handle the logic of driving through the individual steps + /// of [currentPath], handling obstacle detection, replanning logic, and recorrection. + /// + /// This node will not plan a new path, see [planPath] + /// + /// This node will fail if either: + /// 1. There is no path planned + /// 2. The node for following the current path step failed + /// 3. A new obstacle was detected + /// 4. 5 steps of the path have been followed + /// 5. A step of the path was completed but the rover has not reached [destination] + /// + /// Since this node will fail if the rover isn't near [destination], this + /// should be wrapped in a decorator such as a selector or inverter to prevent + /// the entire tree from failing. BaseNode followPath(GpsCoordinates destination) { late AutonomyAStarState currentWaypoint; // Orientation the rover should be facing before driving forward @@ -264,6 +321,14 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { ); } + /// Creates a node to plan and follow a path towards [destination] + /// + /// This node combines [planPath] and [followPath] to dynamically plan + /// and follow a path to drive the rover towards [destination]. + /// + /// If a path could not be planned towards [destination], the node will + /// fail. If the rover has reached [destination], it will succeed, otherwise, + /// it will return running. BaseNode pathToDestination(GpsCoordinates destination) { var resolvedOrientation = false; return Sequence( From a16977a0c4daf6feb0e425cde8d4e40418a69495 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 25 May 2025 15:03:34 -0400 Subject: [PATCH 068/110] Initial FSM implementation --- lib/src/drive/drive_interface.dart | 30 +++ lib/src/drive/rover_drive.dart | 41 ++++ lib/src/drive/sensor_drive.dart | 95 ++++++++ lib/src/drive/sim_drive.dart | 41 +++- lib/src/drive/timed_drive.dart | 106 ++++++++- lib/src/fsm/decorators.dart | 34 +++ lib/src/fsm/fsm_controller.dart | 30 +++ lib/src/fsm/rover_fsm.dart | 8 + lib/src/fsm/rover_state.dart | 25 ++ lib/src/fsm/state_utils.dart | 70 ++++++ .../orchestrator/orchestrator_interface.dart | 3 + lib/src/orchestrator/rover_orchestrator.dart | 222 +++++++++++++++++- 12 files changed, 690 insertions(+), 15 deletions(-) create mode 100644 lib/src/fsm/decorators.dart create mode 100644 lib/src/fsm/fsm_controller.dart create mode 100644 lib/src/fsm/rover_fsm.dart create mode 100644 lib/src/fsm/rover_state.dart create mode 100644 lib/src/fsm/state_utils.dart diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index c315e26..25ce324 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -1,4 +1,5 @@ import "package:autonomy/interfaces.dart"; +import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:behavior_tree/behavior_tree.dart"; import "drive_config.dart"; @@ -35,9 +36,38 @@ abstract class DriveInterface extends Service { /// The drive configuration for the rover it is running on DriveConfig config; + /// Getter to access the FSM controller + FSMController get controller => collection.orchestrator.controller; + /// Constructor for Drive Interface DriveInterface({required this.collection, this.config = roverConfig}); + StateInterface driveStateState(AutonomyAStarState state) { + if (state.instruction == DriveDirection.stop) { + return FunctionalState( + controller, + onUpdate: (controller) => controller.popState(), + ); + } else if (state.instruction == DriveDirection.forward) { + return driveForwardState(state.position); + } else { + return turnStateState(state); + } + } + + StateInterface resolveOrientationState() => + faceDirectionState(collection.imu.nearest); + + StateInterface driveForwardState(GpsCoordinates coordinates); + + StateInterface faceDirectionState(CardinalDirection direction) => + faceOrientationState(direction.orientation); + + StateInterface faceOrientationState(Orientation orientation); + + StateInterface turnStateState(AutonomyAStarState state) => + faceOrientationState(state.orientation.orientation); + BaseNode driveStateNode(AutonomyAStarState state) { if (state.instruction == DriveDirection.stop) { return Task(() { diff --git a/lib/src/drive/rover_drive.dart b/lib/src/drive/rover_drive.dart index 624e54d..58ecb52 100644 --- a/lib/src/drive/rover_drive.dart +++ b/lib/src/drive/rover_drive.dart @@ -1,5 +1,7 @@ import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; +import "package:autonomy/src/fsm/rover_fsm.dart"; +import "package:autonomy/src/fsm/rover_state.dart"; import "package:behavior_tree/behavior_tree.dart"; import "sensor_drive.dart"; @@ -147,4 +149,43 @@ class RoverDrive extends DriveInterface { @override BaseNode spinForArucoNode(int arucoId, {CameraName? desiredCamera}) => sensorDrive.spinForArucoNode(arucoId, desiredCamera: desiredCamera); + + @override + StateInterface driveForwardState(GpsCoordinates coordinates) { + if (useGps) { + return sensorDrive.driveForwardState(coordinates); + } else { + return SequenceState( + controller, + steps: [ + timedDrive.driveForwardState(coordinates), + simDrive.driveForwardState(coordinates), + ], + ); + } + } + + @override + StateInterface turnStateState(AutonomyAStarState state) { + if (useImu) { + return sensorDrive.turnStateState(state); + } else { + return SequenceState( + controller, + steps: [ + timedDrive.turnStateState(state), + simDrive.turnStateState(state), + ], + ); + } + } + + @override + StateInterface faceOrientationState(Orientation orientation) { + if (useImu) { + return sensorDrive.faceOrientationState(orientation); + } else { + return simDrive.faceOrientationState(orientation); + } + } } diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index f267a4d..a269da3 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -1,11 +1,84 @@ import "package:autonomy/autonomy.dart"; import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; +import "package:autonomy/src/drive/drive_config.dart"; +import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:autonomy/src/utils/behavior_util.dart"; import "package:behavior_tree/behavior_tree.dart"; import "drive_commands.dart"; +class SensorForwardState extends RoverState { + final AutonomyInterface collection; + final GpsCoordinates position; + + final RoverDriveCommands drive; + + DriveConfig get config => collection.drive.config; + + SensorForwardState(super.controller, { + required this.collection, + required this.position, + required this.drive, + }); + + @override + void update() { + drive.setThrottle(config.forwardThrottle); + drive.moveForward(); + + if (collection.gps.isNear( + position, + Constants.intermediateStepTolerance, + )) { + controller.popState(); + } + } + + @override + void exit() { + drive.stopMotors(); + } +} + +class SensorTurnState extends RoverState { + final AutonomyInterface collection; + final Orientation orientation; + + final RoverDriveCommands drive; + + DriveConfig get config => collection.drive.config; + + SensorTurnState(super.controller, { + required this.collection, + required this.orientation, + required this.drive, + }); + + @override + void update() { + drive.setThrottle(config.turnThrottle); + + final current = collection.imu.heading; + final target = orientation.heading; + final error = (target - current).clampHalfAngle(); + if (error < 0) { + drive.spinRight(); + } else { + drive.spinLeft(); + } + + if (collection.imu.isNear(orientation)) { + controller.popState(); + } + } + + @override + void exit() { + drive.stopMotors(); + } +} + /// An implementation of [DriveInterface] that uses the rover's sensors to /// determine its direction to move in and whether or not it has moved in its /// desired direction/orientation @@ -19,6 +92,28 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { /// Default constructor for SensorDrive SensorDrive({required super.collection, super.config}); + @override + StateInterface driveForwardState(GpsCoordinates coordinates) => + TimeoutDecorator( + child: SensorForwardState( + controller, + collection: collection, + position: coordinates, + drive: this, + ), + onTimeout: (controller) => controller.popState(), + timeout: Constants.driveGPSTimeout, + ); + + @override + StateInterface faceOrientationState(Orientation orientation) => + SensorTurnState( + controller, + collection: collection, + orientation: orientation, + drive: this, + ); + @override BaseNode driveForwardNode(GpsCoordinates coordinates) => Selector( children: [ diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index 205bec5..bc35421 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -1,4 +1,5 @@ import "package:autonomy/interfaces.dart"; +import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:autonomy/src/utils/behavior_util.dart"; import "package:behavior_tree/behavior_tree.dart"; @@ -18,11 +19,45 @@ class DriveSimulator extends DriveInterface { /// Constructor for DriveSimulator, initializing the default fields, and whether or not it should delay DriveSimulator({required super.collection, this.shouldDelay = false, super.config}); - BaseNode _delayAndExecute({ + BaseNode _delayAndExecuteNode({ required Duration delay, required BaseNode child, }) => Sequence(children: [DelayedNode(delay), child]); + RoverState _delayAndExecuteState({ + required Duration delay, + required StateInterface child, + }) => SequenceState( + child.controller, + steps: [DelayedState(child.controller, delayTime: delay), child], + ); + + @override + StateInterface driveForwardState(GpsCoordinates coordinates) => + _delayAndExecuteState( + delay: shouldDelay ? delay : Duration.zero, + child: FunctionalState( + controller, + onUpdate: (controller) { + collection.gps.update(coordinates); + controller.popState(); + }, + ), + ); + + @override + StateInterface faceOrientationState(Orientation orientation) => + _delayAndExecuteState( + delay: shouldDelay ? delay : Duration.zero, + child: FunctionalState( + controller, + onUpdate: (controller) { + collection.imu.update(orientation); + controller.popState(); + }, + ), + ); + @override BaseNode driveForwardNode(GpsCoordinates coordinates) { final updateGps = Task(() { @@ -31,7 +66,7 @@ class DriveSimulator extends DriveInterface { }); return ConditionalNode( condition: () => shouldDelay, - onTrue: _delayAndExecute(delay: delay, child: updateGps), + onTrue: _delayAndExecuteNode(delay: delay, child: updateGps), onFalse: updateGps, ); } @@ -44,7 +79,7 @@ class DriveSimulator extends DriveInterface { }); return ConditionalNode( condition: () => shouldDelay, - onTrue: _delayAndExecute(delay: delay, child: updateImu), + onTrue: _delayAndExecuteNode(delay: delay, child: updateImu), onFalse: updateImu, ); } diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index 5b7aaa4..a0c26b5 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -2,17 +2,45 @@ import "dart:math"; import "package:autonomy/interfaces.dart"; import "package:autonomy/src/drive/drive_config.dart"; +import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:behavior_tree/behavior_tree.dart"; import "drive_commands.dart"; -class _TimedOperation extends BaseNode { +class _TimedOperationState extends RoverState { + DateTime _startTime = DateTime(0); + + final Duration time; + final void Function() operation; + + _TimedOperationState( + super.controller, { + required this.time, + required this.operation, + }); + + @override + void enter() { + _startTime = DateTime.now(); + } + + @override + void update() { + if (DateTime.now().difference(_startTime) >= time) { + controller.popState(); + return; + } + operation(); + } +} + +class _TimedOperationNode extends BaseNode { DateTime? _start; final Duration time; final void Function() operation; - _TimedOperation({required this.time, required this.operation}); + _TimedOperationNode({required this.time, required this.operation}); @override void tick() { @@ -49,7 +77,71 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { } @override - BaseNode driveForwardNode(GpsCoordinates coordinates) => _TimedOperation( + StateInterface faceOrientationState(Orientation orientation) { + throw UnsupportedError( + "Cannot face any arbitrary direction using TimedDrive", + ); + } + + @override + StateInterface driveForwardState(GpsCoordinates coordinates) => + _TimedOperationState( + controller, + time: + config.oneMeterDelay * + (collection.imu.nearest.isPerpendicular ? 1 : sqrt2), + operation: () { + setThrottle(config.forwardThrottle); + moveForward(); + }, + ); + + @override + StateInterface turnStateState(AutonomyAStarState state) => switch (state + .instruction) { + DriveDirection.forward => throw UnimplementedError(), + + DriveDirection.left => _TimedOperationState( + controller, + time: config.turnDelay, + operation: () { + setThrottle(config.turnThrottle); + spinLeft(); + }, + ), + + DriveDirection.right => _TimedOperationState( + controller, + time: config.turnDelay, + operation: () { + setThrottle(config.turnThrottle); + spinRight(); + }, + ), + + DriveDirection.quarterLeft => _TimedOperationState( + controller, + time: config.turnDelay * 0.5, + operation: () { + setThrottle(config.turnThrottle); + spinLeft(); + }, + ), + + DriveDirection.quarterRight => _TimedOperationState( + controller, + time: config.turnDelay * 0.5, + operation: () { + setThrottle(config.turnThrottle); + spinRight(); + }, + ), + + DriveDirection.stop => throw UnimplementedError(), + }; + + @override + BaseNode driveForwardNode(GpsCoordinates coordinates) => _TimedOperationNode( time: config.oneMeterDelay * (collection.imu.nearest.isPerpendicular ? 1 : sqrt2), @@ -64,7 +156,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { .instruction) { DriveDirection.forward => throw UnimplementedError(), - DriveDirection.left => _TimedOperation( + DriveDirection.left => _TimedOperationNode( time: config.turnDelay, operation: () { setThrottle(config.turnThrottle); @@ -72,7 +164,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { }, ), - DriveDirection.right => _TimedOperation( + DriveDirection.right => _TimedOperationNode( time: config.turnDelay, operation: () { setThrottle(config.turnThrottle); @@ -80,7 +172,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { }, ), - DriveDirection.quarterLeft => _TimedOperation( + DriveDirection.quarterLeft => _TimedOperationNode( time: config.turnDelay * 0.5, operation: () { setThrottle(config.turnThrottle); @@ -88,7 +180,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { }, ), - DriveDirection.quarterRight => _TimedOperation( + DriveDirection.quarterRight => _TimedOperationNode( time: config.turnDelay * 0.5, operation: () { setThrottle(config.turnThrottle); diff --git a/lib/src/fsm/decorators.dart b/lib/src/fsm/decorators.dart new file mode 100644 index 0000000..3453131 --- /dev/null +++ b/lib/src/fsm/decorators.dart @@ -0,0 +1,34 @@ +import "package:autonomy/src/fsm/rover_fsm.dart"; + +class TimeoutDecorator extends RoverState { + final RoverState child; + final Duration timeout; + final StateCallback onTimeout; + + DateTime startTime = DateTime(0); + + TimeoutDecorator({ + required this.child, + required this.timeout, + required this.onTimeout, + }) : super(child.controller); + + @override + void enter() { + startTime = DateTime.now(); + child.enter(); + } + + @override + void update() { + if (DateTime.now().difference(startTime) > timeout) { + return onTimeout(controller); + } + child.update(); + } + + @override + void exit() { + child.exit(); + } +} diff --git a/lib/src/fsm/fsm_controller.dart b/lib/src/fsm/fsm_controller.dart new file mode 100644 index 0000000..91f368f --- /dev/null +++ b/lib/src/fsm/fsm_controller.dart @@ -0,0 +1,30 @@ +import "package:autonomy/src/fsm/rover_fsm.dart"; + +class FSMController { + final List _stateStack = []; + + void pushState(StateInterface state) { + state.enter(); + _stateStack.add(state); + } + + void transitionTo(StateInterface state) { + _stateStack.removeLast().exit(); + state.enter(); + _stateStack.add(state); + } + + void popState() { + _stateStack.removeLast().exit(); + } + + void update() { + if (_stateStack.isNotEmpty) { + _stateStack.last.update(); + } + } + + bool hasState() => _stateStack.isNotEmpty; + + bool hasSubstate() => _stateStack.length > 1; +} diff --git a/lib/src/fsm/rover_fsm.dart b/lib/src/fsm/rover_fsm.dart new file mode 100644 index 0000000..a6b5715 --- /dev/null +++ b/lib/src/fsm/rover_fsm.dart @@ -0,0 +1,8 @@ +import "package:autonomy/src/fsm/fsm_controller.dart"; + +export "fsm_controller.dart"; +export "rover_state.dart"; +export "decorators.dart"; +export "state_utils.dart"; + +typedef StateCallback = void Function(FSMController controller); diff --git a/lib/src/fsm/rover_state.dart b/lib/src/fsm/rover_state.dart new file mode 100644 index 0000000..4b5d6f9 --- /dev/null +++ b/lib/src/fsm/rover_state.dart @@ -0,0 +1,25 @@ +import "package:autonomy/src/fsm/rover_fsm.dart"; + +abstract class StateInterface { + FSMController get controller; + + void enter(); + void update(); + void exit(); +} + +class RoverState implements StateInterface { + @override + final FSMController controller; + + RoverState(this.controller); + + @override + void enter() {} + + @override + void update() {} + + @override + void exit() {} +} diff --git a/lib/src/fsm/state_utils.dart b/lib/src/fsm/state_utils.dart new file mode 100644 index 0000000..1852dd0 --- /dev/null +++ b/lib/src/fsm/state_utils.dart @@ -0,0 +1,70 @@ +import "package:autonomy/src/fsm/rover_fsm.dart"; + +class DelayedState extends RoverState { + final Duration delayTime; + + DateTime _startTime = DateTime(0); + + DelayedState(super.controller, {required this.delayTime}); + + @override + void enter() { + _startTime = DateTime.now(); + } + + @override + void update() { + if (DateTime.now().difference(_startTime) >= delayTime) { + controller.popState(); + } + } +} + +class FunctionalState extends RoverState { + final StateCallback? onEnter; + final StateCallback? onUpdate; + final StateCallback? onExit; + + FunctionalState( + super.controller, { + this.onEnter, + this.onUpdate, + this.onExit, + }); + + @override + void enter() => onEnter?.call(controller); + + @override + void update() => onUpdate?.call(controller); + + @override + void exit() => onExit?.call(controller); +} + +class SequenceState extends RoverState { + final List steps; + + int _stepIndex = 0; + + SequenceState(super.controller, {required this.steps}); + + @override + void enter() { + if (steps.isNotEmpty) { + controller.pushState(steps[_stepIndex]); + } + } + + @override + void update() { + _stepIndex++; + // There's another state left in the sequence, push it + if (_stepIndex < steps.length) { + controller.pushState(steps[_stepIndex]); + } else { + // sequence is done + controller.popState(); + } + } +} diff --git a/lib/src/orchestrator/orchestrator_interface.dart b/lib/src/orchestrator/orchestrator_interface.dart index 676d570..6afe280 100644 --- a/lib/src/orchestrator/orchestrator_interface.dart +++ b/lib/src/orchestrator/orchestrator_interface.dart @@ -1,6 +1,7 @@ import "dart:async"; import "package:autonomy/interfaces.dart"; +import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:behavior_tree/behavior_tree.dart"; import "package:meta/meta.dart"; @@ -12,6 +13,8 @@ abstract class OrchestratorInterface extends Service { AutonomyState currentState = AutonomyState.AUTONOMY_STATE_UNDEFINED; Timer? behaviorTreeTimer; + FSMController controller = FSMController(); + BaseNode behaviorRoot = Condition(() => true); Future onCommand(AutonomyCommand command) async { diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 6e47daa..9897e66 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -2,12 +2,194 @@ import "dart:math"; import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; +import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:autonomy/src/utils/behavior_util.dart"; import "package:behavior_tree/behavior_tree.dart"; import "dart:async"; import "package:coordinate_converter/coordinate_converter.dart"; +class PathingState extends RoverState { + final AutonomyInterface collection; + final RoverOrchestrator orchestrator; + + final GpsCoordinates destination; + + PathingState( + super.controller, { + required this.collection, + required this.orchestrator, + required this.destination, + }); + + @override + void enter() { + orchestrator.currentState = AutonomyState.PATHING; + orchestrator.findAndLockObstacles(); + } + + @override + void update() { + final current = collection.gps.coordinates; + orchestrator.currentPath = collection.pathfinder.getPath( + orchestrator.currentCommand!.destination, + ); + if (orchestrator.currentPath == null) { + collection.logger.error( + "Could not find a path", + body: + "No path found from ${current.prettyPrint()} to ${destination.prettyPrint()}", + ); + controller.popState(); + } else { + collection.logger.debug( + "Found a path from ${current.prettyPrint()} to ${destination.prettyPrint()}: ${orchestrator.currentPath!.length} steps", + ); + collection.logger.debug("Here is a summary of the path"); + for (final step in orchestrator.currentPath!) { + collection.logger.debug(step.toString()); + } + controller.transitionTo( + NavigationState( + controller, + collection: collection, + orchestrator: orchestrator, + destination: destination, + ), + ); + } + } +} + +class NavigationState extends RoverState { + final AutonomyInterface collection; + final RoverOrchestrator orchestrator; + + final GpsCoordinates destination; + + bool hasCorrected = false; + bool hasFollowed = false; + int waypointIndex = 0; + + AutonomyAStarState? currentPathState; + + NavigationState( + super.controller, { + required this.collection, + required this.orchestrator, + required this.destination, + }); + + @override + void enter() { + waypointIndex = 0; + hasCorrected = false; + hasFollowed = false; + + currentPathState = orchestrator.currentPath?[waypointIndex]; + orchestrator.currentState = AutonomyState.DRIVING; + } + + void checkOrientation(AutonomyAStarState state) { + Orientation targetOrientation; + // if it has RTK, point towards the next coordinate + if (collection.gps.coordinates.hasRTK) { + final difference = + state.position.toUTM() - collection.gps.coordinates.toUTM(); + + final angle = atan2(difference.y, difference.x) * 180 / pi; + + targetOrientation = Orientation(z: angle); + } else { + targetOrientation = state.orientation.orientation; + } + + if (!collection.imu.isNear( + targetOrientation, + Constants.driveRealignmentEpsilon, + )) { + collection.logger.info("Re-aligning IMU to correct orientation"); + controller.pushState( + collection.drive.faceOrientationState(targetOrientation), + ); + } + } + + void checkPosition(AutonomyAStarState state) { + final distanceError = collection.gps.coordinates.distanceTo( + state.startPostition, + ); + if (distanceError > Constants.replanErrorMeters) { + collection.logger.info( + "Replanning Path", + body: "Rover is $distanceError meters off the path", + ); + controller.transitionTo( + PathingState( + controller, + collection: collection, + orchestrator: orchestrator, + destination: destination, + ), + ); + } + } + + void checkCurrentPosition(AutonomyAStarState state) { + if (state.instruction == DriveDirection.forward) { + checkOrientation(state); + } else { + checkPosition(state); + } + } + + @override + void update() { + if (currentPathState == null) { + controller.popState(); + } + if (!hasCorrected) { + hasCorrected = true; + checkCurrentPosition(orchestrator.currentPath![waypointIndex]); + return; + } + if (!hasFollowed) { + hasFollowed = true; + collection.logger.debug(currentPathState!.toString()); + controller.pushState( + collection.drive.driveStateState( + currentPathState!, + ), + ); + return; + } + if (waypointIndex >= 5 || orchestrator.findAndLockObstacles()) { + collection.drive.stop(); + controller.transitionTo( + PathingState( + controller, + collection: collection, + orchestrator: orchestrator, + destination: destination, + ), + ); + return; + } + if (collection.gps.isNear(destination, Constants.maxErrorMeters)) { + controller.popState(); + return; + } + + + orchestrator.traversed.add(currentPathState!.position); + + waypointIndex++; + hasCorrected = false; + hasFollowed = false; + currentPathState = orchestrator.currentPath?[waypointIndex]; + } +} + class RoverOrchestrator extends OrchestratorInterface with ValueReporter { /// The GPS coordinates that the rover has traversed during the task final List traversed = []; @@ -488,26 +670,56 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { isCorrectingWaypointOrientation = false; replanPath = true; behaviorRoot = pathToDestination(destination); + controller.pushState( + SequenceState( + controller, + steps: [ + collection.drive.resolveOrientationState(), + PathingState( + controller, + collection: collection, + orchestrator: this, + destination: destination, + ), + ], + ), + ); behaviorTreeTimer = Timer.periodic(const Duration(milliseconds: 10), ( timer, ) { if (currentCommand == null) { return; } - behaviorRoot.tick(); - if (behaviorRoot.status == NodeStatus.failure) { - behaviorRoot.reset(); + if (!controller.hasState()) { currentState = AutonomyState.NO_SOLUTION; currentCommand = null; timer.cancel(); - } else if (behaviorRoot.status == NodeStatus.success) { - behaviorRoot.reset(); + return; + } + if (collection.gps.isNear(destination, Constants.maxErrorMeters)) { timer.cancel(); collection.logger.info("Task complete"); currentState = AutonomyState.AT_DESTINATION; collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); + collection.drive.stop(); currentCommand = null; + return; } + controller.update(); + // behaviorRoot.tick(); + // if (behaviorRoot.status == NodeStatus.failure) { + // behaviorRoot.reset(); + // currentState = AutonomyState.NO_SOLUTION; + // currentCommand = null; + // timer.cancel(); + // } else if (behaviorRoot.status == NodeStatus.success) { + // behaviorRoot.reset(); + // timer.cancel(); + // collection.logger.info("Task complete"); + // currentState = AutonomyState.AT_DESTINATION; + // collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); + // currentCommand = null; + // } }); // detect obstacles before and after resolving orientation, as a "scan" // collection.detector.findObstacles(); From a5afb663826617f1d53bf0bd634a2db347a4c96d Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 25 May 2025 16:10:08 -0400 Subject: [PATCH 069/110] Fix null exception and use set for path message --- lib/src/orchestrator/rover_orchestrator.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 9897e66..5ed87d1 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -150,7 +150,7 @@ class NavigationState extends RoverState { } if (!hasCorrected) { hasCorrected = true; - checkCurrentPosition(orchestrator.currentPath![waypointIndex]); + checkCurrentPosition(currentPathState!); return; } if (!hasFollowed) { @@ -237,11 +237,11 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { ...collection.pathfinder.obstacles, ...collection.pathfinder.lockedObstacles, ], - path: [ + path: { for (final transition in currentPath ?? []) transition.position, ...traversed, - ], + }, task: currentCommand?.task, crash: false, // TODO: Investigate if this is used and how to use it better ); From 2b31be40e86d34d7fffb55f107b7dc5db19e852b Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 25 May 2025 16:52:50 -0400 Subject: [PATCH 070/110] Fix additional null error --- lib/src/orchestrator/rover_orchestrator.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 5ed87d1..1b8a37f 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -147,6 +147,7 @@ class NavigationState extends RoverState { void update() { if (currentPathState == null) { controller.popState(); + return; } if (!hasCorrected) { hasCorrected = true; From f7cc737a320a905ee8bdf80bc2bde7bc0f5eb979 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 25 May 2025 16:53:14 -0400 Subject: [PATCH 071/110] Combine drive speed commands --- lib/src/drive/drive_commands.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/drive/drive_commands.dart b/lib/src/drive/drive_commands.dart index 3b149e7..5979008 100644 --- a/lib/src/drive/drive_commands.dart +++ b/lib/src/drive/drive_commands.dart @@ -9,7 +9,7 @@ mixin RoverDriveCommands on DriveInterface { /// throttle, as a percentage of the rover's top speed. void setThrottle(double throttle) { collection.logger.trace("Setting throttle to $throttle"); - sendCommand(DriveCommand(throttle: throttle, setThrottle: true)); + sendCommand(DriveCommand(setThrottle: true, throttle: throttle)); } /// Sets the speeds of the left and right wheels, using differential steering. @@ -18,8 +18,9 @@ mixin RoverDriveCommands on DriveInterface { void _setSpeeds({required double left, required double right}) { right *= -1; collection.logger.trace("Setting speeds to $left and $right"); - sendCommand(DriveCommand(left: left, setLeft: true)); - sendCommand(DriveCommand(right: right, setRight: true)); + sendCommand( + DriveCommand(setLeft: true, setRight: true, left: left, right: right), + ); } /// Stops the motors, setting the throttle and speeds to 0 From 2f597450ea229e9db38fd35a1ad016892c3f9f58 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 25 May 2025 18:12:18 -0400 Subject: [PATCH 072/110] Began documentation for FSM classes --- lib/src/fsm/decorators.dart | 17 ++++++-- lib/src/fsm/fsm_controller.dart | 31 ++++++++++++++ lib/src/fsm/rover_fsm.dart | 1 + lib/src/fsm/rover_state.dart | 11 +++++ lib/src/fsm/state_utils.dart | 31 +++++++++++--- lib/src/orchestrator/rover_orchestrator.dart | 45 ++++++++++++++++++++ 6 files changed, 127 insertions(+), 9 deletions(-) diff --git a/lib/src/fsm/decorators.dart b/lib/src/fsm/decorators.dart index 3453131..28f56b5 100644 --- a/lib/src/fsm/decorators.dart +++ b/lib/src/fsm/decorators.dart @@ -1,12 +1,23 @@ import "package:autonomy/src/fsm/rover_fsm.dart"; +/// State to add a timeout to a state +/// +/// The [child] of this state will be updated each time update is +/// called on this state, but when [timeout] amount of time elapses +/// after starting, the state will be popped. class TimeoutDecorator extends RoverState { + /// The child state the timeout will be applied to final RoverState child; + + /// The timeout duration final Duration timeout; + + /// The function to execute when the timeout is reached final StateCallback onTimeout; - DateTime startTime = DateTime(0); + DateTime _startTime = DateTime(0); + /// Constructor for [TimeoutDecorator] TimeoutDecorator({ required this.child, required this.timeout, @@ -15,13 +26,13 @@ class TimeoutDecorator extends RoverState { @override void enter() { - startTime = DateTime.now(); + _startTime = DateTime.now(); child.enter(); } @override void update() { - if (DateTime.now().difference(startTime) > timeout) { + if (DateTime.now().difference(_startTime) > timeout) { return onTimeout(controller); } child.update(); diff --git a/lib/src/fsm/fsm_controller.dart b/lib/src/fsm/fsm_controller.dart index 91f368f..40d5a27 100644 --- a/lib/src/fsm/fsm_controller.dart +++ b/lib/src/fsm/fsm_controller.dart @@ -1,30 +1,61 @@ import "package:autonomy/src/fsm/rover_fsm.dart"; +/// Class to control a Finite State Machine, handling state pushes, +/// pops, and transitions +/// +/// The class maintains a stack of [StateInterface], where the most recent +/// state, or the one at the top of the stack, is the one being updated. +/// +/// When a new state is pushed, it will be added to the top of the state +/// stack, and becomes the new state being updated. This means that any +/// states underneath will no longer be updated. +/// +/// When a state is popped, it is removed from the top of the stack, and will be exited. class FSMController { final List _stateStack = []; + /// Pushes a new state to the top of the stack, and enters it void pushState(StateInterface state) { state.enter(); _stateStack.add(state); } + /// Pops and exits the latest state from the top of the stack and replaces it with [state] void transitionTo(StateInterface state) { _stateStack.removeLast().exit(); state.enter(); _stateStack.add(state); } + /// Pops the latest state from the top of the stack void popState() { _stateStack.removeLast().exit(); } + /// Pops all states the come before a state of type [T], not including [T] + void popUntil() { + final index = _stateStack.lastIndexWhere((element) => element is T); + + if (index == -1) { + return; + } + + for (var i = 0; i < _stateStack.length - index - 1; i++) { + popState(); + } + } + + /// Updates the state controller, calling the update + /// method on the last element of the state stack void update() { if (_stateStack.isNotEmpty) { _stateStack.last.update(); } } + /// Whether or not there are any states in the stack bool hasState() => _stateStack.isNotEmpty; + /// Whether or not there is a substate, or more than one state, in the stack bool hasSubstate() => _stateStack.length > 1; } diff --git a/lib/src/fsm/rover_fsm.dart b/lib/src/fsm/rover_fsm.dart index a6b5715..5d9e9c8 100644 --- a/lib/src/fsm/rover_fsm.dart +++ b/lib/src/fsm/rover_fsm.dart @@ -5,4 +5,5 @@ export "rover_state.dart"; export "decorators.dart"; export "state_utils.dart"; +/// Callback for a state method that takes in a controller typedef StateCallback = void Function(FSMController controller); diff --git a/lib/src/fsm/rover_state.dart b/lib/src/fsm/rover_state.dart index 4b5d6f9..d106bb7 100644 --- a/lib/src/fsm/rover_state.dart +++ b/lib/src/fsm/rover_state.dart @@ -1,17 +1,28 @@ import "package:autonomy/src/fsm/rover_fsm.dart"; +/// Abstracted version of a state abstract class StateInterface { + /// The controller for the state machine FSMController get controller; + /// Called when the state is initially entered void enter(); + + /// Called when the state is updated from the controller void update(); + + /// Called when the state is exited or removed from the stack void exit(); } +/// An implementation of [StateInterface] to be used for the rover +/// +/// Stores an own internal [FSMController] class RoverState implements StateInterface { @override final FSMController controller; + /// Constructor for [RoverState] initializing its controller RoverState(this.controller); @override diff --git a/lib/src/fsm/state_utils.dart b/lib/src/fsm/state_utils.dart index 1852dd0..854c68e 100644 --- a/lib/src/fsm/state_utils.dart +++ b/lib/src/fsm/state_utils.dart @@ -1,10 +1,18 @@ import "package:autonomy/src/fsm/rover_fsm.dart"; +/// State that can only be ran for [delayTime] amount of time +/// after entering before it automatically pops itself from the +/// state stack +/// +/// This is commonly used in sequences to add a delay between actions. +/// This state has no functional logic to it. class DelayedState extends RoverState { + /// The amount of time to wait for before popping final Duration delayTime; DateTime _startTime = DateTime(0); + /// Default constructor for [DelayedState], initializing the delay time DelayedState(super.controller, {required this.delayTime}); @override @@ -20,17 +28,22 @@ class DelayedState extends RoverState { } } +/// A state with no internal logic that operates only on [StateCallback]s given by the user. +/// +/// This state is to allow for simple states to be created inline rather than from a subclass. class FunctionalState extends RoverState { + /// [StateCallback] for when the state is entered final StateCallback? onEnter; + + /// [StateCallback] for when the state is updated final StateCallback? onUpdate; + + /// [StateCallback] for when the state is exited final StateCallback? onExit; - FunctionalState( - super.controller, { - this.onEnter, - this.onUpdate, - this.onExit, - }); + /// Default constructor for [FunctionalState], initializing callbacks + /// for entering, updating, and exiting + FunctionalState(super.controller, {this.onEnter, this.onUpdate, this.onExit}); @override void enter() => onEnter?.call(controller); @@ -42,11 +55,17 @@ class FunctionalState extends RoverState { void exit() => onExit?.call(controller); } +/// A state which pushes multiple sub-states in order +/// +/// This will push and execute the provided [steps] in order, +/// until all states have been entered and finished class SequenceState extends RoverState { + /// The list of states to execute in order final List steps; int _stepIndex = 0; + /// Default constructor for [SequenceState], initializing the steps SequenceState(super.controller, {required this.steps}); @override diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 1b8a37f..efaee03 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -9,12 +9,22 @@ import "dart:async"; import "package:coordinate_converter/coordinate_converter.dart"; +/// State for when the rover is finding a path +/// +/// When ran the state will attempt to plan a path to its given [destination], +/// if successful, it will transition to [NavigationState], if unsuccessful, +/// it will be popped from the stack. class PathingState extends RoverState { + /// The autonomy collection for the state final AutonomyInterface collection; + + /// The orchestrator for the state final RoverOrchestrator orchestrator; + /// The destination to plan a path to final GpsCoordinates destination; + /// Default constructor for [PathingState] PathingState( super.controller, { required this.collection, @@ -61,18 +71,38 @@ class PathingState extends RoverState { } } +/// State to manage the navigation of the rover +/// +/// This state should be pushed after [PathingState], as it depends +/// on having a path already made for the rover to follow. +/// +/// This state will manage following each individual step of the path as well +/// as performing necessary corrections and replanning. +/// +/// When the path has to be replanned, this state will transition to [PathingState] class NavigationState extends RoverState { + /// The collection for the state final AutonomyInterface collection; + + /// The orchestrator for the state final RoverOrchestrator orchestrator; + /// The final destination to navigate to final GpsCoordinates destination; + /// Whether or not the state has performed pre-step correction bool hasCorrected = false; + + /// Whether or not the state has just completed following a path step bool hasFollowed = false; + + /// The index of the waypoint being followed int waypointIndex = 0; + /// The current step of the path being followed AutonomyAStarState? currentPathState; + /// Default constructor for [NavigationState] NavigationState( super.controller, { required this.collection, @@ -90,6 +120,12 @@ class NavigationState extends RoverState { orchestrator.currentState = AutonomyState.DRIVING; } + /// Checks if the rover is oriented properly before driving the [state] + /// + /// This is assuming that the step's instruction is to drive forward. + /// + /// If the rover is not facing the proper direction, a new state will be pushed + /// to re-correct the rover's orientation void checkOrientation(AutonomyAStarState state) { Orientation targetOrientation; // if it has RTK, point towards the next coordinate @@ -115,6 +151,10 @@ class NavigationState extends RoverState { } } + /// Checks if the rover is within a certain distance of [state]'s starting position + /// + /// If the rover is not within [Constants.replanErrorMeters] of the state's starting + /// position, the path will be replanned void checkPosition(AutonomyAStarState state) { final distanceError = collection.gps.coordinates.distanceTo( state.startPostition, @@ -135,6 +175,11 @@ class NavigationState extends RoverState { } } + /// Check's the position and orientation of [state] before following it + /// + /// If the instruction of [state] is to move forward, it will check if the + /// orientation is correct using [checkOrientation], otherwise, it will check + /// the position using [checkPosition] void checkCurrentPosition(AutonomyAStarState state) { if (state.instruction == DriveDirection.forward) { checkOrientation(state); From d3b9a6def52abfc8e26a050ea884c7a5d54f8c30 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 25 May 2025 19:21:47 -0400 Subject: [PATCH 073/110] Several FSM fixes - enter state after adding to stack - Add exit function to timed drive --- lib/src/drive/timed_drive.dart | 10 ++++++++++ lib/src/fsm/fsm_controller.dart | 4 ++-- lib/src/fsm/state_utils.dart | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index a0c26b5..6d42df2 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -12,11 +12,13 @@ class _TimedOperationState extends RoverState { final Duration time; final void Function() operation; + final void Function()? onDone; _TimedOperationState( super.controller, { required this.time, required this.operation, + this.onDone, }); @override @@ -32,6 +34,9 @@ class _TimedOperationState extends RoverState { } operation(); } + + @override + void exit() => onDone?.call(); } class _TimedOperationNode extends BaseNode { @@ -94,6 +99,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { setThrottle(config.forwardThrottle); moveForward(); }, + onDone: stopMotors, ); @override @@ -108,6 +114,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { setThrottle(config.turnThrottle); spinLeft(); }, + onDone: stopMotors, ), DriveDirection.right => _TimedOperationState( @@ -117,6 +124,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { setThrottle(config.turnThrottle); spinRight(); }, + onDone: stopMotors, ), DriveDirection.quarterLeft => _TimedOperationState( @@ -126,6 +134,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { setThrottle(config.turnThrottle); spinLeft(); }, + onDone: stopMotors, ), DriveDirection.quarterRight => _TimedOperationState( @@ -135,6 +144,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { setThrottle(config.turnThrottle); spinRight(); }, + onDone: stopMotors, ), DriveDirection.stop => throw UnimplementedError(), diff --git a/lib/src/fsm/fsm_controller.dart b/lib/src/fsm/fsm_controller.dart index 40d5a27..b0fe42d 100644 --- a/lib/src/fsm/fsm_controller.dart +++ b/lib/src/fsm/fsm_controller.dart @@ -16,15 +16,15 @@ class FSMController { /// Pushes a new state to the top of the stack, and enters it void pushState(StateInterface state) { - state.enter(); _stateStack.add(state); + state.enter(); } /// Pops and exits the latest state from the top of the stack and replaces it with [state] void transitionTo(StateInterface state) { _stateStack.removeLast().exit(); - state.enter(); _stateStack.add(state); + state.enter(); } /// Pops the latest state from the top of the stack diff --git a/lib/src/fsm/state_utils.dart b/lib/src/fsm/state_utils.dart index 854c68e..5a87781 100644 --- a/lib/src/fsm/state_utils.dart +++ b/lib/src/fsm/state_utils.dart @@ -70,6 +70,7 @@ class SequenceState extends RoverState { @override void enter() { + _stepIndex = 0; if (steps.isNotEmpty) { controller.pushState(steps[_stepIndex]); } From b4f3e762bb14ed3702dee6af5e69ace9226094ef Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 25 May 2025 19:33:40 -0400 Subject: [PATCH 074/110] Began fixing unit tests --- test/network_test.dart | 42 +++++++++++++--- test/path_test.dart | 106 ++++++++++++++++------------------------- test/rover_test.dart | 1 - 3 files changed, 77 insertions(+), 72 deletions(-) diff --git a/test/network_test.dart b/test/network_test.dart index 8c64bb6..853f6a7 100644 --- a/test/network_test.dart +++ b/test/network_test.dart @@ -4,6 +4,7 @@ import "dart:io"; import "package:autonomy/autonomy.dart"; import "package:autonomy/src/drive/drive_config.dart"; import "package:burt_network/burt_network.dart"; +import "package:coordinate_converter/coordinate_converter.dart"; import "package:test/test.dart"; class MockSubsystems extends Service { @@ -120,30 +121,57 @@ void main() => group("[Network]", tags: ["network"], () { ); await simulator.init(); - final origin = GpsCoordinates(latitude: 0, longitude: 0); - final oneMeter = (lat: 1, long: 0).toGps(); + final origin = UTMCoordinates(x: 5, y: 5, zoneNumber: 31); + final oneMeter = + (origin + UTMCoordinates(x: 0, y: 1, zoneNumber: 1)).toGps(); + simulator.gps.update(origin.toGps()); expect(subsystems.throttle, 0); expect(subsystems.left, 0); expect(subsystems.right, 0); - expect(simulator.gps.isNear(origin), isTrue); + expect(simulator.gps.isNear(origin.toGps()), isTrue); expect(simulator.gps.isNear(oneMeter), isFalse); expect(subsystems.throttleFlag, isFalse); - final forwardFuture = simulator.drive.driveForward(oneMeter); + final forwardFuture = Future.delayed( + simulator.drive.config.oneMeterDelay, + ); + final forwardState = simulator.drive.driveForwardState(oneMeter); + simulator.orchestrator.controller.pushState(forwardState); + simulator.orchestrator.controller.update(); + simulator.orchestrator.controller.update(); + simulator.orchestrator.controller.update(); + await Future.delayed(simulator.drive.config.oneMeterDelay * 0.5); + expect(subsystems.throttleFlag, isTrue); expect(subsystems.throttle, isNot(0)); expect(subsystems.left, isNot(0)); expect(subsystems.right, isNot(0)); - expect(simulator.gps.isNear(origin), isTrue); + expect(simulator.gps.isNear(origin.toGps()), isTrue); expect(simulator.gps.isNear(oneMeter), isFalse); + + simulator.orchestrator.controller.update(); + simulator.orchestrator.controller.update(); + simulator.orchestrator.controller.update(); + await forwardFuture; - await Future.delayed(simulator.drive.config.oneMeterDelay * 0.5); + + simulator.orchestrator.controller.update(); + simulator.orchestrator.controller.update(); + simulator.orchestrator.controller.update(); + + await Future.delayed(Duration.zero); + expect(subsystems.throttleFlag, isFalse); expect(subsystems.throttle, 0); expect(subsystems.left, 0); expect(subsystems.right, 0); - expect(simulator.gps.isNear(origin), isFalse); + + simulator.orchestrator.controller.update(); + simulator.orchestrator.controller.update(); + simulator.orchestrator.controller.update(); + + expect(simulator.gps.isNear(origin.toGps(), 0.5), isFalse); expect(simulator.gps.isNear(oneMeter), isTrue); subsystems.enabled = false; diff --git a/test/path_test.dart b/test/path_test.dart index c664735..4a3e701 100644 --- a/test/path_test.dart +++ b/test/path_test.dart @@ -1,46 +1,42 @@ import "dart:math"; -import "package:autonomy/constants.dart"; import "package:burt_network/burt_network.dart"; +import "package:coordinate_converter/coordinate_converter.dart"; import "package:test/test.dart"; import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; import "package:autonomy/simulator.dart"; -extension DriveFollowPath on DriveInterface { - Future followPath(List path) async { - for (final step in path) { - await driveState(step); - } - } -} +final UTMCoordinates base = UTMCoordinates(x: 5, y: 5, zoneNumber: 31); void main() => group("[Pathfinding]", tags: ["path"], () { - setUp(() => Logger.level = LogLevel.off); + setUp(() => Logger.level = LogLevel.info); tearDown(() => Logger.level = LogLevel.off); test("Simple path from (0, 0) to (5, 5) exists", () { final simulator = AutonomySimulator(); - final destination = (lat: 5, long: 5).toGps(); + final destination = (base + UTMCoordinates(x: 5, y: 5, zoneNumber: 1)).toGps(); simulator.logger.info("Each step is ${GpsUtils.northMeters.toGps().latitude.toStringAsFixed(5)}"); simulator.logger.info("Going to ${destination.prettyPrint()}"); - simulator.pathfinder = RoverPathfinder(collection: simulator); + simulator.pathfinder = RoverPathfinder(collection: simulator); + simulator.gps.update(base.toGps()); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); }); test("Small paths are efficient", () { - const oldError = Constants.maxErrorMeters; - GpsUtils.maxErrorMeters = 1; final simulator = AutonomySimulator(); // Plan a path from (0, 0) to (5, 5) simulator.pathfinder = RoverPathfinder(collection: simulator); - final destination = (lat: 5, long: 5).toGps(); + final destination = (base + UTMCoordinates(x: 5, y: 5, zoneNumber: 1)).toGps(); simulator.logger.info("Going to ${destination.prettyPrint()}"); + simulator.gps.update(base.toGps()); final path = simulator.pathfinder.getPath(destination); - expect(path, isNotNull); if (path == null) return; + expect(path, isNotNull); + + if (path == null) return; var turnCount = 0; for (final step in path) { @@ -54,32 +50,16 @@ void main() => group("[Pathfinding]", tags: ["path"], () { // start + quarter turn left + 7 forward = 8 steps expect(turnCount, 1); expect(path.length, 7); - - GpsUtils.maxErrorMeters = oldError; - }); - - test("Following path gets to the end", () async { - final simulator = AutonomySimulator(); - final destination = (lat: 5, long: 5).toGps(); - simulator.pathfinder = RoverPathfinder(collection: simulator); - final path = simulator.pathfinder.getPath(destination); - - expect(path, isNotNull); if (path == null) return; - expect(simulator.gps.isNear(GpsCoordinates()), isTrue); - - await simulator.drive.followPath(path); - expect(simulator.gps.isNear(destination), isTrue); - - await simulator.dispose(); }); test("Avoid obstacles but reach the goal", () async { // Logger.level = LogLevel.all; final simulator = AutonomySimulator(); - final destination = (lat: 5, long: 0).toGps(); + final destination = (base + UTMCoordinates(y: 5, x: 0, zoneNumber: 1)).toGps(); simulator.pathfinder = RoverPathfinder(collection: simulator); - simulator.pathfinder.recordObstacle((lat: 1, long: 0).toGps()); - simulator.pathfinder.recordObstacle((lat: 2, long: 0).toGps()); + simulator.pathfinder.recordObstacle((base + UTMCoordinates(y: 1, x: 0, zoneNumber: 1)).toGps()); + simulator.pathfinder.recordObstacle((base + UTMCoordinates(y: 2, x: 0, zoneNumber: 1)).toGps()); + simulator.gps.update(base.toGps()); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); if (path == null) { @@ -91,41 +71,34 @@ void main() => group("[Pathfinding]", tags: ["path"], () { expect(simulator.pathfinder.isObstacle(step.position), isFalse); } expect(path.length, 10, reason: "1 turn + 1 forward + 1 turn + 4 forward + 1 45 degree turn + 1 forward + 1 stop = 10 steps total"); - await simulator.drive.followPath(path); - expect(simulator.gps.isNear(destination), isTrue); - await simulator.dispose(); }); test("Stress test", () async { - final oldError = GpsUtils.maxErrorMeters; - final oldMoveLength = GpsUtils.moveLengthMeters; - GpsUtils.maxErrorMeters = 1; - GpsUtils.moveLengthMeters = 1; final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.logger.trace("Starting from ${simulator.gps.coordinates.prettyPrint()}"); - simulator.logger.trace("Each step is +/- ${GpsUtils.north.prettyPrint()}"); final destination = (lat: 1000, long: 1000).toGps(); simulator.logger.info("Going to ${destination.prettyPrint()}"); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); await simulator.dispose(); - GpsUtils.maxErrorMeters = oldError; - GpsUtils.moveLengthMeters = oldMoveLength; }); test("Impossible paths are reported", () async { final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); - final destination = (lat: 5, long: 5).toGps(); + final destination = UTMCoordinates(x: 10, y: 10, zoneNumber: 31).toGps(); final obstacles = { - (lat: 1, long: -1).toGps(), (lat: 1, long: 0).toGps(), (lat: 1, long: 1).toGps(), - (lat: 0, long: -1).toGps(), /* Rover */ (lat: 0, long: 1).toGps(), - (lat: -1, long: -1).toGps(), (lat: -1, long: 0).toGps(), (lat: -1, long: 1).toGps(), + // dart format off + (base + UTMCoordinates(y: 1, x: -1, zoneNumber: 1)).toGps(), (base + UTMCoordinates(y: 1, x: 0, zoneNumber: 1)).toGps(), (base + UTMCoordinates(y: 1, x: 1, zoneNumber: 1)).toGps(), + (base + UTMCoordinates(y: 0, x: -1, zoneNumber: 1)).toGps(), /* Rover */ (base + UTMCoordinates(y: 0, x: 1, zoneNumber: 1)).toGps(), + (base + UTMCoordinates(y: -1, x: -1, zoneNumber: 1)).toGps(), (base + UTMCoordinates(y: -1, x: 0, zoneNumber: 1)).toGps(), (base + UTMCoordinates(y: -1, x: 1, zoneNumber: 1)).toGps(), + // dart format on }; for (final obstacle in obstacles) { simulator.pathfinder.recordObstacle(obstacle); } + simulator.gps.update(base.toGps()); final path = simulator.pathfinder.getPath(destination); expect(path, isNull); await simulator.dispose(); @@ -135,7 +108,8 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("path chooses to move diagonally", () async { final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); - final destination = (lat: 5, long: 5).toGps(); + final destination = (base + UTMCoordinates(x: 5, y: 5, zoneNumber: 1)).toGps(); + simulator.gps.update(base.toGps()); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); expect(path!.where((state) => state.instruction == DriveDirection.forward).length, 5); @@ -146,14 +120,17 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("doesn't drive through an obstacle", () async { final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); - final destination = (lat: 5, long: 5).toGps(); + final destination = UTMCoordinates(x: 10, y: 10, zoneNumber: 31).toGps(); final obstacles = { - (lat: 1, long: 0).toGps(), /* Destination */ - /* Rover */ (lat: 0, long: 1).toGps(), + // dart format off + (base + UTMCoordinates(y: 1, x: 0, zoneNumber: 1)).toGps(), /* Destination */ + /* Rover */ (base + UTMCoordinates(y: 0, x: 1, zoneNumber: 1)).toGps(), + // dart format on }; for (final obstacle in obstacles) { simulator.pathfinder.recordObstacle(obstacle); } + simulator.gps.update(base.toGps()); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); expect(path!.where((state) => state.instruction == DriveDirection.forward).length, greaterThan(2)); @@ -163,14 +140,15 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("doesn't drive through an obstacle", () async { final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); - final destination = (lat: 5, long: 5).toGps(); + final destination = UTMCoordinates(x: 10, y: 10, zoneNumber: 31).toGps(); final obstacles = { - (lat: 1, long: 0).toGps(), /* Destination */ - /* Rover */ + base.toGps(), /* Destination */ + /* Rover */ }; for (final obstacle in obstacles) { simulator.pathfinder.recordObstacle(obstacle); } + simulator.gps.update(base.toGps()); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); expect(path!.where((state) => state.instruction == DriveDirection.forward).length, greaterThan(1)); @@ -186,16 +164,16 @@ void main() => group("[Pathfinding]", tags: ["path"], () { final originalPath = [ AutonomyAStarState( - position: (lat: 0, long: 0).toGps(), - goal: (lat: 0, long: 0).toGps(), + position: UTMCoordinates(x: 0, y: 0, zoneNumber: 31).toGps(), + goal: UTMCoordinates(x: 0, y: 0, zoneNumber: 31).toGps(), collection: simulator, instruction: DriveDirection.quarterLeft, orientation: CardinalDirection.northEast, depth: sqrt2, ), AutonomyAStarState( - position: (lat: 0, long: 0).toGps(), - goal: (lat: 0, long: 0).toGps(), + position: UTMCoordinates(x: 0, y: 0, zoneNumber: 31).toGps(), + goal: UTMCoordinates(x: 0, y: 0, zoneNumber: 31).toGps(), collection: simulator, instruction: DriveDirection.quarterLeft, orientation: CardinalDirection.east, @@ -216,16 +194,16 @@ void main() => group("[Pathfinding]", tags: ["path"], () { final originalPath = [ AutonomyAStarState( - position: (lat: 0, long: 0).toGps(), - goal: (lat: 0, long: 0).toGps(), + position: UTMCoordinates(x: 0, y: 0, zoneNumber: 31).toGps(), + goal: UTMCoordinates(x: 0, y: 0, zoneNumber: 31).toGps(), collection: simulator, instruction: DriveDirection.quarterLeft, orientation: CardinalDirection.northEast, depth: sqrt2, ), AutonomyAStarState( - position: (lat: 0, long: 0).toGps(), - goal: (lat: 0, long: 0).toGps(), + position: UTMCoordinates(x: 0, y: 0, zoneNumber: 31).toGps(), + goal: UTMCoordinates(x: 0, y: 0, zoneNumber: 31).toGps(), collection: simulator, instruction: DriveDirection.quarterRight, orientation: CardinalDirection.north, diff --git a/test/rover_test.dart b/test/rover_test.dart index 2563f64..ed7cff6 100644 --- a/test/rover_test.dart +++ b/test/rover_test.dart @@ -1,6 +1,5 @@ import "package:test/test.dart"; -import "package:burt_network/protobuf.dart"; import "package:burt_network/logging.dart"; import "package:autonomy/interfaces.dart"; From 37219be9dbe5297c6820f06731c3ee111eaf93e6 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 25 May 2025 20:00:01 -0400 Subject: [PATCH 075/110] Add FSM tests --- dart_test.yaml | 1 + lib/src/fsm/fsm_controller.dart | 4 + test/fsm_test.dart | 178 ++++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 test/fsm_test.dart diff --git a/dart_test.yaml b/dart_test.yaml index 4bbaf05..acd943d 100644 --- a/dart_test.yaml +++ b/dart_test.yaml @@ -5,3 +5,4 @@ tags: simulator: orchestrator: network: + fsm: diff --git a/lib/src/fsm/fsm_controller.dart b/lib/src/fsm/fsm_controller.dart index b0fe42d..6532bee 100644 --- a/lib/src/fsm/fsm_controller.dart +++ b/lib/src/fsm/fsm_controller.dart @@ -1,4 +1,5 @@ import "package:autonomy/src/fsm/rover_fsm.dart"; +import "package:meta/meta.dart"; /// Class to control a Finite State Machine, handling state pushes, /// pops, and transitions @@ -12,6 +13,9 @@ import "package:autonomy/src/fsm/rover_fsm.dart"; /// /// When a state is popped, it is removed from the top of the stack, and will be exited. class FSMController { + @visibleForTesting + List get stack => _stateStack; + final List _stateStack = []; /// Pushes a new state to the top of the stack, and enters it diff --git a/test/fsm_test.dart b/test/fsm_test.dart new file mode 100644 index 0000000..36a1257 --- /dev/null +++ b/test/fsm_test.dart @@ -0,0 +1,178 @@ +import "package:autonomy/src/fsm/rover_fsm.dart"; +import "package:test/test.dart"; + +class TrackingState extends RoverState { + int enterCount = 0; + int updateCount = 0; + int exitCount = 0; + + TrackingState(super.controller); + + @override + void enter() => enterCount++; + + @override + void update() => updateCount++; + + @override + void exit() => exitCount++; +} + +void main() => group("[State Machine]", tags: ["fsm"], () { + final controller = FSMController(); + + test("Pushing and popping", () { + final state = TrackingState(controller); + + controller.pushState(state); + + expect(state.enterCount, 1); + controller.update(); + expect(state.updateCount, 1); + controller.popState(); + expect(state.exitCount, 1); + }); + + test("Pushing multiple", () { + final state1 = TrackingState(controller); + final state2 = TrackingState(controller); + + controller.pushState(state1); + + expect(state1.enterCount, 1); + controller.update(); + expect(state1.updateCount, 1); + + controller.pushState(state2); + + expect(state1.exitCount, 0); + + expect(state2.enterCount, 1); + controller.update(); + expect(state2.updateCount, 1); + controller.popState(); + expect(state2.exitCount, 1); + expect(state1.exitCount, 0); + + controller.popState(); + + expect(state1.exitCount, 1); + }); + + test("Sequence", () { + final state1 = TrackingState(controller); + final state2 = TrackingState(controller); + final sequence = SequenceState(controller, steps: [state1, state2]); + + controller.pushState(sequence); + + // State 1 gets pushed, state 2 remains idle + expect(state1.enterCount, 1); + expect(state2.enterCount, 0); + + controller.update(); + + // State 1 gets updated + expect(state1.updateCount, 1); + expect(state2.updateCount, 0); + + controller.popState(); + + // State 1 exits, state 2 doesn't enter until next update + expect(state1.exitCount, 1); + expect(state2.enterCount, 0); + + controller.update(); + + // State 2 enters, doesn't update until next controller update + expect(state1.updateCount, 1); + expect(state2.enterCount, 1); + expect(state2.updateCount, 0); + + controller.update(); + + expect(state2.updateCount, 1); + }); + + test("Timeout", () async { + const timeoutDuration = Duration(milliseconds: 10); + + var timedOut = false; + + final state1 = TrackingState(controller); + final timeout = TimeoutDecorator( + child: state1, + timeout: timeoutDuration, + onTimeout: (_) { + timedOut = true; + }, + ); + + controller.pushState(timeout); + controller.update(); + expect(timedOut, false); + + await Future.delayed(timeoutDuration); + + // Doesn't time out until next update + expect(timedOut, false); + controller.update(); + expect(timedOut, true); + }); + + test("Delayed State", () async { + const delayTime = Duration(milliseconds: 10); + + final state1 = TrackingState(controller); + final state2 = TrackingState(controller); + final sequence = SequenceState( + controller, + steps: [state1, DelayedState(controller, delayTime: delayTime), state2], + ); + + controller.pushState(sequence); + + expect(state1.enterCount, 1); + expect(state2.enterCount, 0); + + controller.update(); + + expect(state1.updateCount, 1); + expect(state2.updateCount, 0); + + controller.popState(); + + expect(state1.exitCount, 1); + expect(state2.enterCount, 0); + + controller.update(); + + expect(state1.updateCount, 1); + expect(state2.enterCount, 0); + expect(state2.updateCount, 0); + + controller.update(); + + expect(state1.updateCount, 1); + expect(state2.enterCount, 0); + expect(state2.updateCount, 0); + + await Future.delayed(delayTime); + + controller.update(); + + expect(state1.updateCount, 1); + expect(state2.enterCount, 0); + expect(state2.updateCount, 0); + + controller.update(); + + expect(state1.updateCount, 1); + expect(state2.enterCount, 1); + expect(state2.updateCount, 0); + + controller.update(); + + expect(state2.updateCount, 1); + }); +}); From 4592c34c0258a42a430d7185112534e4566cad98 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 25 May 2025 22:40:39 -0400 Subject: [PATCH 076/110] Fix multiple paths being generated at once --- .../orchestrator/orchestrator_interface.dart | 4 +- lib/src/orchestrator/rover_orchestrator.dart | 9 ++- lib/src/utils/periodic_timer.dart | 64 +++++++++++++++++++ lib/utils.dart | 1 + 4 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 lib/src/utils/periodic_timer.dart diff --git a/lib/src/orchestrator/orchestrator_interface.dart b/lib/src/orchestrator/orchestrator_interface.dart index 6afe280..fb82245 100644 --- a/lib/src/orchestrator/orchestrator_interface.dart +++ b/lib/src/orchestrator/orchestrator_interface.dart @@ -11,7 +11,7 @@ abstract class OrchestratorInterface extends Service { AutonomyCommand? currentCommand; AutonomyState currentState = AutonomyState.AUTONOMY_STATE_UNDEFINED; - Timer? behaviorTreeTimer; + PeriodicTimer? executionTimer; FSMController controller = FSMController(); @@ -64,7 +64,7 @@ abstract class OrchestratorInterface extends Service { Future abort() async { currentCommand = null; collection.logger.warning("Aborting task!"); - behaviorTreeTimer?.cancel(); + executionTimer?.cancel(); behaviorRoot.reset(); currentState = AutonomyState.ABORTING; await collection.drive.stop(); diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index efaee03..c04d65c 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -730,10 +730,13 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { ], ), ); - behaviorTreeTimer = Timer.periodic(const Duration(milliseconds: 10), ( - timer, - ) { + executionTimer = PeriodicTimer(const Duration(milliseconds: 10), (timer) { if (currentCommand == null) { + collection.logger.warning( + "Execution timer running while command is null", + body: "Canceling timer", + ); + timer.cancel(); return; } if (!controller.hasState()) { diff --git a/lib/src/utils/periodic_timer.dart b/lib/src/utils/periodic_timer.dart new file mode 100644 index 0000000..efaecf4 --- /dev/null +++ b/lib/src/utils/periodic_timer.dart @@ -0,0 +1,64 @@ +import "dart:async"; + +/// An alternative to [Timer.periodic] that ensures its callback is only being run once. +/// +/// Using [Timer.periodic] can cause your function to run in parallel with itself. For example: +/// +/// ```dart +/// void main() => Timer.periodic(Duration(seconds: 1), twoSecondDelay); +/// +/// void twoSecondDelay(_) async { +/// final id = Random().nextInt(100); +/// print("Starting task $id"); +/// await Future.delayed(Duration(seconds: 2)); +/// print("Finished tick $id"); +/// } +/// ``` +/// +/// Running the above code will cause `twoSecondDelay` to be started before the previous call has +/// even finished. If your function uses a handle to a blocking resource, then the second call will +/// crash or stall while the first one is still running. This class ensures that each call to +/// [function] finishes running before the next one begins, while still making sure that they +/// are called approximately every [interval]. +class PeriodicTimer { + /// The interval at which to run [function]. + final Duration interval; + + /// The function to run. Can be synchronous or asynchronous. + final FutureOr Function(PeriodicTimer timer) function; + + /// A timer that runs the next [_tick] at exactly the right time. + Timer? timer; + + /// Whether this timer is currently running. + bool isRunning = true; + + /// Creates a periodic timer and starts the next tick asynchronously. + PeriodicTimer(this.interval, this.function) { + Timer.run(_tick); + } + + /// Calls [function] once and ensures it is called again exactly after [interval]. + /// + /// This function calls [function] and measures how long it takes to run. Afterwards, + /// it waits a delay so that the delay plus the elapsed time equals [interval]. + Future _tick() async { + final startTime = DateTime.timestamp(); + await function(this); + final delay = interval - DateTime.timestamp().difference(startTime); + if (!isRunning) return; + timer = Timer(delay, _tick); + } + + /// Cancels the timer. + void cancel() { + isRunning = false; + timer?.cancel(); + } + + /// Restarts the timer and begins the next tick asynchronously. + void restart() { + isRunning = true; + Timer.run(_tick); + } +} diff --git a/lib/utils.dart b/lib/utils.dart index 4829620..813bc37 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -3,6 +3,7 @@ export "src/utils/corrector.dart"; export "src/utils/error.dart"; export "src/utils/gps_utils.dart"; export "src/utils/imu_utils.dart"; +export "src/utils/periodic_timer.dart"; export "src/utils/receiver.dart"; export "src/utils/reporter.dart"; From 09c7f5bc6811aa7f15419ca005b7e6630db647ce Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 00:12:48 -0400 Subject: [PATCH 077/110] Clear controller states on command ending --- lib/src/fsm/fsm_controller.dart | 7 +++++++ lib/src/orchestrator/orchestrator_interface.dart | 15 +++++++++++---- lib/src/orchestrator/rover_orchestrator.dart | 12 +++++++++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/src/fsm/fsm_controller.dart b/lib/src/fsm/fsm_controller.dart index 6532bee..752b55f 100644 --- a/lib/src/fsm/fsm_controller.dart +++ b/lib/src/fsm/fsm_controller.dart @@ -31,6 +31,13 @@ class FSMController { state.enter(); } + /// Clears all states from the stack + void clearStates() { + for (var i = 0; i < _stateStack.length; i++) { + popState(); + } + } + /// Pops the latest state from the top of the stack void popState() { _stateStack.removeLast().exit(); diff --git a/lib/src/orchestrator/orchestrator_interface.dart b/lib/src/orchestrator/orchestrator_interface.dart index fb82245..e4d9620 100644 --- a/lib/src/orchestrator/orchestrator_interface.dart +++ b/lib/src/orchestrator/orchestrator_interface.dart @@ -61,13 +61,19 @@ abstract class OrchestratorInterface extends Service { } @mustCallSuper - Future abort() async { + void onCommandEnd() { currentCommand = null; - collection.logger.warning("Aborting task!"); executionTimer?.cancel(); - behaviorRoot.reset(); + executionTimer = null; + controller.clearStates(); + collection.drive.stop(); + } + + @mustCallSuper + Future abort() async { + collection.logger.warning("Aborting task!"); + onCommandEnd(); currentState = AutonomyState.ABORTING; - await collection.drive.stop(); // await collection.dispose(); // await collection.init(); // exit(1); @@ -77,5 +83,6 @@ abstract class OrchestratorInterface extends Service { void handleArucoTask(AutonomyCommand command); void handleHammerTask(AutonomyCommand command); void handleBottleTask(AutonomyCommand command); + AutonomyData get statusMessage; } diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index c04d65c..5aaae10 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -275,6 +275,12 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { return super.abort(); } + @override + void onCommandEnd() { + currentPath = null; + super.onCommandEnd(); + } + @override AutonomyData get statusMessage => AutonomyData( destination: currentCommand?.destination, @@ -736,22 +742,22 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { "Execution timer running while command is null", body: "Canceling timer", ); + onCommandEnd(); timer.cancel(); return; } if (!controller.hasState()) { currentState = AutonomyState.NO_SOLUTION; - currentCommand = null; + onCommandEnd(); timer.cancel(); return; } if (collection.gps.isNear(destination, Constants.maxErrorMeters)) { timer.cancel(); collection.logger.info("Task complete"); + onCommandEnd(); currentState = AutonomyState.AT_DESTINATION; collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); - collection.drive.stop(); - currentCommand = null; return; } controller.update(); From 1563e5e3807d7791d9304839caffc3b1b1ff6563 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 13:02:52 -0400 Subject: [PATCH 078/110] Improved obstacle avoidance and fixed some unit tests --- lib/constants.dart | 17 ++++- .../pathfinding/pathfinding_interface.dart | 11 +++- lib/src/utils/a_star.dart | 64 ++++--------------- lib/src/utils/gps_utils.dart | 5 +- test/network_test.dart | 15 ++--- test/orchestrator_test.dart | 2 +- test/path_test.dart | 49 +++++++------- test/sensor_test.dart | 9 +-- test/test_util.dart | 10 +++ 9 files changed, 85 insertions(+), 97 deletions(-) create mode 100644 test/test_util.dart diff --git a/lib/constants.dart b/lib/constants.dart index a712176..3cafaad 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,9 +1,21 @@ import "package:autonomy/autonomy.dart"; +/// Constant variables for Autonomy. +/// +/// These constants control the tolerances and behaviors of the +/// pathfinding and path following, such as move lengths, maximum +/// error distance allowed, timeouts, etc. class Constants { + /// Private internal constructor for constants + Constants._(); + /// The maximum error or "tolerance" for reaching the end goal static const double maxErrorMeters = 1; + /// The closest distance the pathfinding algorithm will allow + /// the rover to go near an obstacle + static const double obstacleAvoidanceRadius = 0.5; + /// How close the rover should get to a drive coordinate before /// continuing with the path static const double intermediateStepTolerance = 0.25; @@ -25,7 +37,10 @@ class Constants { /// /// Only applies to individual "drive forward" steps, to prevent indefinite driving /// if it never reaches within [maxErrorMeters] of its desired position. - static const Duration driveGPSTimeout = Duration(seconds: 4, milliseconds: 500); + static const Duration driveGPSTimeout = Duration( + seconds: 4, + milliseconds: 500, + ); /// The maximum time to spend searching for an aruco tag static const Duration arucoSearchTimeout = Duration(seconds: 20); diff --git a/lib/src/pathfinding/pathfinding_interface.dart b/lib/src/pathfinding/pathfinding_interface.dart index 42671a2..c234171 100644 --- a/lib/src/pathfinding/pathfinding_interface.dart +++ b/lib/src/pathfinding/pathfinding_interface.dart @@ -1,3 +1,4 @@ +import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; abstract class PathfindingInterface extends Service { @@ -19,8 +20,14 @@ abstract class PathfindingInterface extends Service { } bool isObstacle(GpsCoordinates coordinates) => - obstacles.any((obstacle) => obstacle.isNear(coordinates)) || - _lockedObstacles.any((obstacle) => obstacle.isNear(coordinates)); + obstacles.any( + (obstacle) => + obstacle.isNear(coordinates, Constants.obstacleAvoidanceRadius), + ) || + _lockedObstacles.any( + (obstacle) => + obstacle.isNear(coordinates, Constants.obstacleAvoidanceRadius), + ); @override Future dispose() async { diff --git a/lib/src/utils/a_star.dart b/lib/src/utils/a_star.dart index dd0de0c..eaa7ebb 100644 --- a/lib/src/utils/a_star.dart +++ b/lib/src/utils/a_star.dart @@ -88,70 +88,32 @@ class AutonomyAStarState extends AStarState { /// Returns whether or not the rover will drive between or right next to an obstacle diagonally
///
- /// Case 1:
+ /// Example 1:
/// 0 X
/// X R
/// Assuming the rover is facing 0 and trying to drive forward, will return false
///
- /// Case 2:
- /// 0 X
+ /// Example 2:
+ /// 0 0
/// X R
- /// Assuming the rover is facing north and trying to turn 45 degrees left, will return false
- ///
- /// Case 3:
- /// 0 X
- /// 0 R
- /// If the rover is facing left but trying to turn 45 degrees right, will return false
+ /// Assuming the rover is facing 0 and trying to drive forward, will return false
///
- /// Case 4:
- /// 0 X 0
- /// 0 R 0
- /// If the rover is facing northeast to 0 and trying to turn left, will return false bool willDriveThroughObstacle(AutonomyAStarState state) { + // Can't hit an obstacle while turning final isTurn = state.instruction != DriveDirection.forward; - final isQuarterTurn = state.instruction == DriveDirection.quarterLeft || state.instruction == DriveDirection.quarterRight; - - if ( - // Can't hit an obstacle while turning - state.instruction != DriveDirection.forward - - // Forward drive across the perpendicular axis - || (!isTurn && state.orientation.isPerpendicular) - - // Not encountering any sort of diagonal angle - || (isTurn && isQuarterTurn && state.orientation.isPerpendicular) - - // No diagonal movement, won't drive between obstacles - || (!isQuarterTurn && orientation.isPerpendicular) - ) { + // Forward drive across the perpendicular axis + final isPerpendicular = state.orientation.isPerpendicular; + if (isTurn || isPerpendicular) { return false; } - final CardinalDirection orientation1; - final CardinalDirection orientation2; - - // Case 1, trying to drive while facing a 45 degree angle - if (!isTurn) { - orientation1 = state.orientation.turnQuarterLeft(); - orientation2 = state.orientation.turnQuarterRight(); - } else if (isQuarterTurn) { // Case 2 and Case 3 - orientation1 = orientation; - orientation2 = (state.instruction == DriveDirection.quarterLeft) - ? orientation1.turnLeft() - : orientation1.turnRight(); - } else { // Case 4 - orientation1 = (state.instruction == DriveDirection.left) - ? orientation.turnQuarterLeft() - : orientation.turnQuarterRight(); - orientation2 = (state.instruction == DriveDirection.left) - ? state.orientation.turnQuarterLeft() - : state.orientation.turnQuarterRight(); - } + final orientation1 = state.orientation.turnQuarterLeft(); + final orientation2 = state.orientation.turnQuarterRight(); // Since the state being passed has a position of moving after the - // turn, we have to check the position of where it started - return collection.pathfinder.isObstacle(position.goForward(orientation1)) - || collection.pathfinder.isObstacle(position.goForward(orientation2)); + // turn, we have to check the position of where it started (our current position) + return collection.pathfinder.isObstacle(position.goForward(orientation1)) || + collection.pathfinder.isObstacle(position.goForward(orientation2)); } bool isValidState(AutonomyAStarState state) => diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index bb7a2d4..41f0b18 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -93,10 +93,7 @@ extension GpsUtils on GpsCoordinates { return distanceTo(other) < tolerance; } - GpsCoordinates operator +(GpsCoordinates other) => GpsCoordinates( - latitude: latitude + other.latitude, - longitude: longitude + other.longitude, - ); + GpsCoordinates operator +(GpsCoordinates other) => (toUTM() + other.toUTM()).toGps(); GpsCoordinates operator -(GpsCoordinates other) => GpsCoordinates( latitude: latitude - other.latitude, diff --git a/test/network_test.dart b/test/network_test.dart index 853f6a7..fd7588f 100644 --- a/test/network_test.dart +++ b/test/network_test.dart @@ -4,9 +4,10 @@ import "dart:io"; import "package:autonomy/autonomy.dart"; import "package:autonomy/src/drive/drive_config.dart"; import "package:burt_network/burt_network.dart"; -import "package:coordinate_converter/coordinate_converter.dart"; import "package:test/test.dart"; +import "test_util.dart"; + class MockSubsystems extends Service { final socket = RoverSocket( device: Device.AUTONOMY, @@ -121,14 +122,12 @@ void main() => group("[Network]", tags: ["network"], () { ); await simulator.init(); - final origin = UTMCoordinates(x: 5, y: 5, zoneNumber: 31); - final oneMeter = - (origin + UTMCoordinates(x: 0, y: 1, zoneNumber: 1)).toGps(); - simulator.gps.update(origin.toGps()); + final oneMeter = origin.plus(x: 0, y: 1); + simulator.gps.update(origin); expect(subsystems.throttle, 0); expect(subsystems.left, 0); expect(subsystems.right, 0); - expect(simulator.gps.isNear(origin.toGps()), isTrue); + expect(simulator.gps.isNear(origin), isTrue); expect(simulator.gps.isNear(oneMeter), isFalse); expect(subsystems.throttleFlag, isFalse); @@ -147,7 +146,7 @@ void main() => group("[Network]", tags: ["network"], () { expect(subsystems.throttle, isNot(0)); expect(subsystems.left, isNot(0)); expect(subsystems.right, isNot(0)); - expect(simulator.gps.isNear(origin.toGps()), isTrue); + expect(simulator.gps.isNear(origin), isTrue); expect(simulator.gps.isNear(oneMeter), isFalse); simulator.orchestrator.controller.update(); @@ -171,7 +170,7 @@ void main() => group("[Network]", tags: ["network"], () { simulator.orchestrator.controller.update(); simulator.orchestrator.controller.update(); - expect(simulator.gps.isNear(origin.toGps(), 0.5), isFalse); + expect(simulator.gps.isNear(origin, 0.5), isFalse); expect(simulator.gps.isNear(oneMeter), isTrue); subsystems.enabled = false; diff --git a/test/orchestrator_test.dart b/test/orchestrator_test.dart index f8419b5..5250d53 100644 --- a/test/orchestrator_test.dart +++ b/test/orchestrator_test.dart @@ -5,7 +5,7 @@ import "package:test/test.dart"; import "package:autonomy/autonomy.dart"; import "package:burt_network/burt_network.dart"; -void main() => group("[Orchestrator]", tags: ["orchestrator"], () { +void main() => group("[Orchestrator]", skip: true, tags: ["orchestrator"], () { setUp(() => Logger.level = LogLevel.info); tearDown(() => Logger.level = LogLevel.off); diff --git a/test/path_test.dart b/test/path_test.dart index 4a3e701..2bb801b 100644 --- a/test/path_test.dart +++ b/test/path_test.dart @@ -1,5 +1,6 @@ import "dart:math"; +import "package:autonomy/constants.dart"; import "package:burt_network/burt_network.dart"; import "package:coordinate_converter/coordinate_converter.dart"; import "package:test/test.dart"; @@ -8,19 +9,19 @@ import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; import "package:autonomy/simulator.dart"; -final UTMCoordinates base = UTMCoordinates(x: 5, y: 5, zoneNumber: 31); +import "test_util.dart"; void main() => group("[Pathfinding]", tags: ["path"], () { - setUp(() => Logger.level = LogLevel.info); + setUp(() => Logger.level = LogLevel.off); tearDown(() => Logger.level = LogLevel.off); test("Simple path from (0, 0) to (5, 5) exists", () { final simulator = AutonomySimulator(); - final destination = (base + UTMCoordinates(x: 5, y: 5, zoneNumber: 1)).toGps(); + final destination = origin.plus(x: 5, y: 5); simulator.logger.info("Each step is ${GpsUtils.northMeters.toGps().latitude.toStringAsFixed(5)}"); simulator.logger.info("Going to ${destination.prettyPrint()}"); simulator.pathfinder = RoverPathfinder(collection: simulator); - simulator.gps.update(base.toGps()); + simulator.gps.update(origin); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); }); @@ -30,9 +31,9 @@ void main() => group("[Pathfinding]", tags: ["path"], () { // Plan a path from (0, 0) to (5, 5) simulator.pathfinder = RoverPathfinder(collection: simulator); - final destination = (base + UTMCoordinates(x: 5, y: 5, zoneNumber: 1)).toGps(); + final destination = origin.plus(x: 5, y: 5); simulator.logger.info("Going to ${destination.prettyPrint()}"); - simulator.gps.update(base.toGps()); + simulator.gps.update(origin); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); @@ -55,11 +56,11 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("Avoid obstacles but reach the goal", () async { // Logger.level = LogLevel.all; final simulator = AutonomySimulator(); - final destination = (base + UTMCoordinates(y: 5, x: 0, zoneNumber: 1)).toGps(); + final destination = origin.plus(x: 0, y: 5); simulator.pathfinder = RoverPathfinder(collection: simulator); - simulator.pathfinder.recordObstacle((base + UTMCoordinates(y: 1, x: 0, zoneNumber: 1)).toGps()); - simulator.pathfinder.recordObstacle((base + UTMCoordinates(y: 2, x: 0, zoneNumber: 1)).toGps()); - simulator.gps.update(base.toGps()); + simulator.pathfinder.recordObstacle(origin.plus(x: 0, y: 1)); + simulator.pathfinder.recordObstacle(origin.plus(x: 0, y: 2)); + simulator.gps.update(origin); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); if (path == null) { @@ -70,15 +71,17 @@ void main() => group("[Pathfinding]", tags: ["path"], () { simulator.logger.trace(step.toString()); expect(simulator.pathfinder.isObstacle(step.position), isFalse); } - expect(path.length, 10, reason: "1 turn + 1 forward + 1 turn + 4 forward + 1 45 degree turn + 1 forward + 1 stop = 10 steps total"); + expect(path.length, 9, reason: "1 turn + 1 forward + 1 turn + 5 forward + 1 stop = 9 steps total"); + expect(path.last.position.distanceTo(destination), lessThan(Constants.maxErrorMeters)); }); test("Stress test", () async { final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.logger.trace("Starting from ${simulator.gps.coordinates.prettyPrint()}"); - final destination = (lat: 1000, long: 1000).toGps(); + final destination = origin.plus(x: 1000, y: 1000); simulator.logger.info("Going to ${destination.prettyPrint()}"); + simulator.gps.update(origin); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); await simulator.dispose(); @@ -90,15 +93,15 @@ void main() => group("[Pathfinding]", tags: ["path"], () { final destination = UTMCoordinates(x: 10, y: 10, zoneNumber: 31).toGps(); final obstacles = { // dart format off - (base + UTMCoordinates(y: 1, x: -1, zoneNumber: 1)).toGps(), (base + UTMCoordinates(y: 1, x: 0, zoneNumber: 1)).toGps(), (base + UTMCoordinates(y: 1, x: 1, zoneNumber: 1)).toGps(), - (base + UTMCoordinates(y: 0, x: -1, zoneNumber: 1)).toGps(), /* Rover */ (base + UTMCoordinates(y: 0, x: 1, zoneNumber: 1)).toGps(), - (base + UTMCoordinates(y: -1, x: -1, zoneNumber: 1)).toGps(), (base + UTMCoordinates(y: -1, x: 0, zoneNumber: 1)).toGps(), (base + UTMCoordinates(y: -1, x: 1, zoneNumber: 1)).toGps(), + origin.plus(x: -1, y: 1), origin.plus(x: 0, y: 1), origin.plus(x: 1, y: 1), + origin.plus(x: -1, y: 0), /* Rover */ origin.plus(x: 1, y: 0), + origin.plus(x: -1, y: -1), origin.plus(x: 0, y: -1), origin.plus(x: 1, y: -1), // dart format on }; for (final obstacle in obstacles) { simulator.pathfinder.recordObstacle(obstacle); } - simulator.gps.update(base.toGps()); + simulator.gps.update(origin); final path = simulator.pathfinder.getPath(destination); expect(path, isNull); await simulator.dispose(); @@ -108,8 +111,8 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("path chooses to move diagonally", () async { final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); - final destination = (base + UTMCoordinates(x: 5, y: 5, zoneNumber: 1)).toGps(); - simulator.gps.update(base.toGps()); + final destination = origin.plus(x: 5, y: 5); + simulator.gps.update(origin); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); expect(path!.where((state) => state.instruction == DriveDirection.forward).length, 5); @@ -123,14 +126,14 @@ void main() => group("[Pathfinding]", tags: ["path"], () { final destination = UTMCoordinates(x: 10, y: 10, zoneNumber: 31).toGps(); final obstacles = { // dart format off - (base + UTMCoordinates(y: 1, x: 0, zoneNumber: 1)).toGps(), /* Destination */ - /* Rover */ (base + UTMCoordinates(y: 0, x: 1, zoneNumber: 1)).toGps(), + origin.plus(x: 0, y: 1), /* Destination */ + /* Rover */ origin.plus(x: 1, y: 0), // dart format on }; for (final obstacle in obstacles) { simulator.pathfinder.recordObstacle(obstacle); } - simulator.gps.update(base.toGps()); + simulator.gps.update(origin); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); expect(path!.where((state) => state.instruction == DriveDirection.forward).length, greaterThan(2)); @@ -142,13 +145,13 @@ void main() => group("[Pathfinding]", tags: ["path"], () { simulator.pathfinder = RoverPathfinder(collection: simulator); final destination = UTMCoordinates(x: 10, y: 10, zoneNumber: 31).toGps(); final obstacles = { - base.toGps(), /* Destination */ + origin, /* Destination */ /* Rover */ }; for (final obstacle in obstacles) { simulator.pathfinder.recordObstacle(obstacle); } - simulator.gps.update(base.toGps()); + simulator.gps.update(origin); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); expect(path!.where((state) => state.instruction == DriveDirection.forward).length, greaterThan(1)); diff --git a/test/sensor_test.dart b/test/sensor_test.dart index 0f37ff8..91c0af3 100644 --- a/test/sensor_test.dart +++ b/test/sensor_test.dart @@ -1,12 +1,7 @@ import "package:autonomy/autonomy.dart"; +import "package:burt_network/burt_network.dart"; import "package:test/test.dart"; -import "package:burt_network/protobuf.dart"; - -import "package:autonomy/interfaces.dart"; -import "package:autonomy/simulator.dart"; -import "package:autonomy/src/rover/gps.dart"; - const imuError = 2.5; const gpsPrecision = 7; @@ -243,5 +238,5 @@ void main() => group("[Sensors]", tags: ["sensors"], () { // simulator.logger.info("Unaffected orientation: ${realImu.heading}"); // expect(realImu.isNear(orientation.heading), isTrue); // await simulator.dispose(); - // }); + }); }); diff --git a/test/test_util.dart b/test/test_util.dart new file mode 100644 index 0000000..506f222 --- /dev/null +++ b/test/test_util.dart @@ -0,0 +1,10 @@ +import "package:autonomy/autonomy.dart"; +import "package:coordinate_converter/coordinate_converter.dart"; + +final GpsCoordinates origin = + UTMCoordinates(x: 5, y: 5, zoneNumber: 31).toGps(); + +extension GpsTransformUtil on GpsCoordinates { + GpsCoordinates plus({required double x, required double y}) => + (toUTM() + UTMCoordinates(x: x, y: y, zoneNumber: 31)).toGps(); +} From cbfd939d707d953ddc15e69efb646a1f2ca71d37 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 13:18:01 -0400 Subject: [PATCH 079/110] Replace sensor test with tests for error corrector --- dart_test.yaml | 6 +- test/corrector_test.dart | 30 +++++ test/sensor_test.dart | 242 --------------------------------------- 3 files changed, 33 insertions(+), 245 deletions(-) create mode 100644 test/corrector_test.dart delete mode 100644 test/sensor_test.dart diff --git a/dart_test.yaml b/dart_test.yaml index acd943d..c16bb01 100644 --- a/dart_test.yaml +++ b/dart_test.yaml @@ -1,6 +1,6 @@ -tags: - path: - sensors: +tags: + corrector: + path: rover: simulator: orchestrator: diff --git a/test/corrector_test.dart b/test/corrector_test.dart new file mode 100644 index 0000000..ee4c5c7 --- /dev/null +++ b/test/corrector_test.dart @@ -0,0 +1,30 @@ +import "package:autonomy/autonomy.dart"; +import "package:test/test.dart"; + +void main() => group("[Corrector]", tags: ["corrector"], () { + test("Sample Average", () { + final random = RandomError(5); + final corrector = ErrorCorrector(maxSamples: 10); + + corrector.addValue(0); + + var count = 0; + for (var i = 0; i < 1000; i++) { + corrector.addValue(random.value); + + if (corrector.calibratedValue.abs() < 1.25) count++; + } + final percentage = count / 1000; + expect(percentage, greaterThan(0.75)); + }); + + test("Rejects Outliers", () { + final corrector = ErrorCorrector(maxSamples: 10, maxDeviation: 5); + + corrector.addValue(100); + corrector.addValue(-50); + corrector.addValue(20); + + expect(corrector.calibratedValue, 100); + }); +}); diff --git a/test/sensor_test.dart b/test/sensor_test.dart deleted file mode 100644 index 91c0af3..0000000 --- a/test/sensor_test.dart +++ /dev/null @@ -1,242 +0,0 @@ -import "package:autonomy/autonomy.dart"; -import "package:burt_network/burt_network.dart"; -import "package:test/test.dart"; - -const imuError = 2.5; -const gpsPrecision = 7; - -void main() => group("[Sensors]", tags: ["sensors"], () { - setUp(() => Logger.level = LogLevel.off); - tearDown(() => Logger.level = LogLevel.off); - - test("GpsUtils.isNear", () { - final origin = (lat: 0, long: 0).toGps(); - expect(GpsCoordinates(latitude: 0, longitude: 0), origin); - expect(origin.isNear(origin), isTrue); - - final a = (lat: 5, long: 5).toGps(); - final a2 = (lat: 5, long: 5).toGps(); - final b = (lat: 5, long: 6.5).toGps(); - - expect(a.isNear(a), isTrue); - expect(a.isNear(a2), isTrue); - expect(a.isNear(b), isFalse); - - final c = GpsCoordinates(latitude: 0, longitude: 0); - final d = GpsCoordinates(latitude: 45.123, longitude: 72.123); - final e = GpsCoordinates(latitude: 12.123, longitude: 24.123); - - expect(c.isNear(d), isFalse); - expect(c.isNear(e), isFalse); - expect(d.isNear(e), isFalse); - - final f = (lat: 12, long: 12).toGps(); - final g = (lat: 12.2, long: 12.2).toGps(); - expect(f.isNear(g), isTrue); - }); - - test("GPS noise when stationary", () async { - // Set up a simulated and real GPS, both starting at (0, 0) - final oldError = GpsUtils.maxErrorMeters; - GpsUtils.maxErrorMeters = 3; - Logger.level = LogLevel.off; - - final simulator = AutonomySimulator(); - final realGps = RoverGps(collection: simulator); - final simulatedGps = GpsSimulator(collection: simulator, maxError: GpsInterface.gpsError); - final origin = GpsCoordinates(); - simulatedGps.update(origin); - realGps.update(origin); - expect(realGps.isNear(origin), isTrue); - - // Feed many noisy signals to produce a cleaner signal. - var count = 0; - for (var step = 0; step < 1000; step++) { - realGps.update(simulatedGps.coordinates); - simulator.logger.trace("New coordinate: ${realGps.coordinates.latitude.toStringAsFixed(gpsPrecision)} vs real position: ${origin.latitude.toStringAsFixed(gpsPrecision)}"); - if (step < 10) continue; - if (realGps.isNear(origin)) count++; - } - - final percentage = count / 1000; - simulator.logger.info("Final coordinates: ${realGps.coordinates.latitude}"); - expect(percentage, greaterThan(0.75), reason: "GPS should be accurate >75% of the time: $percentage"); - - // Ensure that *very* noisy readings don't affect anything. - simulator.logger.debug("Adding 100, 100"); - simulator.gps.update(GpsCoordinates(latitude: 100, longitude: 100)); - expect(realGps.isNear(origin), isTrue); - - await simulator.dispose(); - GpsUtils.maxErrorMeters = oldError; - }); - - test("IMU noise when stationary", () async { - Logger.level = LogLevel.off; - final simulator = AutonomySimulator(); - final simulatedImu = ImuSimulator(collection: simulator, maxError: imuError); - final realImu = RoverImu(collection: simulator); - final north = CardinalDirection.north; - simulatedImu.update(north.orientation); - - var count = 0; - for (var i = 0; i < 1000; i++) { - final newData = simulatedImu.raw; - realImu.update(newData); - simulator.logger.trace("Got new value: ${newData.heading}"); - simulator.logger.trace(" New heading: ${realImu.heading}"); - simulator.logger.trace(" Real position: ${north.angle}"); - if (i < 10) continue; - simulator.logger.trace(" Values are similar: ${realImu.isNear(CardinalDirection.north)}"); - if (realImu.isNear(north)) count++; - } - - final percentage = count / 1000; - simulator.logger.info("Final orientation: ${realImu.heading}"); - expect(percentage, greaterThan(0.75), reason: "IMU should be accurate >75% of the time: $percentage"); - - - realImu.forceUpdate(OrientationUtils.south); - expect(realImu.isNear(north), isTrue); - await simulator.dispose(); - }); - - test("GPS noise when moving", - skip: "GPS noise is reduced enough with RTK where filtering is not necessary", - () async { - // Set up a simulated and real GPS, both starting at (0, 0) - final oldError = GpsUtils.maxErrorMeters; - GpsUtils.maxErrorMeters = 3; - final simulator = AutonomySimulator(); - final realGps = RoverGps(collection: simulator); - final simulatedGps = GpsSimulator(collection: simulator, maxError: GpsInterface.gpsError); - var realCoordinates = GpsCoordinates(); - simulatedGps.update(realCoordinates); - realGps.forceUpdate(realCoordinates); - expect(realGps.coordinates.isNear(realCoordinates), isTrue); - - // For each step forward, use the noisy GPS to update the real GPS. - var count = 0; - for (var step = 0; step < 1000; step++) { - realCoordinates += GpsUtils.north; - simulatedGps.update(realCoordinates); - realGps.forceUpdate(simulatedGps.coordinates); - simulator.logger.trace("New coordinate: ${realGps.coordinates.latitude.toStringAsFixed(5)} vs real position: ${realCoordinates.latitude.toStringAsFixed(5)}"); - simulator.logger.trace(" Difference: ${(realGps.latitude - realCoordinates.latitude).abs().toStringAsFixed(5)} < ${GpsUtils.epsilonLatitude.toStringAsFixed(5)}"); - if (step < 10) continue; - if (realGps.isNear(realCoordinates)) count++; - } - - final percentage = count / 1000; - simulator.logger.info("Final coordinates: ${realGps.coordinates.prettyPrint()} vs actual: ${realCoordinates.prettyPrint()}"); - expect(percentage, greaterThan(0.75), reason: "GPS should be accurate >75% of the time: $percentage"); - - // Ensure that *very* noisy readings don't affect anything. - simulator.logger.debug("Adding 100, 100"); - simulator.gps.update(GpsCoordinates(latitude: 100, longitude: 100)); - expect(realGps.isNear(realCoordinates), isTrue); - GpsUtils.maxErrorMeters = oldError; - }); - - test("IMU noise when moving", skip: "IMU is currently accurate enough to not need filtering", () async { - Logger.level = LogLevel.off; - final simulator = AutonomySimulator(); - final simulatedImu = ImuSimulator(collection: simulator, maxError: imuError); - final realImu = RoverImu(collection: simulator); - final orientation = CardinalDirection.north.orientation; - simulatedImu.update(orientation); - - var count = 0; - for (var i = 0; i < 350; i++) { - orientation.z += 1; - simulatedImu.update(orientation); - final newData = simulatedImu.raw; - realImu.forceUpdate(newData); - simulator.logger.trace("Got new value: ${newData.heading}"); - simulator.logger.trace(" New heading: ${realImu.heading}"); - simulator.logger.trace(" Real position: ${orientation.heading}"); - if (i < 10) continue; - simulator.logger.trace(" Values are similar: ${realImu.isNear(CardinalDirection.north)}"); - if (realImu.isNear(CardinalDirection.north)) count++; - } - - final percentage = count / 350; - expect(percentage, greaterThan(0.75), reason: "IMU should be accurate >75% of the time: $percentage"); - - final badData = Orientation(z: 10); - simulator.logger.info("Final orientation: ${realImu.heading}"); - simulator.logger.info("Bad orientation: ${badData.heading}"); - realImu.forceUpdate(badData); - simulator.logger.info("Unaffected orientation: ${realImu.heading}"); - expect(realImu.isNear(CardinalDirection.north), isTrue); - await simulator.dispose(); - }); - - test("GPS latitude is set properly", () async { - final simulator = AutonomySimulator(); - const utahLatitude = 38.406683; - final utah = GpsCoordinates(latitude: utahLatitude); - - simulator.gps.forceUpdate(utah); - expect(simulator.hasValue, isFalse); - expect(GpsInterface.currentLatitude, 0); - - await simulator.init(); - await simulator.waitForValue(); - expect(simulator.hasValue, isTrue); - expect(GpsInterface.currentLatitude, utahLatitude); - - await simulator.dispose(); - GpsInterface.currentLatitude = 0; - }); - - test("GPS can start in a real location", () async { - final simulator = AutonomySimulator(); - final ocean = GpsCoordinates(latitude: 0, longitude: 0); - final newYork = GpsCoordinates(latitude: 45, longitude: 72); - simulator.gps = RoverGps(collection: simulator); - - await simulator.init(); - expect(simulator.gps.coordinates, ocean); - expect(simulator.gps.isNear(newYork), isFalse); - expect(ocean.isNear(newYork), isFalse); - - simulator.gps.forceUpdate(newYork); - expect(simulator.gps.isNear(ocean), isFalse); - expect(simulator.gps.isNear(newYork), isTrue); - - await simulator.dispose(); - }); - - test("IMU can handle values on the edge", () async { - Logger.level = LogLevel.off; - final simulator = AutonomySimulator(); - final simulatedImu = ImuSimulator(collection: simulator, maxError: imuError); - final realImu = RoverImu(collection: simulator); - final orientation = Orientation(z: 360); - simulatedImu.update(orientation); - - var count = 0; - for (var i = 0; i < 1000; i++) { - final newData = simulatedImu.raw; - realImu.update(newData); - simulator.logger.trace("Got new value: ${newData.heading}"); - simulator.logger.trace(" New heading: ${realImu.heading}"); - simulator.logger.trace(" Real position: ${orientation.heading}"); - if (i < 10) continue; - simulator.logger.trace(" Values are similar: ${realImu.isNear(orientation.heading)}"); - if (realImu.isNear(orientation.heading)) count++; - } - - // final percentage = count / 1000; - // expect(percentage, greaterThan(0.75), reason: "IMU should be accurate >75% of the time: $percentage"); - - // final badData = Orientation(z: 10); - // simulator.logger.info("Final orientation: ${realImu.heading}"); - // simulator.logger.info("Bad orientation: ${badData.heading}"); - // realImu.update(badData); - // simulator.logger.info("Unaffected orientation: ${realImu.heading}"); - // expect(realImu.isNear(orientation.heading), isTrue); - // await simulator.dispose(); - }); -}); From cd2ab752769bcb5df2e21fe6fe6eca9b0c1594d9 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 13:26:32 -0400 Subject: [PATCH 080/110] Repaired and removed old legacy tests --- test/rover_test.dart | 54 ---------------------------------------- test/simulator_test.dart | 47 ++-------------------------------- 2 files changed, 2 insertions(+), 99 deletions(-) diff --git a/test/rover_test.dart b/test/rover_test.dart index ed7cff6..a9042d5 100644 --- a/test/rover_test.dart +++ b/test/rover_test.dart @@ -4,7 +4,6 @@ import "package:burt_network/logging.dart"; import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; -import "package:autonomy/simulator.dart"; void main() => group("[Rover]", tags: ["rover"], () { test("Can be restarted", () async { @@ -15,17 +14,6 @@ void main() => group("[Rover]", tags: ["rover"], () { await rover.dispose(); }); - test("Real pathfinding is coherent", () async { - Logger.level = LogLevel.off; - final simulator = AutonomySimulator(); - simulator.pathfinder = RoverPathfinder(collection: simulator); - await testPath(simulator); - simulator.gps.update((lat: 0, long: 0).toGps()); - simulator.imu.update(Orientation()); - await testPath2(simulator); - await simulator.dispose(); - }); - test("Waits for sensor data", () async { final rover = RoverAutonomy(); final position = (lat: 5, long: 5).toGps(); @@ -55,45 +43,3 @@ void main() => group("[Rover]", tags: ["rover"], () { await rover.dispose(); }); }); - -Future testPath(AutonomyInterface simulator) async { - final destination = (lat: 5, long: 5).toGps(); - final result = simulator.pathfinder.getPath(destination); - expect(simulator.gps.latitude, 0); - expect(simulator.gps.longitude, 0); - expect(simulator.imu.heading, 0); - expect(result, isNotNull); if (result == null) return; - expect(result, isNotEmpty); - for (final transition in result) { - simulator.logger.trace(" From: ${simulator.gps.coordinates.prettyPrint()} facing ${simulator.imu.heading}"); - simulator.logger.debug(" $transition"); - await simulator.drive.driveState(transition); - expect(simulator.gps.isNear(transition.position), isTrue); - simulator.logger.trace("New orientation: ${simulator.imu.heading}"); - simulator.logger.trace("Expected orientation: ${transition.orientation}"); - expect(simulator.imu.nearest, transition.orientation); - } -} - -Future testPath2(AutonomyInterface simulator) async { - // Logger.level = LogLevel.all; - final destination = (lat: 4, long: 0).toGps(); - simulator.pathfinder.recordObstacle((lat: 2, long: 0).toGps()); - simulator.pathfinder.recordObstacle((lat: 4, long: -1).toGps()); - simulator.pathfinder.recordObstacle((lat: 4, long: 1).toGps()); - final result = simulator.pathfinder.getPath(destination); - expect(simulator.gps.latitude, 0); - expect(simulator.gps.longitude, 0); - expect(simulator.imu.heading, 0); - expect(result, isNotNull); if (result == null) return; - expect(result, isNotEmpty); - for (final transition in result) { - simulator.logger.debug(transition.toString()); - simulator.logger.trace(" From: ${simulator.gps.coordinates.prettyPrint()}"); - await simulator.drive.driveState(transition); - expect(simulator.gps.isNear(transition.position), isTrue); - expect(simulator.pathfinder.isObstacle(simulator.gps.coordinates), isFalse); - expect(simulator.imu.nearest, transition.orientation); - simulator.logger.trace(" To: ${simulator.gps.coordinates.prettyPrint()}"); - } -} diff --git a/test/simulator_test.dart b/test/simulator_test.dart index 0d986a1..b76ee46 100644 --- a/test/simulator_test.dart +++ b/test/simulator_test.dart @@ -4,7 +4,7 @@ import "package:autonomy/autonomy.dart"; import "package:burt_network/burt_network.dart"; void main() => group("[Simulator]", tags: ["simulator"], () { - test("Simulator can be restarted", () async { + test("Simulator can be restarted", () async { Logger.level = LogLevel.off; final simulator = AutonomySimulator(); expect(simulator.isInitialized, isFalse); @@ -15,55 +15,12 @@ void main() => group("[Simulator]", tags: ["simulator"], () { await simulator.dispose(); expect(simulator.isInitialized, isFalse); }); - - test("Simulated drive test with simulated GPS", () async { - Logger.level = LogLevel.off; - final simulator = AutonomySimulator(); - var position = GpsCoordinates(); - final gps = simulator.gps; - expect(gps.coordinates.isNear(position), isTrue); - // Turning left takes you +90 degrees - await simulator.drive.turnLeft(); - expect(simulator.imu.heading, 90); - expect(gps.coordinates.isNear(position), isTrue); - // Turning right takes you -90 degrees - await simulator.drive.turnRight(); - expect(simulator.imu.heading, 0); - expect(gps.coordinates.isNear(position), isTrue); - // Going straight takes you 1 cell forward - await simulator.drive.goForward(); - position += GpsUtils.north; - expect(gps.coordinates.isNear(position), isTrue); - expect(simulator.imu.heading, 0); - // Going forward at 90 degrees - await simulator.drive.turnLeft(); - await simulator.drive.goForward(); - position += GpsUtils.west; - expect(gps.coordinates.isNear(position), isTrue); - expect(simulator.imu.heading, 90); - // Going forward at 180 degrees - await simulator.drive.turnLeft(); - await simulator.drive.goForward(); - position += GpsUtils.south; - expect(gps.coordinates.isNear(position), isTrue); - expect(simulator.imu.heading, 180); - // Going forward at 270 degrees - await simulator.drive.turnLeft(); - await simulator.drive.goForward(); - position += GpsUtils.east; - expect(gps.coordinates.isNear(position), isTrue); - expect(simulator.imu.heading, 270); - // 4 lefts go back to north - await simulator.drive.turnLeft(); - expect(simulator.imu.heading, 0); - expect(gps.coordinates.isNear(position), isTrue); - await simulator.dispose(); - }); test("Already has values", () async { final simulator = AutonomySimulator(); expect(simulator.hasValue, isFalse); await simulator.init(); expect(simulator.hasValue, isTrue); + await simulator.dispose(); }); }); From c38a7674d03a62283eed8d0139b6612dfe1d03a4 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 13:33:16 -0400 Subject: [PATCH 081/110] Re-enable tests --- .github/workflows/analyze.yml | 11 ++++++----- analysis_options.yaml | 3 --- bin/arcuo.dart | 16 ---------------- bin/path.dart | 16 +++++++++++----- test/rover_test.dart | 5 +++-- 5 files changed, 20 insertions(+), 31 deletions(-) delete mode 100644 bin/arcuo.dart diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index b822d66..b6fc1c5 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -10,12 +10,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Clone repo + uses: actions/checkout@v4 # Note: This workflow uses the latest stable version of the Dart SDK. # You can specify other versions if desired, see documentation here: # https://github.com/dart-lang/setup-dart/blob/main/README.md - - uses: dart-lang/setup-dart@v1 + - name: Setup dart + uses: dart-lang/setup-dart@v1 with: sdk: 3.6.0 @@ -36,6 +38,5 @@ jobs: # Your project will need to have tests in test/ and a dependency on # package:test for this step to succeed. Note that Flutter projects will # want to change this to 'flutter test'. - # FIXME: Disabling tests for now - # - name: Run tests - # run: dart test + - name: Run tests + run: dart test diff --git a/analysis_options.yaml b/analysis_options.yaml index 0f5506c..6a2b793 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -25,9 +25,6 @@ analyzer: # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-raw-types.md strict-raw-types: true - exclude: - - test/*.dart - linter: rules: # Rules NOT in package:very_good_analysis diff --git a/bin/arcuo.dart b/bin/arcuo.dart deleted file mode 100644 index d924090..0000000 --- a/bin/arcuo.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:autonomy/autonomy.dart"; - -void main() async { - final rover = RoverAutonomy(); - rover.drive = RoverDrive(collection: rover, useImu: false, useGps: false); - await rover.init(); - //await rover.waitForValue(); - final didSeeAruco = await rover.drive.spinForAruco(); - if (didSeeAruco) { - rover.logger.info("Found aruco"); - await rover.drive.approachAruco(); - } else { - rover.logger.warning("COuld not find aruco"); - } - rover.logger.info("Done"); -} diff --git a/bin/path.dart b/bin/path.dart index 8ae1f28..c4c0802 100644 --- a/bin/path.dart +++ b/bin/path.dart @@ -1,15 +1,19 @@ import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; import "package:autonomy/simulator.dart"; +import "package:coordinate_converter/coordinate_converter.dart"; void main() { - GpsUtils.maxErrorMeters = 1; - final destination = (lat: 1000, long: 1000).toGps(); + final destination = + UTMCoordinates(x: 5 + 1000, y: 5 + 1000, zoneNumber: 31).toGps(); final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); + simulator.gps.update(UTMCoordinates(x: 5, y: 5, zoneNumber: 31).toGps()); final path = simulator.pathfinder.getPath(destination); if (path == null) { - simulator.logger.critical("Could not find path to ${destination.prettyPrint()}"); + simulator.logger.critical( + "Could not find path to ${destination.prettyPrint()}", + ); return; } if (path.isEmpty) { @@ -18,9 +22,11 @@ void main() { } var turnCount = 0; for (final step in path) { - if (step.instruction == DriveDirection.left || step.instruction == DriveDirection.right) { + if (step.instruction.isTurn) { turnCount++; } } - simulator.logger.info("Got a path with ${path.length} steps and $turnCount turn(s)"); + simulator.logger.info( + "Got a path with ${path.length} steps and $turnCount turn(s)", + ); } diff --git a/test/rover_test.dart b/test/rover_test.dart index a9042d5..f59630d 100644 --- a/test/rover_test.dart +++ b/test/rover_test.dart @@ -5,6 +5,8 @@ import "package:burt_network/logging.dart"; import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; +import "test_util.dart"; + void main() => group("[Rover]", tags: ["rover"], () { test("Can be restarted", () async { Logger.level = LogLevel.off; @@ -16,7 +18,6 @@ void main() => group("[Rover]", tags: ["rover"], () { test("Waits for sensor data", () async { final rover = RoverAutonomy(); - final position = (lat: 5, long: 5).toGps(); final orientation = Orientation(); final data = VideoData(); @@ -24,7 +25,7 @@ void main() => group("[Rover]", tags: ["rover"], () { expect(rover.hasValue, isFalse); expect(rover.gps.hasValue, isFalse); - rover.gps.forceUpdate(position); + rover.gps.forceUpdate(origin); expect(rover.gps.hasValue, isTrue); expect(rover.hasValue, isFalse); From 66d172411ed890c1feaf94a4f8273cd77ea48ee9 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 13:41:01 -0400 Subject: [PATCH 082/110] Reformated files --- bin/latlong.dart | 20 +++- bin/random.dart | 7 +- lib/rover.dart | 32 +++++-- lib/simulator.dart | 46 ++++++--- lib/src/detector/rover_detector.dart | 6 +- lib/src/detector/sim_detector.dart | 9 +- lib/src/drive/drive_commands.dart | 6 +- lib/src/drive/drive_config.dart | 10 +- lib/src/drive/drive_interface.dart | 36 ++++--- lib/src/drive/rover_drive.dart | 11 +-- lib/src/drive/sensor_drive.dart | 32 +++---- lib/src/drive/sim_drive.dart | 19 ++-- lib/src/drive/timed_drive.dart | 20 ++-- lib/src/gps/gps_interface.dart | 3 +- lib/src/gps/sim_gps.dart | 9 +- lib/src/imu/cardinal_direction.dart | 1 - lib/src/imu/imu_interface.dart | 3 +- lib/src/imu/sim_imu.dart | 4 +- lib/src/orchestrator/rover_orchestrator.dart | 88 ++++++++++------- lib/src/orchestrator/sim_orchestrator.dart | 18 +--- .../pathfinding/pathfinding_interface.dart | 5 +- lib/src/pathfinding/rover_pathfinding.dart | 17 +++- lib/src/pathfinding/sim_pathfinding.dart | 95 ++++++++++++++++--- lib/src/utils/a_star.dart | 14 ++- lib/src/utils/corrector.dart | 5 +- lib/src/utils/gps_utils.dart | 70 +++++++++----- lib/src/utils/imu_utils.dart | 4 + lib/src/video/rover_video.dart | 6 +- lib/src/video/video_interface.dart | 3 +- test/orchestrator_test.dart | 40 ++++++-- test/path_test.dart | 45 +++++++-- 31 files changed, 471 insertions(+), 213 deletions(-) diff --git a/bin/latlong.dart b/bin/latlong.dart index 2c8bbf5..f286be9 100644 --- a/bin/latlong.dart +++ b/bin/latlong.dart @@ -8,15 +8,25 @@ const utahLatitude = 38.406683; void printInfo(String name, double latitude) { GpsInterface.currentLatitude = latitude; print("At $name:"); - print(" There are ${GpsUtils.metersPerLongitude.toStringAsFixed(2)} meters per 1 degree of longitude"); - print(" Our max error in longitude would be ${GpsUtils.epsilonLongitude.toStringAsFixed(20)} degrees"); + print( + " There are ${GpsUtils.metersPerLongitude.toStringAsFixed(2)} meters per 1 degree of longitude", + ); + print( + " Our max error in longitude would be ${GpsUtils.epsilonLongitude.toStringAsFixed(20)} degrees", + ); final isWithinRange = GpsInterface.gpsError <= GpsUtils.epsilonLongitude; - print(" Our GPS has ${GpsInterface.gpsError} degrees of error, so this would ${isWithinRange ? 'work' : 'not work'}"); + print( + " Our GPS has ${GpsInterface.gpsError} degrees of error, so this would ${isWithinRange ? 'work' : 'not work'}", + ); } void main() { - print("There are always ${GpsUtils.metersPerLatitude} meters in 1 degree of latitude"); - print(" So our max error in latitude is always ${GpsUtils.epsilonLatitude.toStringAsFixed(20)} degrees"); + print( + "There are always ${GpsUtils.metersPerLatitude} meters in 1 degree of latitude", + ); + print( + " So our max error in latitude is always ${GpsUtils.epsilonLatitude.toStringAsFixed(20)} degrees", + ); printInfo("the equator", 0); printInfo("Binghamton", binghamtonLatitude); printInfo("Utah", utahLatitude); diff --git a/bin/random.dart b/bin/random.dart index c356d40..c927ba2 100644 --- a/bin/random.dart +++ b/bin/random.dart @@ -4,7 +4,8 @@ import "package:autonomy/interfaces.dart"; const maxError = GpsInterface.gpsError; const maxSamples = 10; -final epsilon = GpsUtils.epsilonLatitude; // we need to be accurate within 1 meter +final epsilon = + GpsUtils.epsilonLatitude; // we need to be accurate within 1 meter const n = 1000; bool verbose = false; @@ -17,7 +18,9 @@ bool test() { if (verbose) { final calibrated = corrector.calibratedValue; print("Current value: $value, Corrected value: $calibrated"); - print(" Difference: ${calibrated.toStringAsFixed(7)} < ${epsilon.toStringAsFixed(7)}"); + print( + " Difference: ${calibrated.toStringAsFixed(7)} < ${epsilon.toStringAsFixed(7)}", + ); } } return corrector.calibratedValue.abs() <= epsilon; diff --git a/lib/rover.dart b/lib/rover.dart index 38f5845..d212542 100644 --- a/lib/rover.dart +++ b/lib/rover.dart @@ -15,16 +15,30 @@ import "package:burt_network/burt_network.dart"; class RoverAutonomy extends AutonomyInterface { /// A server to communicate with the dashboard and receive data from the subsystems. // @override late final AutonomyServer server = AutonomyServer(collection: this); - @override late final server = RoverSocket(port: 8003, device: Device.AUTONOMY, collection: this); + @override + late final server = RoverSocket( + port: 8003, + device: Device.AUTONOMY, + collection: this, + ); + /// A helper class to handle driving the rover. - @override late DriveInterface drive = RoverDrive(collection: this); - @override late GpsInterface gps = RoverGps(collection: this); - @override late ImuInterface imu = RoverImu(collection: this); - @override late final logger = BurtLogger(socket: server); - @override late PathfindingInterface pathfinder = RoverPathfinder(collection: this); - @override late DetectorInterface detector = RoverDetector(collection: this); - @override late VideoInterface video = RoverVideo(collection: this); - @override late OrchestratorInterface orchestrator = RoverOrchestrator(collection: this); + @override + late DriveInterface drive = RoverDrive(collection: this); + @override + late GpsInterface gps = RoverGps(collection: this); + @override + late ImuInterface imu = RoverImu(collection: this); + @override + late final logger = BurtLogger(socket: server); + @override + late PathfindingInterface pathfinder = RoverPathfinder(collection: this); + @override + late DetectorInterface detector = RoverDetector(collection: this); + @override + late VideoInterface video = RoverVideo(collection: this); + @override + late OrchestratorInterface orchestrator = RoverOrchestrator(collection: this); @override Future onDisconnect() async { diff --git a/lib/simulator.dart b/lib/simulator.dart index 8d3d068..b5f1ce4 100644 --- a/lib/simulator.dart +++ b/lib/simulator.dart @@ -11,21 +11,45 @@ import "package:autonomy/simulator.dart"; import "package:burt_network/burt_network.dart"; class AutonomySimulator extends AutonomyInterface { - @override late final logger = BurtLogger(socket: server); - @override late final server = RoverSocket(port: 8003, device: Device.AUTONOMY, collection: this); - @override late GpsInterface gps = GpsSimulator(collection: this); - @override late ImuInterface imu = ImuSimulator(collection: this); - @override late DriveInterface drive = DriveSimulator(collection: this); - @override late PathfindingInterface pathfinder = PathfindingSimulator(collection: this); - @override late DetectorInterface detector = DetectorSimulator(collection: this, obstacles: []); - @override late VideoInterface video = VideoSimulator(collection: this); - @override late OrchestratorInterface orchestrator = OrchestratorSimulator(collection: this); + @override + late final logger = BurtLogger(socket: server); + @override + late final server = RoverSocket( + port: 8003, + device: Device.AUTONOMY, + collection: this, + ); + @override + late GpsInterface gps = GpsSimulator(collection: this); + @override + late ImuInterface imu = ImuSimulator(collection: this); + @override + late DriveInterface drive = DriveSimulator(collection: this); + @override + late PathfindingInterface pathfinder = PathfindingSimulator(collection: this); + @override + late DetectorInterface detector = DetectorSimulator( + collection: this, + obstacles: [], + ); + @override + late VideoInterface video = VideoSimulator(collection: this); + @override + late OrchestratorInterface orchestrator = OrchestratorSimulator( + collection: this, + ); bool isInitialized = false; @override - Future init() { isInitialized = true; return super.init(); } + Future init() { + isInitialized = true; + return super.init(); + } @override - Future dispose() { isInitialized = false; return super.dispose(); } + Future dispose() { + isInitialized = false; + return super.dispose(); + } } diff --git a/lib/src/detector/rover_detector.dart b/lib/src/detector/rover_detector.dart index a839e96..ab984b7 100644 --- a/lib/src/detector/rover_detector.dart +++ b/lib/src/detector/rover_detector.dart @@ -74,8 +74,7 @@ class RoverDetector extends DetectorInterface { ); queuedObstacles.add( - (collection.gps.coordinates.toUTM() + roverToPoint) - .toGps(), + (collection.gps.coordinates.toUTM() + roverToPoint).toGps(), ); } @@ -95,7 +94,8 @@ class RoverDetector extends DetectorInterface { final toRemove = temporaryObstacles.where((coordinates) { final delta = coordinates.toUTM() - roverUtm; final roverToPoint = (atan2(delta.y, delta.x) - pi / 2) * 180 / pi; - final relativeAngle = (collection.imu.heading + roverToPoint).clampHalfAngle(); + final relativeAngle = + (collection.imu.heading + roverToPoint).clampHalfAngle(); return relativeAngle > -135 && relativeAngle < 135; }); diff --git a/lib/src/detector/sim_detector.dart b/lib/src/detector/sim_detector.dart index 4b8e57a..607da56 100644 --- a/lib/src/detector/sim_detector.dart +++ b/lib/src/detector/sim_detector.dart @@ -5,8 +5,7 @@ class SimulatedObstacle { final int radius; SimulatedObstacle({required this.coordinates, required this.radius}); - bool isNear(GpsCoordinates other) => - coordinates.distanceTo(other) <= radius; + bool isNear(GpsCoordinates other) => coordinates.distanceTo(other) <= radius; } class DetectorSimulator extends DetectorInterface { @@ -29,7 +28,9 @@ class DetectorSimulator extends DetectorInterface { final coordinates = collection.gps.coordinates; var result = false; for (final obstacle in obstacles) { - if (!obstacle.isNear(coordinates) || found.contains(obstacle.coordinates)) continue; + if (!obstacle.isNear(coordinates) || found.contains(obstacle.coordinates)) { + continue; + } result = true; found.add(obstacle.coordinates); collection.pathfinder.recordObstacle(obstacle.coordinates); @@ -38,5 +39,5 @@ class DetectorSimulator extends DetectorInterface { } @override - bool isOnSlope() => false; // if on [slopedLatitude] + bool isOnSlope() => false; // if on [slopedLatitude] } diff --git a/lib/src/drive/drive_commands.dart b/lib/src/drive/drive_commands.dart index 5979008..47cd4fa 100644 --- a/lib/src/drive/drive_commands.dart +++ b/lib/src/drive/drive_commands.dart @@ -31,14 +31,18 @@ mixin RoverDriveCommands on DriveInterface { /// Sets the speeds of the wheels to spin the rover left void spinLeft() => _setSpeeds(left: -1, right: 1); + /// Sets the speeds of the wheels to spin the rover right void spinRight() => _setSpeeds(left: 1, right: -1); + /// Sets the speeds of the wheels to move the rover in a straight line void moveForward() => _setSpeeds(left: 1, right: 1); /// Sets the angle of the front camera. void setCameraAngle({required double swivel, required double tilt}) { - collection.logger.trace("Setting camera angles to $swivel (swivel) and $tilt (tilt)"); + collection.logger.trace( + "Setting camera angles to $swivel (swivel) and $tilt (tilt)", + ); final command = DriveCommand(frontSwivel: swivel, frontTilt: tilt); sendCommand(command); } diff --git a/lib/src/drive/drive_config.dart b/lib/src/drive/drive_config.dart index b8455c3..54068b9 100644 --- a/lib/src/drive/drive_config.dart +++ b/lib/src/drive/drive_config.dart @@ -13,12 +13,16 @@ import "package:burt_network/burt_network.dart"; class DriveConfig { /// The throttle to set the drive to when moving forward final double forwardThrottle; + /// The throttle to set the drive to when turning final double turnThrottle; + /// The time it takes to turn 90 degrees final Duration turnDelay; + /// The time it takes to move one meter forward final Duration oneMeterDelay; + /// The IP address for the subsystems program final String subsystemsAddress; @@ -32,10 +36,8 @@ class DriveConfig { }); /// The [SocketInfo] for Subsystems, created using [subsystemsAddress] and port 8001 - SocketInfo get subsystems => SocketInfo( - address: InternetAddress(subsystemsAddress), - port: 8001, - ); + SocketInfo get subsystems => + SocketInfo(address: InternetAddress(subsystemsAddress), port: 8001); } /// The [DriveConfig] for the rover diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index 25ce324..a9a3e9c 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -8,14 +8,19 @@ import "drive_config.dart"; enum DriveDirection { /// Move forward forward, + /// Turn 90 degrees left left, + /// Turn 90 degrees right right, + /// Turn 45 degrees left quarterLeft, + /// Turn 45 degrees right quarterRight, + /// Stop moving stop; @@ -27,7 +32,7 @@ enum DriveDirection { } /// An abstract class for driving. -/// +/// /// This allows for easy stubbing to simulate drive if certain sensors are not used. abstract class DriveInterface extends Service { /// The autonomy collection of the rover's sensors, pathfinders, loggers, and UDP sockets @@ -107,13 +112,15 @@ abstract class DriveInterface extends Service { Future faceOrientation(Orientation orientation); /// Turn to face the orientation of [direction], returns whether or not it was able to turn - Future faceDirection(CardinalDirection direction) => faceOrientation(direction.orientation); + Future faceDirection(CardinalDirection direction) => + faceOrientation(direction.orientation); /// Utility method to send a command to subsystems - void sendCommand(Message message) => collection.server.sendMessage(message, destination: config.subsystems); + void sendCommand(Message message) => + collection.server.sendMessage(message, destination: config.subsystems); /// Spins the rover to the nearest IMU rotation - /// + /// /// This exists so the rover can generate a path based on a known /// orientation that aligns to the possible orientations defined by [CardinalDirection] Future resolveOrientation() => faceDirection(collection.imu.nearest); @@ -121,14 +128,15 @@ abstract class DriveInterface extends Service { /// Turns to face the state's [AutonomyAStarState.orientation]. /// /// Exists so that the TimedDrive can implement this in terms of [AutonomyAStarState.instruction]. - /// + /// /// Returns whether or not the turn was successful - Future turnState(AutonomyAStarState state) => faceDirection(state.orientation); + Future turnState(AutonomyAStarState state) => + faceDirection(state.orientation); /// Drives the rover based on the instruction and desired positions in [state] - /// + /// /// This determines based on the [state] whether it should move forward, turn, or stop - /// + /// /// Returns whether or not the drive was successful Future driveState(AutonomyAStarState state) { if (state.instruction == DriveDirection.stop) { @@ -141,16 +149,20 @@ abstract class DriveInterface extends Service { } /// Utility method to send a command to change the color of the LED strip - /// + /// /// This is used to signal the state of the autonomous driving outside of the rover void setLedStrip(ProtoColor color, {bool blink = false}) { - final command = DriveCommand(color: color, blink: blink ? BoolState.YES : BoolState.NO); + final command = DriveCommand( + color: color, + blink: blink ? BoolState.YES : BoolState.NO, + ); sendCommand(command); } /// Spin to face an Aruco tag, returns whether or not it was able to face the tag - Future spinForAruco(int arucoId, {CameraName? desiredCamera}) async => false; + Future spinForAruco(int arucoId, {CameraName? desiredCamera}) async => + false; /// Drive forward to approach an Aruco tag - Future approachAruco() async { } + Future approachAruco() async {} } diff --git a/lib/src/drive/rover_drive.dart b/lib/src/drive/rover_drive.dart index 58ecb52..cdd5000 100644 --- a/lib/src/drive/rover_drive.dart +++ b/lib/src/drive/rover_drive.dart @@ -1,7 +1,6 @@ import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; import "package:autonomy/src/fsm/rover_fsm.dart"; -import "package:autonomy/src/fsm/rover_state.dart"; import "package:behavior_tree/behavior_tree.dart"; import "sensor_drive.dart"; @@ -27,7 +26,7 @@ class RoverDrive extends DriveInterface { late final simDrive = DriveSimulator(collection: collection, config: config); /// Constructor for RoverDrive - /// + /// /// Takes in parameters for whether or not to use the GPS and imu /// These will determine when to use [sensorDrive] or [timedDrive] RoverDrive({ @@ -80,10 +79,8 @@ class RoverDrive extends DriveInterface { } @override - Future spinForAruco( - int arucoId, { - CameraName? desiredCamera, - }) => sensorDrive.spinForAruco(arucoId, desiredCamera: desiredCamera); + Future spinForAruco(int arucoId, {CameraName? desiredCamera}) => + sensorDrive.spinForAruco(arucoId, desiredCamera: desiredCamera); @override Future approachAruco() => sensorDrive.approachAruco(); @@ -136,7 +133,7 @@ class RoverDrive extends DriveInterface { ); } } - + @override BaseNode faceOrientationNode(Orientation orientation) { if (useImu) { diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index a269da3..06ebd64 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -16,7 +16,8 @@ class SensorForwardState extends RoverState { DriveConfig get config => collection.drive.config; - SensorForwardState(super.controller, { + SensorForwardState( + super.controller, { required this.collection, required this.position, required this.drive, @@ -27,10 +28,7 @@ class SensorForwardState extends RoverState { drive.setThrottle(config.forwardThrottle); drive.moveForward(); - if (collection.gps.isNear( - position, - Constants.intermediateStepTolerance, - )) { + if (collection.gps.isNear(position, Constants.intermediateStepTolerance)) { controller.popState(); } } @@ -49,7 +47,8 @@ class SensorTurnState extends RoverState { DriveConfig get config => collection.drive.config; - SensorTurnState(super.controller, { + SensorTurnState( + super.controller, { required this.collection, required this.orientation, required this.drive, @@ -147,7 +146,7 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { }), ], ); - + @override BaseNode spinForArucoNode(int arucoId, {CameraName? desiredCamera}) => Selector( @@ -177,12 +176,12 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { /// Will periodically check for a condition to become true. This can be /// thought of as a "wait until", where the rover will periodically check /// if it has reached its desired position or orientation. - /// + /// /// The [predicate] method is intended to manuever the rover until its condition /// becomes true. To prevent infinite driving, this will return false if either /// the command is canceled, or [stopNearObstacle] is true and the rover becomes /// too close to an obstacle. - /// + /// /// Returns whether or not the feedback loop reached its desired state. Future runFeedback( bool Function() predicate, { @@ -209,7 +208,7 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { Future init() async => true; @override - Future dispose() async { } + Future dispose() async {} @override Future driveForward(GpsCoordinates position) async { @@ -223,13 +222,14 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { position, Constants.intermediateStepTolerance, ); - // ignore: require_trailing_commas + // ignore: require_trailing_commas }, stopNearObstacle: true).timeout( Constants.driveGPSTimeout, onTimeout: () { collection.logger.warning( "GPS Drive timed out", - body: "Failed to reach ${position.prettyPrint()} after ${Constants.driveGPSTimeout}", + body: + "Failed to reach ${position.prettyPrint()} after ${Constants.driveGPSTimeout}", ); return false; }, @@ -274,10 +274,7 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { } @override - Future spinForAruco( - int arucoId, { - CameraName? desiredCamera, - }) async { + Future spinForAruco(int arucoId, {CameraName? desiredCamera}) async { setThrottle(config.turnThrottle); var foundAruco = true; foundAruco = await runFeedback(() { @@ -288,7 +285,8 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { return collection.video.getArucoDetection( arucoId, desiredCamera: desiredCamera, - ) != null; + ) != + null; }).timeout( Constants.arucoSearchTimeout, onTimeout: () { diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index bc35421..35ce269 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -5,19 +5,23 @@ import "package:behavior_tree/behavior_tree.dart"; /// An implementation of [DriveInterface] that will not move the rover, /// and only update its sensor readings based on the desired values -/// +/// /// This assumes that the implementations for sensors are not expected to be updated from the rover, /// otherwise, this can cause the rover to not follow its path properly class DriveSimulator extends DriveInterface { /// The amount of time to wait before updating the virtual sensor readings static const delay = Duration(milliseconds: 500); - + /// Whether or not to wait before updating virtual sensor readings, /// this can be useful when simulating the individual steps of a path final bool shouldDelay; /// Constructor for DriveSimulator, initializing the default fields, and whether or not it should delay - DriveSimulator({required super.collection, this.shouldDelay = false, super.config}); + DriveSimulator({ + required super.collection, + this.shouldDelay = false, + super.config, + }); BaseNode _delayAndExecuteNode({ required Duration delay, @@ -88,7 +92,7 @@ class DriveSimulator extends DriveInterface { Future init() async => true; @override - Future dispose() async { } + Future dispose() async {} @override Future driveForward(GpsCoordinates position) async { @@ -99,13 +103,16 @@ class DriveSimulator extends DriveInterface { @override Future faceOrientation(Orientation orientation) async { - if (shouldDelay) await Future.delayed(const Duration(milliseconds: 500)); + if (shouldDelay) { + await Future.delayed(const Duration(milliseconds: 500)); + } collection.imu.update(orientation); return true; } @override - Future spinForAruco(int arucoId, {CameraName? desiredCamera}) async => true; + Future spinForAruco(int arucoId, {CameraName? desiredCamera}) async => + true; @override Future stop() async { diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index 6d42df2..2ffbd1e 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -67,9 +67,9 @@ class _TimedOperationNode extends BaseNode { } /// An implementation of [DriveInterface] that drives for a specified amount of time without using sensors -/// +/// /// The time to drive/turn for is defined by [DriveConfig.oneMeterDelay] and [DriveConfig.turnDelay] -/// +/// /// This should only be used if the rover is not using sensors for autonomous driving class TimedDrive extends DriveInterface with RoverDriveCommands { TimedDrive({required super.collection, super.config}); @@ -205,7 +205,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { Future init() async => true; @override - Future dispose() async { } + Future dispose() async {} @override Future driveForward(GpsCoordinates position) async { @@ -237,7 +237,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { } /// Moves forward for the amount of time it will take to drive the specified [distance] - /// + /// /// This will set the speeds to move forward, and wait for the amount of /// time specified by the [DriveConfig.oneMeterDelay] Future goForward([double distance = 1]) async { @@ -250,7 +250,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { } /// Turns left for the amount of time it will take to spin left 90 degrees - /// + /// /// This will set the speeds to turn left, and wait for the amount of /// time specified by the [DriveConfig.turnDelay] Future turnLeft() async { @@ -261,7 +261,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { } /// Turns right for the amount of time it will take to spin right 90 degrees - /// + /// /// This will set the speeds to turn right, and wait for the amount of /// time defined by the [DriveConfig.turnDelay] Future turnRight() async { @@ -272,7 +272,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { } /// Turns left for the amount of time it will take to spin left 45 degrees - /// + /// /// This will set the speeds to turn left, and wait for the amount of /// time defined by the [DriveConfig.turnDelay] / 2 Future turnQuarterLeft() async { @@ -283,7 +283,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { } /// Turns right for the amount of time it will take to spin right 45 degrees - /// + /// /// This will set the speeds to turn right, and wait for the amount of /// time defined by the [DriveConfig.turnDelay] / 2 Future turnQuarterRight() async { @@ -295,5 +295,7 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { @override Future faceOrientation(Orientation orientation) => - throw UnsupportedError("Cannot face any arbitrary direction using TimedDrive"); + throw UnsupportedError( + "Cannot face any arbitrary direction using TimedDrive", + ); } diff --git a/lib/src/gps/gps_interface.dart b/lib/src/gps/gps_interface.dart index 7ce52d1..8c62f69 100644 --- a/lib/src/gps/gps_interface.dart +++ b/lib/src/gps/gps_interface.dart @@ -17,7 +17,8 @@ abstract class GpsInterface extends Service with Receiver { GpsCoordinates get coordinates; - bool isNear(GpsCoordinates other, [double? tolerance]) => coordinates.isNear(other, tolerance); + bool isNear(GpsCoordinates other, [double? tolerance]) => + coordinates.isNear(other, tolerance); @override Future waitForValue() async { diff --git a/lib/src/gps/sim_gps.dart b/lib/src/gps/sim_gps.dart index d90a7f5..11c37df 100644 --- a/lib/src/gps/sim_gps.dart +++ b/lib/src/gps/sim_gps.dart @@ -2,13 +2,13 @@ import "package:autonomy/interfaces.dart"; class GpsSimulator extends GpsInterface with ValueReporter { final RandomError _error; - GpsSimulator({required super.collection, double maxError = 0}) : - _error = RandomError(maxError); + GpsSimulator({required super.collection, double maxError = 0}) + : _error = RandomError(maxError); @override RoverPosition getMessage() => RoverPosition(gps: coordinates); - GpsCoordinates _coordinates = GpsCoordinates(); + final GpsCoordinates _coordinates = GpsCoordinates(); @override GpsCoordinates get coordinates => GpsCoordinates( @@ -17,7 +17,8 @@ class GpsSimulator extends GpsInterface with ValueReporter { ); @override - void update(GpsCoordinates newValue) => _coordinates = newValue; + void update(GpsCoordinates newValue) => + _coordinates.mergeFromMessage(newValue); @override Future init() async { diff --git a/lib/src/imu/cardinal_direction.dart b/lib/src/imu/cardinal_direction.dart index 68017c6..ce30dc1 100644 --- a/lib/src/imu/cardinal_direction.dart +++ b/lib/src/imu/cardinal_direction.dart @@ -1,4 +1,3 @@ - import "package:autonomy/interfaces.dart"; enum CardinalDirection { diff --git a/lib/src/imu/imu_interface.dart b/lib/src/imu/imu_interface.dart index 9ff199b..38daeb1 100644 --- a/lib/src/imu/imu_interface.dart +++ b/lib/src/imu/imu_interface.dart @@ -14,7 +14,8 @@ abstract class ImuInterface extends Service with Receiver { @visibleForTesting void forceUpdate(Orientation newValue) {} - bool isNear(Orientation orientation, [double? tolerance]) => raw.isNear(orientation.heading, tolerance); + bool isNear(Orientation orientation, [double? tolerance]) => + raw.isNear(orientation.heading, tolerance); @override Future init() async => true; diff --git a/lib/src/imu/sim_imu.dart b/lib/src/imu/sim_imu.dart index 8798446..7c2e570 100644 --- a/lib/src/imu/sim_imu.dart +++ b/lib/src/imu/sim_imu.dart @@ -2,8 +2,8 @@ import "package:autonomy/interfaces.dart"; class ImuSimulator extends ImuInterface with ValueReporter { final RandomError _error; - ImuSimulator({required super.collection, double maxError = 0}) : - _error = RandomError(maxError); + ImuSimulator({required super.collection, double maxError = 0}) + : _error = RandomError(maxError); @override RoverPosition getMessage() => RoverPosition(orientation: raw); diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 5aaae10..8335ae0 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -202,11 +202,7 @@ class NavigationState extends RoverState { if (!hasFollowed) { hasFollowed = true; collection.logger.debug(currentPathState!.toString()); - controller.pushState( - collection.drive.driveStateState( - currentPathState!, - ), - ); + controller.pushState(collection.drive.driveStateState(currentPathState!)); return; } if (waypointIndex >= 5 || orchestrator.findAndLockObstacles()) { @@ -226,7 +222,6 @@ class NavigationState extends RoverState { return; } - orchestrator.traversed.add(currentPathState!.position); waypointIndex++; @@ -295,7 +290,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { ...traversed, }, task: currentCommand?.task, - crash: false, // TODO: Investigate if this is used and how to use it better + crash: false, // TODO: Investigate if this is used and how to use it better ); @override @@ -444,7 +439,8 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { currentWaypoint = currentPath![waypointIndex]; currentState = AutonomyState.DRIVING; - if (!hasCheckedWaypointOrientation && !isCorrectingWaypointOrientation) { + if (!hasCheckedWaypointOrientation && + !isCorrectingWaypointOrientation) { // if it has RTK, point towards the next coordinate if (collection.gps.coordinates.hasRTK) { final difference = @@ -550,7 +546,9 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { // relies on the tree not being "success" until we have reached our destination, // adding this here guarantees that this node will only be successful if we are // done following - Condition(() => collection.gps.isNear(destination, Constants.maxErrorMeters)), + Condition( + () => collection.gps.isNear(destination, Constants.maxErrorMeters), + ), ], ); } @@ -625,15 +623,20 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { }) async { await collection.drive.resolveOrientation(); collection.detector.findObstacles(); - while (!collection.gps.coordinates.isNear(goal) && !(alternateEndCondition?.call() ?? false)) { + while (!collection.gps.coordinates.isNear(goal) && + !(alternateEndCondition?.call() ?? false)) { // Calculate a path collection.logger.debug("Finding a path"); currentState = AutonomyState.PATHING; final path = collection.pathfinder.getPath(goal); - currentPath = path; // also use local variable path for promotion + currentPath = path; // also use local variable path for promotion if (path == null) { final current = collection.gps.coordinates; - collection.logger.error("Could not find a path", body: "No path found from ${current.prettyPrint()} to ${goal.prettyPrint()}"); + collection.logger.error( + "Could not find a path", + body: + "No path found from ${current.prettyPrint()} to ${goal.prettyPrint()}", + ); if (abortOnError) { currentState = AutonomyState.NO_SOLUTION; currentCommand = null; @@ -642,7 +645,9 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } // Try to take that path final current = collection.gps.coordinates; - collection.logger.debug("Found a path from ${current.prettyPrint()} to ${goal.prettyPrint()}: ${path.length} steps"); + collection.logger.debug( + "Found a path from ${current.prettyPrint()} to ${goal.prettyPrint()}: ${path.length} steps", + ); collection.logger.debug("Here is a summary of the path"); for (final step in path) { collection.logger.debug(step.toString()); @@ -656,9 +661,14 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { break; } // Replan if too far from start point - final distanceError = collection.gps.coordinates.distanceTo(state.startPostition); + final distanceError = collection.gps.coordinates.distanceTo( + state.startPostition, + ); if (distanceError >= Constants.replanErrorMeters) { - collection.logger.info("Replanning Path", body: "Rover is $distanceError meters off the path"); + collection.logger.info( + "Replanning Path", + body: "Rover is $distanceError meters off the path", + ); findAndLockObstacles(); break; } @@ -667,7 +677,8 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { Orientation targetOrientation; // if it has RTK, point towards the next coordinate if (collection.gps.coordinates.hasRTK) { - final difference = state.position.toUTM() - collection.gps.coordinates.toUTM(); + final difference = + state.position.toUTM() - collection.gps.coordinates.toUTM(); final angle = atan2(difference.y, difference.x) * 180 / pi; @@ -702,7 +713,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { final foundObstacle = findAndLockObstacles(); if (foundObstacle) { collection.logger.debug("Found an obstacle. Recalculating path..."); - break; // calculate a new path + break; // calculate a new path } } } @@ -712,8 +723,13 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { @override void handleGpsTask(AutonomyCommand command) { final destination = command.destination; - collection.logger.info("Received GPS Task", body: "Go to ${destination.prettyPrint()}"); - collection.logger.debug("Currently at ${collection.gps.coordinates.prettyPrint()}"); + collection.logger.info( + "Received GPS Task", + body: "Go to ${destination.prettyPrint()}", + ); + collection.logger.debug( + "Currently at ${collection.gps.coordinates.prettyPrint()}", + ); traversed.clear(); collection.drive.setLedStrip(ProtoColor.RED); waypointIndex = 0; @@ -787,7 +803,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } @override - void handleArucoTask(AutonomyCommand command) async { + Future handleArucoTask(AutonomyCommand command) async { collection.drive.setLedStrip(ProtoColor.RED); // Go to GPS coordinates @@ -867,8 +883,13 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { ); if (command.destination != GpsCoordinates(latitude: 0, longitude: 0)) { - if (!await calculateAndFollowPath(command.destination, abortOnError: false)) { - collection.logger.error("Failed to follow path towards initial destination"); + if (!await calculateAndFollowPath( + command.destination, + abortOnError: false, + )) { + collection.logger.error( + "Failed to follow path towards initial destination", + ); currentState = AutonomyState.NO_SOLUTION; currentCommand = null; return; @@ -907,7 +928,9 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { if (detectedAruco == null || !detectedAruco!.hasBestPnpResult()) { // TODO: handle this condition properly - collection.logger.error("Could not find desired Aruco tag after rotating towards it"); + collection.logger.error( + "Could not find desired Aruco tag after rotating towards it", + ); currentState = AutonomyState.NO_SOLUTION; currentCommand = null; return; @@ -927,7 +950,8 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { final distanceToTag = sqrt( pow(cameraToTag.translation.z, 2) + pow(cameraToTag.translation.x, 2), - ) - 1; // don't drive *into* the tag + ) - + 1; // don't drive *into* the tag if (distanceToTag < 1) { // well that was easy @@ -937,8 +961,12 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { return; } - final relativeX = -distanceToTag * sin((collection.imu.heading - detectedAruco!.yaw) * pi / 180); - final relativeY = distanceToTag * cos((collection.imu.heading - detectedAruco!.yaw) * pi / 180); + final relativeX = + -distanceToTag * + sin((collection.imu.heading - detectedAruco!.yaw) * pi / 180); + final relativeY = + distanceToTag * + cos((collection.imu.heading - detectedAruco!.yaw) * pi / 180); final destinationCoordinates = (collection.gps.coordinates.toUTM() + @@ -1004,12 +1032,8 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { } @override - void handleHammerTask(AutonomyCommand command) async { - - } + Future handleHammerTask(AutonomyCommand command) async {} @override - void handleBottleTask(AutonomyCommand command) async { - - } + Future handleBottleTask(AutonomyCommand command) async {} } diff --git a/lib/src/orchestrator/sim_orchestrator.dart b/lib/src/orchestrator/sim_orchestrator.dart index ae2dd75..8020d36 100644 --- a/lib/src/orchestrator/sim_orchestrator.dart +++ b/lib/src/orchestrator/sim_orchestrator.dart @@ -4,28 +4,20 @@ class OrchestratorSimulator extends OrchestratorInterface { OrchestratorSimulator({required super.collection}); @override - Future dispose() async { } + Future dispose() async {} @override AutonomyData get statusMessage => AutonomyData(); @override - Future handleGpsTask(AutonomyCommand command) async { - - } + Future handleGpsTask(AutonomyCommand command) async {} @override - Future handleArucoTask(AutonomyCommand command) async { - - } + Future handleArucoTask(AutonomyCommand command) async {} @override - Future handleHammerTask(AutonomyCommand command) async { - - } + Future handleHammerTask(AutonomyCommand command) async {} @override - Future handleBottleTask(AutonomyCommand command) async { - - } + Future handleBottleTask(AutonomyCommand command) async {} } diff --git a/lib/src/pathfinding/pathfinding_interface.dart b/lib/src/pathfinding/pathfinding_interface.dart index c234171..a308b64 100644 --- a/lib/src/pathfinding/pathfinding_interface.dart +++ b/lib/src/pathfinding/pathfinding_interface.dart @@ -5,7 +5,10 @@ abstract class PathfindingInterface extends Service { final AutonomyInterface collection; PathfindingInterface({required this.collection}); - List? getPath(GpsCoordinates destination, {bool verbose = false}); + List? getPath( + GpsCoordinates destination, { + bool verbose = false, + }); Set obstacles = {}; final Set _lockedObstacles = {}; diff --git a/lib/src/pathfinding/rover_pathfinding.dart b/lib/src/pathfinding/rover_pathfinding.dart index afe429d..6775282 100644 --- a/lib/src/pathfinding/rover_pathfinding.dart +++ b/lib/src/pathfinding/rover_pathfinding.dart @@ -20,9 +20,10 @@ class RoverPathfinder extends PathfindingInterface { position: previous.position, goal: previous.goal, collection: collection, - instruction: step.instruction == DriveDirection.quarterLeft - ? DriveDirection.left - : DriveDirection.right, + instruction: + step.instruction == DriveDirection.quarterLeft + ? DriveDirection.left + : DriveDirection.right, orientation: step.orientation, depth: step.depth, ); @@ -36,9 +37,15 @@ class RoverPathfinder extends PathfindingInterface { } @override - List? getPath(GpsCoordinates destination, {bool verbose = false}) { + List? getPath( + GpsCoordinates destination, { + bool verbose = false, + }) { if (isObstacle(destination)) return null; - final state = AutonomyAStarState.start(collection: collection, goal: destination); + final state = AutonomyAStarState.start( + collection: collection, + goal: destination, + ); final result = aStar(state, verbose: verbose, limit: 50000); if (result == null) return null; final transitions = result.reconstructPath(); diff --git a/lib/src/pathfinding/sim_pathfinding.dart b/lib/src/pathfinding/sim_pathfinding.dart index 3df3970..42ac80b 100644 --- a/lib/src/pathfinding/sim_pathfinding.dart +++ b/lib/src/pathfinding/sim_pathfinding.dart @@ -9,16 +9,89 @@ class PathfindingSimulator extends PathfindingInterface { Future init() async => true; @override - List getPath(GpsCoordinates destination, {bool verbose = false}) => [ - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 0).toGps(), orientation: CardinalDirection.north, instruction: DriveDirection.right, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 0).toGps(), orientation: CardinalDirection.east, instruction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 1).toGps(), orientation: CardinalDirection.east, instruction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 2).toGps(), orientation: CardinalDirection.east, instruction: DriveDirection.left, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 0, long: 2).toGps(), orientation: CardinalDirection.north, instruction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 1, long: 2).toGps(), orientation: CardinalDirection.north, instruction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 2).toGps(), orientation: CardinalDirection.north, instruction: DriveDirection.left, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 2).toGps(), orientation: CardinalDirection.west, instruction: DriveDirection.forward, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 1).toGps(), orientation: CardinalDirection.west, instruction: DriveDirection.right, collection: collection), - AutonomyAStarState(depth: i++, goal: (lat: 2, long: 1).toGps(), position: (lat: 2, long: 1).toGps(), orientation: CardinalDirection.north, instruction: DriveDirection.forward, collection: collection), + List getPath( + GpsCoordinates destination, { + bool verbose = false, + }) => [ + AutonomyAStarState( + depth: i++, + goal: (lat: 2, long: 1).toGps(), + position: (lat: 0, long: 0).toGps(), + orientation: CardinalDirection.north, + instruction: DriveDirection.right, + collection: collection, + ), + AutonomyAStarState( + depth: i++, + goal: (lat: 2, long: 1).toGps(), + position: (lat: 0, long: 0).toGps(), + orientation: CardinalDirection.east, + instruction: DriveDirection.forward, + collection: collection, + ), + AutonomyAStarState( + depth: i++, + goal: (lat: 2, long: 1).toGps(), + position: (lat: 0, long: 1).toGps(), + orientation: CardinalDirection.east, + instruction: DriveDirection.forward, + collection: collection, + ), + AutonomyAStarState( + depth: i++, + goal: (lat: 2, long: 1).toGps(), + position: (lat: 0, long: 2).toGps(), + orientation: CardinalDirection.east, + instruction: DriveDirection.left, + collection: collection, + ), + AutonomyAStarState( + depth: i++, + goal: (lat: 2, long: 1).toGps(), + position: (lat: 0, long: 2).toGps(), + orientation: CardinalDirection.north, + instruction: DriveDirection.forward, + collection: collection, + ), + AutonomyAStarState( + depth: i++, + goal: (lat: 2, long: 1).toGps(), + position: (lat: 1, long: 2).toGps(), + orientation: CardinalDirection.north, + instruction: DriveDirection.forward, + collection: collection, + ), + AutonomyAStarState( + depth: i++, + goal: (lat: 2, long: 1).toGps(), + position: (lat: 2, long: 2).toGps(), + orientation: CardinalDirection.north, + instruction: DriveDirection.left, + collection: collection, + ), + AutonomyAStarState( + depth: i++, + goal: (lat: 2, long: 1).toGps(), + position: (lat: 2, long: 2).toGps(), + orientation: CardinalDirection.west, + instruction: DriveDirection.forward, + collection: collection, + ), + AutonomyAStarState( + depth: i++, + goal: (lat: 2, long: 1).toGps(), + position: (lat: 2, long: 1).toGps(), + orientation: CardinalDirection.west, + instruction: DriveDirection.right, + collection: collection, + ), + AutonomyAStarState( + depth: i++, + goal: (lat: 2, long: 1).toGps(), + position: (lat: 2, long: 1).toGps(), + orientation: CardinalDirection.north, + instruction: DriveDirection.forward, + collection: collection, + ), ]; } diff --git a/lib/src/utils/a_star.dart b/lib/src/utils/a_star.dart index eaa7ebb..2449d16 100644 --- a/lib/src/utils/a_star.dart +++ b/lib/src/utils/a_star.dart @@ -12,7 +12,8 @@ class AutonomyAStarState extends AStarState { } if (direction == DriveDirection.forward) { return 1; - } else if (direction == DriveDirection.quarterLeft || direction == DriveDirection.quarterRight) { + } else if (direction == DriveDirection.quarterLeft || + direction == DriveDirection.quarterRight) { return sqrt2; } else { return 2 * sqrt2; @@ -68,7 +69,7 @@ class AutonomyAStarState extends AStarState { ); @override - String toString() => switch(instruction) { + String toString() => switch (instruction) { DriveDirection.forward => "Go forward to ${position.prettyPrint()}", DriveDirection.left => "Turn left to face $instruction", DriveDirection.right => "Turn right to face $instruction", @@ -84,7 +85,10 @@ class AutonomyAStarState extends AStarState { String hash() => "${position.prettyPrint()} ($orientation) ($instruction)"; @override - bool isGoal() => position.isNear(goal, min(Constants.moveLengthMeters, Constants.maxErrorMeters)); + bool isGoal() => position.isNear( + goal, + min(Constants.moveLengthMeters, Constants.maxErrorMeters), + ); /// Returns whether or not the rover will drive between or right next to an obstacle diagonally
///
@@ -118,8 +122,8 @@ class AutonomyAStarState extends AStarState { bool isValidState(AutonomyAStarState state) => !(state.instruction == DriveDirection.forward && - collection.pathfinder.isObstacle(state.position)) - && !willDriveThroughObstacle(state); + collection.pathfinder.isObstacle(state.position)) && + !willDriveThroughObstacle(state); Iterable _allNeighbors() => [ copyWith( diff --git a/lib/src/utils/corrector.dart b/lib/src/utils/corrector.dart index 7339f02..ac7d451 100644 --- a/lib/src/utils/corrector.dart +++ b/lib/src/utils/corrector.dart @@ -3,7 +3,10 @@ import "dart:collection"; class ErrorCorrector { final int maxSamples; final double maxDeviation; - ErrorCorrector({required this.maxSamples, this.maxDeviation = double.infinity}); + ErrorCorrector({ + required this.maxSamples, + this.maxDeviation = double.infinity, + }); factory ErrorCorrector.disabled() => ErrorCorrector(maxSamples: 1); double calibratedValue = 0; diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 41f0b18..686b48d 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -1,4 +1,3 @@ - import "dart:math"; import "package:autonomy/constants.dart"; @@ -12,29 +11,46 @@ typedef GpsMeters = ({num lat, num long}); extension GpsMetersUtil on GpsMeters { /// Add 2 [GpsMeters] together GpsMeters operator +(GpsMeters other) => ( - lat: lat + other.lat, - long: long + other.long, - ); + lat: lat + other.lat, + long: long + other.long, + ); /// Subtract 2 [GpsMeters] from each other GpsMeters operator -(GpsMeters other) => ( - lat: lat - other.lat, - long: long - other.long, - ); + lat: lat - other.lat, + long: long - other.long, + ); } extension GpsUtils on GpsCoordinates { - static UTMCoordinates eastMeters = UTMCoordinates(y: 0, x: Constants.moveLengthMeters, zoneNumber: 1); - static UTMCoordinates westMeters = UTMCoordinates(y: 0, x: -Constants.moveLengthMeters, zoneNumber: 1); - static UTMCoordinates northMeters = UTMCoordinates(y: Constants.moveLengthMeters, x: 0, zoneNumber: 1); - static UTMCoordinates southMeters = UTMCoordinates(y: -Constants.moveLengthMeters, x: 0, zoneNumber: 1); + static UTMCoordinates eastMeters = UTMCoordinates( + y: 0, + x: Constants.moveLengthMeters, + zoneNumber: 1, + ); + static UTMCoordinates westMeters = UTMCoordinates( + y: 0, + x: -Constants.moveLengthMeters, + zoneNumber: 1, + ); + static UTMCoordinates northMeters = UTMCoordinates( + y: Constants.moveLengthMeters, + x: 0, + zoneNumber: 1, + ); + static UTMCoordinates southMeters = UTMCoordinates( + y: -Constants.moveLengthMeters, + x: 0, + zoneNumber: 1, + ); static final UTMCoordinates northEastMeters = northMeters + eastMeters; static final UTMCoordinates northWestMeters = northMeters + westMeters; static final UTMCoordinates southEastMeters = southMeters + eastMeters; static final UTMCoordinates southWestMeters = southMeters + westMeters; /// Whether or not the coordinates is fused with the RTK algorithm - bool get hasRTK => rtkMode == RTKMode.RTK_FIXED || rtkMode == RTKMode.RTK_FLOAT; + bool get hasRTK => + rtkMode == RTKMode.RTK_FIXED || rtkMode == RTKMode.RTK_FLOAT; /// The distance to [other] using the haversine formula double distanceTo(GpsCoordinates other) { @@ -45,7 +61,8 @@ extension GpsUtils on GpsCoordinates { final deltaLong = (longitude - other.longitude) * pi / 180; // Apply the Haversine formula - final a = pow(sin(deltaLat / 2), 2) + + final a = + pow(sin(deltaLat / 2), 2) + cos(other.latitude * pi / 180) * cos(latitude * pi / 180) * pow(sin(deltaLong / 2), 2); @@ -93,7 +110,8 @@ extension GpsUtils on GpsCoordinates { return distanceTo(other) < tolerance; } - GpsCoordinates operator +(GpsCoordinates other) => (toUTM() + other.toUTM()).toGps(); + GpsCoordinates operator +(GpsCoordinates other) => + (toUTM() + other.toUTM()).toGps(); GpsCoordinates operator -(GpsCoordinates other) => GpsCoordinates( latitude: latitude - other.latitude, @@ -102,15 +120,17 @@ extension GpsUtils on GpsCoordinates { String prettyPrint() => toProto3Json().toString(); - GpsCoordinates goForward(CardinalDirection orientation) => (toUTM() + - switch (orientation) { - CardinalDirection.north => GpsUtils.northMeters, - CardinalDirection.south => GpsUtils.southMeters, - CardinalDirection.west => GpsUtils.westMeters, - CardinalDirection.east => GpsUtils.eastMeters, - CardinalDirection.northEast => GpsUtils.northEastMeters, - CardinalDirection.northWest => GpsUtils.northWestMeters, - CardinalDirection.southEast => GpsUtils.southEastMeters, - CardinalDirection.southWest => GpsUtils.southWestMeters, - }).toGps(); + GpsCoordinates goForward(CardinalDirection orientation) => + (toUTM() + + switch (orientation) { + CardinalDirection.north => GpsUtils.northMeters, + CardinalDirection.south => GpsUtils.southMeters, + CardinalDirection.west => GpsUtils.westMeters, + CardinalDirection.east => GpsUtils.eastMeters, + CardinalDirection.northEast => GpsUtils.northEastMeters, + CardinalDirection.northWest => GpsUtils.northWestMeters, + CardinalDirection.southEast => GpsUtils.southEastMeters, + CardinalDirection.southWest => GpsUtils.southWestMeters, + }) + .toGps(); } diff --git a/lib/src/utils/imu_utils.dart b/lib/src/utils/imu_utils.dart index 084bc29..06e6365 100644 --- a/lib/src/utils/imu_utils.dart +++ b/lib/src/utils/imu_utils.dart @@ -4,10 +4,13 @@ import "package:autonomy/constants.dart"; extension OrientationUtils on Orientation { /// North orientation static final north = Orientation(z: CardinalDirection.north.angle); + /// East orientation static final west = Orientation(z: CardinalDirection.west.angle); + /// South Orientation static final south = Orientation(z: CardinalDirection.south.angle); + /// East orientation static final east = Orientation(z: CardinalDirection.east.angle); @@ -34,6 +37,7 @@ extension OrientationUtils on Orientation { extension AngleUtils on double { /// The angle clamped between (-180, 180) double clampHalfAngle() => ((this + 180) % 360) - 180; + /// The angle clamped between (0, 360) double clampAngle() => ((this % 360) + 360) % 360; } diff --git a/lib/src/video/rover_video.dart b/lib/src/video/rover_video.dart index 3698d82..e3363e4 100644 --- a/lib/src/video/rover_video.dart +++ b/lib/src/video/rover_video.dart @@ -50,7 +50,9 @@ class RoverVideo extends VideoInterface { constructor: VideoData.fromBuffer, callback: (result) async { if (result.hasFrame() && result.frame.isNotEmpty) return; - if (result.details.name != (desiredCamera ?? result.details.name)) return; + if (result.details.name != (desiredCamera ?? result.details.name)) { + return; + } final object = result.detectedObjects.firstWhereOrNull( (e) => e.objectType == DetectedObjectType.ARUCO && e.arucoTagId == id, ); @@ -60,7 +62,7 @@ class RoverVideo extends VideoInterface { } }, ); - + try { return await completer.future.timeout(timeout); } on TimeoutException { diff --git a/lib/src/video/video_interface.dart b/lib/src/video/video_interface.dart index a9d095d..4344054 100644 --- a/lib/src/video/video_interface.dart +++ b/lib/src/video/video_interface.dart @@ -27,7 +27,8 @@ abstract class VideoInterface extends Service with Receiver { void updateFrame(VideoData result); - DetectedObject? getArucoDetection(int id, {CameraName? desiredCamera}) => null; + DetectedObject? getArucoDetection(int id, {CameraName? desiredCamera}) => + null; Future waitForAruco( int id, { diff --git a/test/orchestrator_test.dart b/test/orchestrator_test.dart index 5250d53..7eb9f75 100644 --- a/test/orchestrator_test.dart +++ b/test/orchestrator_test.dart @@ -10,14 +10,17 @@ void main() => group("[Orchestrator]", skip: true, tags: ["orchestrator"], () { tearDown(() => Logger.level = LogLevel.off); test("Fails for invalid destinations", () async { - Logger.level = LogLevel.off; // this test can log critical messages + Logger.level = LogLevel.off; // this test can log critical messages final simulator = AutonomySimulator(); await simulator.init(); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.orchestrator = RoverOrchestrator(collection: simulator); simulator.pathfinder.recordObstacle((lat: 2, long: 0).toGps()); // Test blocked command: - final command = AutonomyCommand(destination: (lat: 2, long: 0).toGps(), task: AutonomyTask.GPS_ONLY); + final command = AutonomyCommand( + destination: (lat: 2, long: 0).toGps(), + task: AutonomyTask.GPS_ONLY, + ); expect(simulator.gps.latitude, 0); expect(simulator.gps.longitude, 0); expect(simulator.imu.heading, 0); @@ -34,7 +37,7 @@ void main() => group("[Orchestrator]", skip: true, tags: ["orchestrator"], () { }); test("Works for GPS task", () async { - Logger.level = LogLevel.off; // this test can log critical messages + Logger.level = LogLevel.off; // this test can log critical messages final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.orchestrator = RoverOrchestrator(collection: simulator); @@ -46,7 +49,10 @@ void main() => group("[Orchestrator]", skip: true, tags: ["orchestrator"], () { await simulator.init(); // Test normal command: final destination = (lat: 4, long: 0).toGps(); - final command = AutonomyCommand(destination: destination, task: AutonomyTask.GPS_ONLY); + final command = AutonomyCommand( + destination: destination, + task: AutonomyTask.GPS_ONLY, + ); expect(simulator.gps.latitude, 0); expect(simulator.gps.longitude, 0); expect(simulator.imu.heading, 0); @@ -74,7 +80,10 @@ void main() => group("[Orchestrator]", skip: true, tags: ["orchestrator"], () { SimulatedObstacle(coordinates: (lat: 6, long: -1).toGps(), radius: 1), SimulatedObstacle(coordinates: (lat: 6, long: 1).toGps(), radius: 1), ]; - simulator.detector = DetectorSimulator(collection: simulator, obstacles: obstacles); + simulator.detector = DetectorSimulator( + collection: simulator, + obstacles: obstacles, + ); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.orchestrator = RoverOrchestrator(collection: simulator); simulator.drive = DriveSimulator(collection: simulator); @@ -83,7 +92,10 @@ void main() => group("[Orchestrator]", skip: true, tags: ["orchestrator"], () { final destination = (lat: 0, long: 7).toGps(); expect(simulator.gps.isNear(origin), isTrue); expect(simulator.imu.heading, 0); - final command = AutonomyCommand(destination: destination, task: AutonomyTask.GPS_ONLY); + final command = AutonomyCommand( + destination: destination, + task: AutonomyTask.GPS_ONLY, + ); await simulator.orchestrator.onCommand(command); expect(simulator.gps.isNear(destination), isTrue); await simulator.dispose(); @@ -93,11 +105,18 @@ void main() => group("[Orchestrator]", skip: true, tags: ["orchestrator"], () { final simulator = AutonomySimulator(); final start = (lat: 5, long: 0).toGps(); final destination = (lat: 5, long: 5).toGps(); - final command = AutonomyCommand(destination: destination, task: AutonomyTask.GPS_ONLY); + final command = AutonomyCommand( + destination: destination, + task: AutonomyTask.GPS_ONLY, + ); simulator.orchestrator = RoverOrchestrator(collection: simulator); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.gps = RoverGps(collection: simulator); - simulator.drive = RoverDrive(collection: simulator, useGps: true, useImu: false, config: tankConfig); + simulator.drive = RoverDrive( + collection: simulator, + useImu: false, + config: tankConfig, + ); await simulator.init(); expect(simulator.gps.hasValue, isFalse); @@ -105,7 +124,10 @@ void main() => group("[Orchestrator]", skip: true, tags: ["orchestrator"], () { await simulator.orchestrator.onCommand(command); expect(simulator.gps.hasValue, isFalse); expect(GpsInterface.currentLatitude, 0); - expect(simulator.orchestrator.statusMessage.state, AutonomyState.NO_SOLUTION); + expect( + simulator.orchestrator.statusMessage.state, + AutonomyState.NO_SOLUTION, + ); simulator.gps.forceUpdate(start); await simulator.init(); diff --git a/test/path_test.dart b/test/path_test.dart index 2bb801b..b91634b 100644 --- a/test/path_test.dart +++ b/test/path_test.dart @@ -18,7 +18,9 @@ void main() => group("[Pathfinding]", tags: ["path"], () { test("Simple path from (0, 0) to (5, 5) exists", () { final simulator = AutonomySimulator(); final destination = origin.plus(x: 5, y: 5); - simulator.logger.info("Each step is ${GpsUtils.northMeters.toGps().latitude.toStringAsFixed(5)}"); + simulator.logger.info( + "Each step is ${GpsUtils.northMeters.toGps().latitude.toStringAsFixed(5)}", + ); simulator.logger.info("Going to ${destination.prettyPrint()}"); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.gps.update(origin); @@ -30,7 +32,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { final simulator = AutonomySimulator(); // Plan a path from (0, 0) to (5, 5) - simulator.pathfinder = RoverPathfinder(collection: simulator); + simulator.pathfinder = RoverPathfinder(collection: simulator); final destination = origin.plus(x: 5, y: 5); simulator.logger.info("Going to ${destination.prettyPrint()}"); simulator.gps.update(origin); @@ -71,14 +73,24 @@ void main() => group("[Pathfinding]", tags: ["path"], () { simulator.logger.trace(step.toString()); expect(simulator.pathfinder.isObstacle(step.position), isFalse); } - expect(path.length, 9, reason: "1 turn + 1 forward + 1 turn + 5 forward + 1 stop = 9 steps total"); - expect(path.last.position.distanceTo(destination), lessThan(Constants.maxErrorMeters)); + expect( + path.length, + 9, + reason: + "1 turn + 1 forward + 1 turn + 5 forward + 1 stop = 9 steps total", + ); + expect( + path.last.position.distanceTo(destination), + lessThan(Constants.maxErrorMeters), + ); }); test("Stress test", () async { final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); - simulator.logger.trace("Starting from ${simulator.gps.coordinates.prettyPrint()}"); + simulator.logger.trace( + "Starting from ${simulator.gps.coordinates.prettyPrint()}", + ); final destination = origin.plus(x: 1000, y: 1000); simulator.logger.info("Going to ${destination.prettyPrint()}"); simulator.gps.update(origin); @@ -115,7 +127,12 @@ void main() => group("[Pathfinding]", tags: ["path"], () { simulator.gps.update(origin); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); - expect(path!.where((state) => state.instruction == DriveDirection.forward).length, 5); + expect( + path! + .where((state) => state.instruction == DriveDirection.forward) + .length, + 5, + ); expect(path[1].instruction, DriveDirection.quarterRight); await simulator.dispose(); }); @@ -136,7 +153,12 @@ void main() => group("[Pathfinding]", tags: ["path"], () { simulator.gps.update(origin); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); - expect(path!.where((state) => state.instruction == DriveDirection.forward).length, greaterThan(2)); + expect( + path! + .where((state) => state.instruction == DriveDirection.forward) + .length, + greaterThan(2), + ); await simulator.dispose(); }); @@ -145,7 +167,7 @@ void main() => group("[Pathfinding]", tags: ["path"], () { simulator.pathfinder = RoverPathfinder(collection: simulator); final destination = UTMCoordinates(x: 10, y: 10, zoneNumber: 31).toGps(); final obstacles = { - origin, /* Destination */ + origin /* Destination */, /* Rover */ }; for (final obstacle in obstacles) { @@ -154,7 +176,12 @@ void main() => group("[Pathfinding]", tags: ["path"], () { simulator.gps.update(origin); final path = simulator.pathfinder.getPath(destination); expect(path, isNotNull); - expect(path!.where((state) => state.instruction == DriveDirection.forward).length, greaterThan(1)); + expect( + path! + .where((state) => state.instruction == DriveDirection.forward) + .length, + greaterThan(1), + ); await simulator.dispose(); }); }); From 43d6d2cc6d34938391d2e38c52dda0272c7dc536 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 14:04:37 -0400 Subject: [PATCH 083/110] Fix compiler errors in bin/ files --- bin/aruco.dart | 2 +- bin/latlong.dart | 13 ++++++++----- bin/random.dart | 8 +++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/bin/aruco.dart b/bin/aruco.dart index 5f9fdcb..15ca03c 100644 --- a/bin/aruco.dart +++ b/bin/aruco.dart @@ -4,7 +4,7 @@ void main() async { final rover = RoverAutonomy(); await rover.init(); await rover.waitForValue(); - final didSeeAruco = await rover.drive.spinForAruco(); + final didSeeAruco = await rover.drive.spinForAruco(0); if (didSeeAruco) { rover.logger.info("Found aruco"); await rover.drive.approachAruco(); diff --git a/bin/latlong.dart b/bin/latlong.dart index f286be9..320d5a5 100644 --- a/bin/latlong.dart +++ b/bin/latlong.dart @@ -1,5 +1,6 @@ // ignore_for_file: avoid_print +import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; const binghamtonLatitude = 42.0877327; @@ -7,14 +8,16 @@ const utahLatitude = 38.406683; void printInfo(String name, double latitude) { GpsInterface.currentLatitude = latitude; + final metersPerLongitude = GpsToMeters.metersPerLongitude(latitude); print("At $name:"); print( - " There are ${GpsUtils.metersPerLongitude.toStringAsFixed(2)} meters per 1 degree of longitude", + " There are ${metersPerLongitude.toStringAsFixed(2)} meters per 1 degree of longitude", ); print( - " Our max error in longitude would be ${GpsUtils.epsilonLongitude.toStringAsFixed(20)} degrees", + " Our max error in longitude would be ${(Constants.maxErrorMeters / metersPerLongitude).toStringAsFixed(20)} degrees", ); - final isWithinRange = GpsInterface.gpsError <= GpsUtils.epsilonLongitude; + final isWithinRange = + GpsInterface.gpsError <= Constants.maxErrorMeters / metersPerLongitude; print( " Our GPS has ${GpsInterface.gpsError} degrees of error, so this would ${isWithinRange ? 'work' : 'not work'}", ); @@ -22,10 +25,10 @@ void printInfo(String name, double latitude) { void main() { print( - "There are always ${GpsUtils.metersPerLatitude} meters in 1 degree of latitude", + "There are always ${GpsToMeters.metersPerLatitude} meters in 1 degree of latitude", ); print( - " So our max error in latitude is always ${GpsUtils.epsilonLatitude.toStringAsFixed(20)} degrees", + " So our max error in latitude is always ${(Constants.maxErrorMeters / GpsToMeters.metersPerLatitude).toStringAsFixed(20)} degrees", ); printInfo("the equator", 0); printInfo("Binghamton", binghamtonLatitude); diff --git a/bin/random.dart b/bin/random.dart index c927ba2..5458f54 100644 --- a/bin/random.dart +++ b/bin/random.dart @@ -1,11 +1,13 @@ // ignore_for_file: avoid_print +import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; const maxError = GpsInterface.gpsError; const maxSamples = 10; -final epsilon = - GpsUtils.epsilonLatitude; // we need to be accurate within 1 meter +const epsilon = + Constants.maxErrorMeters / + GpsToMeters.metersPerLatitude; // we need to be accurate within 1 meter const n = 1000; bool verbose = false; @@ -33,5 +35,5 @@ void main(List args) { if (test()) count++; } final percentage = (count / n * 100).toStringAsFixed(2); - print("Average performance: %$percentage"); + print("Average performance: $percentage%"); } From 8ea8495bc52854ea1f3b8dbc3f914502fda328bf Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 14:09:08 -0400 Subject: [PATCH 084/110] Remove static variables from gps interface --- bin/latlong.dart | 5 ++--- bin/random.dart | 2 +- lib/constants.dart | 3 +++ lib/src/gps/gps_interface.dart | 17 +++++------------ 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/bin/latlong.dart b/bin/latlong.dart index 320d5a5..3606ebc 100644 --- a/bin/latlong.dart +++ b/bin/latlong.dart @@ -7,7 +7,6 @@ const binghamtonLatitude = 42.0877327; const utahLatitude = 38.406683; void printInfo(String name, double latitude) { - GpsInterface.currentLatitude = latitude; final metersPerLongitude = GpsToMeters.metersPerLongitude(latitude); print("At $name:"); print( @@ -17,9 +16,9 @@ void printInfo(String name, double latitude) { " Our max error in longitude would be ${(Constants.maxErrorMeters / metersPerLongitude).toStringAsFixed(20)} degrees", ); final isWithinRange = - GpsInterface.gpsError <= Constants.maxErrorMeters / metersPerLongitude; + Constants.gpsError <= Constants.maxErrorMeters / metersPerLongitude; print( - " Our GPS has ${GpsInterface.gpsError} degrees of error, so this would ${isWithinRange ? 'work' : 'not work'}", + " Our GPS has ${Constants.gpsError} degrees of error, so this would ${isWithinRange ? 'work' : 'not work'}", ); } diff --git a/bin/random.dart b/bin/random.dart index 5458f54..0df68e0 100644 --- a/bin/random.dart +++ b/bin/random.dart @@ -3,7 +3,7 @@ import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; -const maxError = GpsInterface.gpsError; +const maxError = Constants.gpsError; const maxSamples = 10; const epsilon = Constants.maxErrorMeters / diff --git a/lib/constants.dart b/lib/constants.dart index 3cafaad..414320a 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -9,6 +9,9 @@ class Constants { /// Private internal constructor for constants Constants._(); + /// The maximum error (in degrees) of our GPS + static const gpsError = 0.00003; + /// The maximum error or "tolerance" for reaching the end goal static const double maxErrorMeters = 1; diff --git a/lib/src/gps/gps_interface.dart b/lib/src/gps/gps_interface.dart index 8c62f69..7b4b772 100644 --- a/lib/src/gps/gps_interface.dart +++ b/lib/src/gps/gps_interface.dart @@ -2,30 +2,23 @@ import "package:autonomy/interfaces.dart"; import "package:meta/meta.dart"; abstract class GpsInterface extends Service with Receiver { - static const gpsError = 0.00003; - static double currentLatitude = 0; - final AutonomyInterface collection; - GpsInterface({required this.collection}); + + GpsCoordinates get coordinates; double get longitude => coordinates.longitude; double get latitude => coordinates.latitude; + GpsInterface({required this.collection}); + void update(GpsCoordinates newValue); + @visibleForTesting void forceUpdate(GpsCoordinates newValue) {} - GpsCoordinates get coordinates; - bool isNear(GpsCoordinates other, [double? tolerance]) => coordinates.isNear(other, tolerance); - @override - Future waitForValue() async { - await super.waitForValue(); - currentLatitude = coordinates.latitude; - } - @override Future init() async => true; } From fd374d2b1572e333961233bfb1e0372c7b204a3b Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 14:10:36 -0400 Subject: [PATCH 085/110] Fix errors in orchestrator test --- test/orchestrator_test.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/orchestrator_test.dart b/test/orchestrator_test.dart index 7eb9f75..e6a1c40 100644 --- a/test/orchestrator_test.dart +++ b/test/orchestrator_test.dart @@ -123,7 +123,6 @@ void main() => group("[Orchestrator]", skip: true, tags: ["orchestrator"], () { await simulator.orchestrator.onCommand(command); expect(simulator.gps.hasValue, isFalse); - expect(GpsInterface.currentLatitude, 0); expect( simulator.orchestrator.statusMessage.state, AutonomyState.NO_SOLUTION, @@ -136,10 +135,8 @@ void main() => group("[Orchestrator]", skip: true, tags: ["orchestrator"], () { unawaited(simulator.orchestrator.onCommand(command)); await Future.delayed(Duration.zero); expect(simulator.orchestrator.currentCommand, isNotNull); - expect(GpsInterface.currentLatitude, start.latitude); expect(simulator.orchestrator.currentState, AutonomyState.DRIVING); - GpsInterface.currentLatitude = 0; await simulator.dispose(); }); }); From a00d97d3a6ebaa76ada9d219817155f9359ed619 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 14:16:50 -0400 Subject: [PATCH 086/110] Update docs workflow --- .github/workflows/documentation.yml | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 0267dfe..b152de3 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -24,26 +24,13 @@ jobs: - name: Install Dart uses: dart-lang/setup-dart@v1 with: - sdk: 3.5.2 + sdk: 3.6.0 - name: Install dependencies run: dart pub get - - name: Analyze Dart code - run: dart analyze --fatal-infos - - - name: Output error - if: failure() - run: echo "::error The code or is missing documentation. Run flutter analyze --dartdocs" - - name: Generate documentation - run: dart doc --output=docs - - # Your project will need to have tests in test/ and a dependency on - # package:test for this step to succeed. Note that Flutter projects will - # want to change this to 'flutter test'. - - name: Run tests - run: dart test + run: dartdoc --ignore 'unresolved-doc-reference,not-implemented,no-documentable-libraries,ambiguous-reexport' --exclude 'dart:async,dart:collection,dart:convert,dart:core,dart:developer,dart:io,dart:isolate,dart:math,dart:typed_data,dart:ui,dart:html,dart:js,dart:ffi,dart:js_util' --quiet --json --output docs --no-validate-links --no-verbose-warnings --no-allow-non-local-warnings - name: Commit and push files run: | From d7ab7661397f0fefd071949d5a5fb409fd6e0b7c Mon Sep 17 00:00:00 2001 From: max-mazer Date: Mon, 26 May 2025 15:31:41 -0400 Subject: [PATCH 087/110] optimized return statement --- lib/src/orchestrator/rover_orchestrator.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 8335ae0..cdd71e8 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -148,6 +148,7 @@ class NavigationState extends RoverState { controller.pushState( collection.drive.faceOrientationState(targetOrientation), ); + return; } } @@ -197,7 +198,6 @@ class NavigationState extends RoverState { if (!hasCorrected) { hasCorrected = true; checkCurrentPosition(currentPathState!); - return; } if (!hasFollowed) { hasFollowed = true; From f6aa892777eb4d7538d7323f50a52012006c1800 Mon Sep 17 00:00:00 2001 From: max-mazer Date: Mon, 26 May 2025 15:35:41 -0400 Subject: [PATCH 088/110] optimized return statement --- lib/src/orchestrator/rover_orchestrator.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index cdd71e8..cd1eb77 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -148,7 +148,6 @@ class NavigationState extends RoverState { controller.pushState( collection.drive.faceOrientationState(targetOrientation), ); - return; } } @@ -187,6 +186,8 @@ class NavigationState extends RoverState { } else { checkPosition(state); } + return; + } @override From 92ec58c424f5067134c692c904efc7ab07267d44 Mon Sep 17 00:00:00 2001 From: max-mazer Date: Mon, 26 May 2025 15:47:19 -0400 Subject: [PATCH 089/110] optimized return statement --- lib/src/orchestrator/rover_orchestrator.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index cd1eb77..8e4210b 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -126,7 +126,7 @@ class NavigationState extends RoverState { /// /// If the rover is not facing the proper direction, a new state will be pushed /// to re-correct the rover's orientation - void checkOrientation(AutonomyAStarState state) { + bool checkOrientation(AutonomyAStarState state) { Orientation targetOrientation; // if it has RTK, point towards the next coordinate if (collection.gps.coordinates.hasRTK) { @@ -148,14 +148,16 @@ class NavigationState extends RoverState { controller.pushState( collection.drive.faceOrientationState(targetOrientation), ); + return true; } + return false; } /// Checks if the rover is within a certain distance of [state]'s starting position /// /// If the rover is not within [Constants.replanErrorMeters] of the state's starting /// position, the path will be replanned - void checkPosition(AutonomyAStarState state) { + bool checkPosition(AutonomyAStarState state) { final distanceError = collection.gps.coordinates.distanceTo( state.startPostition, ); @@ -173,6 +175,7 @@ class NavigationState extends RoverState { ), ); } + return false; } /// Check's the position and orientation of [state] before following it @@ -180,14 +183,12 @@ class NavigationState extends RoverState { /// If the instruction of [state] is to move forward, it will check if the /// orientation is correct using [checkOrientation], otherwise, it will check /// the position using [checkPosition] - void checkCurrentPosition(AutonomyAStarState state) { + bool checkCurrentPosition(AutonomyAStarState state) { if (state.instruction == DriveDirection.forward) { - checkOrientation(state); + return checkOrientation(state);; } else { - checkPosition(state); + return checkPosition(state); } - return; - } @override @@ -198,7 +199,8 @@ class NavigationState extends RoverState { } if (!hasCorrected) { hasCorrected = true; - checkCurrentPosition(currentPathState!); + if(checkCurrentPosition(currentPathState!)) return; + } if (!hasFollowed) { hasFollowed = true; From a7e51f6acdf600d39204a29066765f0ad704c92b Mon Sep 17 00:00:00 2001 From: max-mazer Date: Mon, 26 May 2025 15:47:42 -0400 Subject: [PATCH 090/110] small tweak --- lib/src/orchestrator/rover_orchestrator.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 8e4210b..d1b1ace 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -175,7 +175,7 @@ class NavigationState extends RoverState { ), ); } - return false; + return true; } /// Check's the position and orientation of [state] before following it From c1d846df94e450f303ec3f9cb2996c1bb61732b2 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 16:10:19 -0400 Subject: [PATCH 091/110] Fix analysis errors --- lib/src/orchestrator/rover_orchestrator.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index d1b1ace..8a20c29 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -185,7 +185,7 @@ class NavigationState extends RoverState { /// the position using [checkPosition] bool checkCurrentPosition(AutonomyAStarState state) { if (state.instruction == DriveDirection.forward) { - return checkOrientation(state);; + return checkOrientation(state); } else { return checkPosition(state); } @@ -199,8 +199,7 @@ class NavigationState extends RoverState { } if (!hasCorrected) { hasCorrected = true; - if(checkCurrentPosition(currentPathState!)) return; - + if (checkCurrentPosition(currentPathState!)) return; } if (!hasFollowed) { hasFollowed = true; From d7124b9053291cc1e4992ba4121d40f056ed6608 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 16:24:22 -0400 Subject: [PATCH 092/110] Fixed final path position not being traversed - Also replan path if all path states have been traversed --- lib/src/orchestrator/rover_orchestrator.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 8a20c29..b5d1b26 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -207,7 +207,12 @@ class NavigationState extends RoverState { controller.pushState(collection.drive.driveStateState(currentPathState!)); return; } - if (waypointIndex >= 5 || orchestrator.findAndLockObstacles()) { + + orchestrator.traversed.add(currentPathState!.position); + + if (waypointIndex >= orchestrator.currentPath!.length || + waypointIndex >= 5 || + orchestrator.findAndLockObstacles()) { collection.drive.stop(); controller.transitionTo( PathingState( @@ -224,13 +229,18 @@ class NavigationState extends RoverState { return; } - orchestrator.traversed.add(currentPathState!.position); - waypointIndex++; hasCorrected = false; hasFollowed = false; currentPathState = orchestrator.currentPath?[waypointIndex]; } + + @override + void exit() { + if (currentPathState != null) { + orchestrator.traversed.add(currentPathState!.position); + } + } } class RoverOrchestrator extends OrchestratorInterface with ValueReporter { From b88c443fe528d8f5574b3e7bec54acbf1d5f4053 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 17:05:59 -0400 Subject: [PATCH 093/110] Reliability improvements - Use getter for current navigation state --- lib/src/orchestrator/rover_orchestrator.dart | 25 ++++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index b5d1b26..378f1f3 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -100,7 +100,13 @@ class NavigationState extends RoverState { int waypointIndex = 0; /// The current step of the path being followed - AutonomyAStarState? currentPathState; + AutonomyAStarState? get currentPathState { + if (orchestrator.currentPath == null || + waypointIndex >= orchestrator.currentPath!.length) { + return null; + } + return orchestrator.currentPath![waypointIndex]; + } /// Default constructor for [NavigationState] NavigationState( @@ -116,7 +122,6 @@ class NavigationState extends RoverState { hasCorrected = false; hasFollowed = false; - currentPathState = orchestrator.currentPath?[waypointIndex]; orchestrator.currentState = AutonomyState.DRIVING; } @@ -197,10 +202,12 @@ class NavigationState extends RoverState { controller.popState(); return; } + if (!hasCorrected) { hasCorrected = true; if (checkCurrentPosition(currentPathState!)) return; } + if (!hasFollowed) { hasFollowed = true; collection.logger.debug(currentPathState!.toString()); @@ -210,7 +217,7 @@ class NavigationState extends RoverState { orchestrator.traversed.add(currentPathState!.position); - if (waypointIndex >= orchestrator.currentPath!.length || + if (waypointIndex >= orchestrator.currentPath!.length - 1 || waypointIndex >= 5 || orchestrator.findAndLockObstacles()) { collection.drive.stop(); @@ -224,15 +231,10 @@ class NavigationState extends RoverState { ); return; } - if (collection.gps.isNear(destination, Constants.maxErrorMeters)) { - controller.popState(); - return; - } waypointIndex++; hasCorrected = false; hasFollowed = false; - currentPathState = orchestrator.currentPath?[waypointIndex]; } @override @@ -284,8 +286,8 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { @override void onCommandEnd() { - currentPath = null; super.onCommandEnd(); + currentPath = null; } @override @@ -774,12 +776,16 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { timer.cancel(); return; } + if (!controller.hasState()) { currentState = AutonomyState.NO_SOLUTION; onCommandEnd(); timer.cancel(); return; } + + controller.update(); + if (collection.gps.isNear(destination, Constants.maxErrorMeters)) { timer.cancel(); collection.logger.info("Task complete"); @@ -788,7 +794,6 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); return; } - controller.update(); // behaviorRoot.tick(); // if (behaviorRoot.status == NodeStatus.failure) { // behaviorRoot.reset(); From 5d7a6d56a8c0d51f92bf6a5b65f35b9be46a3f08 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 19:09:46 -0400 Subject: [PATCH 094/110] Cleaned up imports --- lib/autonomy.dart | 1 + lib/src/drive/drive_interface.dart | 1 - lib/src/drive/rover_drive.dart | 1 - lib/src/drive/sensor_drive.dart | 3 --- lib/src/drive/sim_drive.dart | 1 - lib/src/drive/timed_drive.dart | 1 - lib/src/gps/rover_gps.dart | 1 + lib/src/imu/imu_interface.dart | 1 + lib/src/imu/rover_imu.dart | 1 + lib/src/imu/sim_imu.dart | 1 + lib/src/orchestrator/orchestrator_interface.dart | 1 - lib/src/orchestrator/rover_orchestrator.dart | 1 - lib/src/utils/imu_utils.dart | 1 - lib/utils.dart | 1 + 14 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/autonomy.dart b/lib/autonomy.dart index 2238505..de71f6b 100644 --- a/lib/autonomy.dart +++ b/lib/autonomy.dart @@ -1,3 +1,4 @@ +export "constants.dart"; export "interfaces.dart"; export "rover.dart"; export "simulator.dart"; diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index a9a3e9c..ec469a4 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -1,5 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:behavior_tree/behavior_tree.dart"; import "drive_config.dart"; diff --git a/lib/src/drive/rover_drive.dart b/lib/src/drive/rover_drive.dart index cdd5000..ab5635a 100644 --- a/lib/src/drive/rover_drive.dart +++ b/lib/src/drive/rover_drive.dart @@ -1,6 +1,5 @@ import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; -import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:behavior_tree/behavior_tree.dart"; import "sensor_drive.dart"; diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 06ebd64..cc800cb 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -1,8 +1,5 @@ import "package:autonomy/autonomy.dart"; -import "package:autonomy/constants.dart"; -import "package:autonomy/interfaces.dart"; import "package:autonomy/src/drive/drive_config.dart"; -import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:autonomy/src/utils/behavior_util.dart"; import "package:behavior_tree/behavior_tree.dart"; diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index 35ce269..b9419a2 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -1,5 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:autonomy/src/utils/behavior_util.dart"; import "package:behavior_tree/behavior_tree.dart"; diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index 2ffbd1e..28b750a 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -2,7 +2,6 @@ import "dart:math"; import "package:autonomy/interfaces.dart"; import "package:autonomy/src/drive/drive_config.dart"; -import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:behavior_tree/behavior_tree.dart"; import "drive_commands.dart"; diff --git a/lib/src/gps/rover_gps.dart b/lib/src/gps/rover_gps.dart index 25e60d0..900062a 100644 --- a/lib/src/gps/rover_gps.dart +++ b/lib/src/gps/rover_gps.dart @@ -3,6 +3,7 @@ import "package:autonomy/interfaces.dart"; class RoverGps extends GpsInterface { final _latitudeCorrector = ErrorCorrector.disabled(); final _longitudeCorrector = ErrorCorrector.disabled(); + RoverGps({required super.collection}); @override diff --git a/lib/src/imu/imu_interface.dart b/lib/src/imu/imu_interface.dart index 38daeb1..15bb4a2 100644 --- a/lib/src/imu/imu_interface.dart +++ b/lib/src/imu/imu_interface.dart @@ -11,6 +11,7 @@ abstract class ImuInterface extends Service with Receiver { CardinalDirection get nearest => CardinalDirection.nearest(raw); void update(Orientation newValue); + @visibleForTesting void forceUpdate(Orientation newValue) {} diff --git a/lib/src/imu/rover_imu.dart b/lib/src/imu/rover_imu.dart index 5a3cf03..8363110 100644 --- a/lib/src/imu/rover_imu.dart +++ b/lib/src/imu/rover_imu.dart @@ -4,6 +4,7 @@ class RoverImu extends ImuInterface { final _xCorrector = ErrorCorrector.disabled(); final _yCorrector = ErrorCorrector.disabled(); final _zCorrector = ErrorCorrector.disabled(); + RoverImu({required super.collection}); @override diff --git a/lib/src/imu/sim_imu.dart b/lib/src/imu/sim_imu.dart index 7c2e570..466b9eb 100644 --- a/lib/src/imu/sim_imu.dart +++ b/lib/src/imu/sim_imu.dart @@ -2,6 +2,7 @@ import "package:autonomy/interfaces.dart"; class ImuSimulator extends ImuInterface with ValueReporter { final RandomError _error; + ImuSimulator({required super.collection, double maxError = 0}) : _error = RandomError(maxError); diff --git a/lib/src/orchestrator/orchestrator_interface.dart b/lib/src/orchestrator/orchestrator_interface.dart index e4d9620..6ccc41c 100644 --- a/lib/src/orchestrator/orchestrator_interface.dart +++ b/lib/src/orchestrator/orchestrator_interface.dart @@ -1,7 +1,6 @@ import "dart:async"; import "package:autonomy/interfaces.dart"; -import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:behavior_tree/behavior_tree.dart"; import "package:meta/meta.dart"; diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 378f1f3..d449323 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -2,7 +2,6 @@ import "dart:math"; import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; -import "package:autonomy/src/fsm/rover_fsm.dart"; import "package:autonomy/src/utils/behavior_util.dart"; import "package:behavior_tree/behavior_tree.dart"; import "dart:async"; diff --git a/lib/src/utils/imu_utils.dart b/lib/src/utils/imu_utils.dart index 06e6365..9b7a539 100644 --- a/lib/src/utils/imu_utils.dart +++ b/lib/src/utils/imu_utils.dart @@ -1,5 +1,4 @@ import "package:autonomy/autonomy.dart"; -import "package:autonomy/constants.dart"; extension OrientationUtils on Orientation { /// North orientation diff --git a/lib/utils.dart b/lib/utils.dart index 813bc37..934bf0b 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,3 +1,4 @@ +export "src/fsm/rover_fsm.dart"; export "src/utils/a_star.dart"; export "src/utils/corrector.dart"; export "src/utils/error.dart"; From 7d7a610f4beeaa2e7dc10da6285f8f6efdd6dfe9 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Mon, 26 May 2025 20:09:58 -0400 Subject: [PATCH 095/110] Fixed formatting --- bin/path.dart | 7 +- bin/task.dart | 6 +- lib/src/detector/rover_detector.dart | 4 +- lib/src/detector/sim_detector.dart | 3 +- lib/src/drive/sensor_drive.dart | 72 ++++----- lib/src/drive/sim_drive.dart | 5 +- lib/src/drive/timed_drive.dart | 168 ++++++++++----------- lib/src/pathfinding/rover_pathfinding.dart | 7 +- lib/src/utils/gps_utils.dart | 12 +- test/fsm_test.dart | 6 +- test/test_util.dart | 7 +- 11 files changed, 154 insertions(+), 143 deletions(-) diff --git a/bin/path.dart b/bin/path.dart index c4c0802..4fd1bb0 100644 --- a/bin/path.dart +++ b/bin/path.dart @@ -4,8 +4,11 @@ import "package:autonomy/simulator.dart"; import "package:coordinate_converter/coordinate_converter.dart"; void main() { - final destination = - UTMCoordinates(x: 5 + 1000, y: 5 + 1000, zoneNumber: 31).toGps(); + final destination = UTMCoordinates( + x: 5 + 1000, + y: 5 + 1000, + zoneNumber: 31, + ).toGps(); final simulator = AutonomySimulator(); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.gps.update(UTMCoordinates(x: 5, y: 5, zoneNumber: 31).toGps()); diff --git a/bin/task.dart b/bin/task.dart index 3736494..621110f 100644 --- a/bin/task.dart +++ b/bin/task.dart @@ -10,12 +10,13 @@ final obstacles = [ // Enter in the Dashboard: Destination = (lat=7, long=0); void main() async { - Logger.level = LogLevel.debug; + Logger.level = LogLevel.trace; final simulator = RoverAutonomy(); simulator.detector = NetworkDetector(collection: simulator); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.orchestrator = RoverOrchestrator(collection: simulator); - simulator.drive = RoverDrive(collection: simulator, useGps: false, useImu: false); + // simulator.drive = RoverDrive(collection: simulator, useGps: false, useImu: false); + simulator.drive = DriveSimulator(collection: simulator, shouldDelay: true); simulator.gps = GpsSimulator(collection: simulator); simulator.imu = ImuSimulator(collection: simulator); simulator.video = VideoSimulator(collection: simulator); @@ -23,5 +24,4 @@ void main() async { await simulator.init(); await simulator.imu.waitForValue(); await simulator.server.waitForConnection(); - } diff --git a/lib/src/detector/rover_detector.dart b/lib/src/detector/rover_detector.dart index ab984b7..307e141 100644 --- a/lib/src/detector/rover_detector.dart +++ b/lib/src/detector/rover_detector.dart @@ -94,8 +94,8 @@ class RoverDetector extends DetectorInterface { final toRemove = temporaryObstacles.where((coordinates) { final delta = coordinates.toUTM() - roverUtm; final roverToPoint = (atan2(delta.y, delta.x) - pi / 2) * 180 / pi; - final relativeAngle = - (collection.imu.heading + roverToPoint).clampHalfAngle(); + final relativeAngle = (collection.imu.heading + roverToPoint) + .clampHalfAngle(); return relativeAngle > -135 && relativeAngle < 135; }); diff --git a/lib/src/detector/sim_detector.dart b/lib/src/detector/sim_detector.dart index 607da56..7cdf293 100644 --- a/lib/src/detector/sim_detector.dart +++ b/lib/src/detector/sim_detector.dart @@ -28,7 +28,8 @@ class DetectorSimulator extends DetectorInterface { final coordinates = collection.gps.coordinates; var result = false; for (final obstacle in obstacles) { - if (!obstacle.isNear(coordinates) || found.contains(obstacle.coordinates)) { + if (!obstacle.isNear(coordinates) || + found.contains(obstacle.coordinates)) { continue; } result = true; diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index cc800cb..d33773d 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -212,25 +212,26 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { collection.logger.info("Driving forward one meter"); setThrottle(config.forwardThrottle); var succeeded = true; - succeeded = await runFeedback(() { - if (!succeeded) return true; - moveForward(); - return collection.gps.isNear( - position, - Constants.intermediateStepTolerance, - ); - // ignore: require_trailing_commas - }, stopNearObstacle: true).timeout( - Constants.driveGPSTimeout, - onTimeout: () { - collection.logger.warning( - "GPS Drive timed out", - body: - "Failed to reach ${position.prettyPrint()} after ${Constants.driveGPSTimeout}", + succeeded = + await runFeedback(() { + if (!succeeded) return true; + moveForward(); + return collection.gps.isNear( + position, + Constants.intermediateStepTolerance, + ); + // ignore: require_trailing_commas + }, stopNearObstacle: true).timeout( + Constants.driveGPSTimeout, + onTimeout: () { + collection.logger.warning( + "GPS Drive timed out", + body: + "Failed to reach ${position.prettyPrint()} after ${Constants.driveGPSTimeout}", + ); + return false; + }, ); - return false; - }, - ); await stop(); return succeeded; } @@ -274,23 +275,24 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { Future spinForAruco(int arucoId, {CameraName? desiredCamera}) async { setThrottle(config.turnThrottle); var foundAruco = true; - foundAruco = await runFeedback(() { - if (!foundAruco) { - return true; - } - spinLeft(); - return collection.video.getArucoDetection( - arucoId, - desiredCamera: desiredCamera, - ) != - null; - }).timeout( - Constants.arucoSearchTimeout, - onTimeout: () { - foundAruco = false; - return false; - }, - ); + foundAruco = + await runFeedback(() { + if (!foundAruco) { + return true; + } + spinLeft(); + return collection.video.getArucoDetection( + arucoId, + desiredCamera: desiredCamera, + ) != + null; + }).timeout( + Constants.arucoSearchTimeout, + onTimeout: () { + foundAruco = false; + return false; + }, + ); await stop(); return foundAruco; } diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index b9419a2..2a79d3d 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -32,7 +32,10 @@ class DriveSimulator extends DriveInterface { required StateInterface child, }) => SequenceState( child.controller, - steps: [DelayedState(child.controller, delayTime: delay), child], + steps: [ + DelayedState(child.controller, delayTime: delay), + child, + ], ); @override diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index 28b750a..c90c680 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -102,52 +102,52 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { ); @override - StateInterface turnStateState(AutonomyAStarState state) => switch (state - .instruction) { - DriveDirection.forward => throw UnimplementedError(), - - DriveDirection.left => _TimedOperationState( - controller, - time: config.turnDelay, - operation: () { - setThrottle(config.turnThrottle); - spinLeft(); - }, - onDone: stopMotors, - ), - - DriveDirection.right => _TimedOperationState( - controller, - time: config.turnDelay, - operation: () { - setThrottle(config.turnThrottle); - spinRight(); - }, - onDone: stopMotors, - ), - - DriveDirection.quarterLeft => _TimedOperationState( - controller, - time: config.turnDelay * 0.5, - operation: () { - setThrottle(config.turnThrottle); - spinLeft(); - }, - onDone: stopMotors, - ), - - DriveDirection.quarterRight => _TimedOperationState( - controller, - time: config.turnDelay * 0.5, - operation: () { - setThrottle(config.turnThrottle); - spinRight(); - }, - onDone: stopMotors, - ), - - DriveDirection.stop => throw UnimplementedError(), - }; + StateInterface turnStateState(AutonomyAStarState state) => + switch (state.instruction) { + DriveDirection.forward => throw UnimplementedError(), + + DriveDirection.left => _TimedOperationState( + controller, + time: config.turnDelay, + operation: () { + setThrottle(config.turnThrottle); + spinLeft(); + }, + onDone: stopMotors, + ), + + DriveDirection.right => _TimedOperationState( + controller, + time: config.turnDelay, + operation: () { + setThrottle(config.turnThrottle); + spinRight(); + }, + onDone: stopMotors, + ), + + DriveDirection.quarterLeft => _TimedOperationState( + controller, + time: config.turnDelay * 0.5, + operation: () { + setThrottle(config.turnThrottle); + spinLeft(); + }, + onDone: stopMotors, + ), + + DriveDirection.quarterRight => _TimedOperationState( + controller, + time: config.turnDelay * 0.5, + operation: () { + setThrottle(config.turnThrottle); + spinRight(); + }, + onDone: stopMotors, + ), + + DriveDirection.stop => throw UnimplementedError(), + }; @override BaseNode driveForwardNode(GpsCoordinates coordinates) => _TimedOperationNode( @@ -161,44 +161,44 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { ); @override - BaseNode turnStateNode(AutonomyAStarState state) => switch (state - .instruction) { - DriveDirection.forward => throw UnimplementedError(), - - DriveDirection.left => _TimedOperationNode( - time: config.turnDelay, - operation: () { - setThrottle(config.turnThrottle); - spinLeft(); - }, - ), - - DriveDirection.right => _TimedOperationNode( - time: config.turnDelay, - operation: () { - setThrottle(config.turnThrottle); - spinRight(); - }, - ), - - DriveDirection.quarterLeft => _TimedOperationNode( - time: config.turnDelay * 0.5, - operation: () { - setThrottle(config.turnThrottle); - spinLeft(); - }, - ), - - DriveDirection.quarterRight => _TimedOperationNode( - time: config.turnDelay * 0.5, - operation: () { - setThrottle(config.turnThrottle); - spinRight(); - }, - ), - - DriveDirection.stop => throw UnimplementedError(), - }; + BaseNode turnStateNode(AutonomyAStarState state) => + switch (state.instruction) { + DriveDirection.forward => throw UnimplementedError(), + + DriveDirection.left => _TimedOperationNode( + time: config.turnDelay, + operation: () { + setThrottle(config.turnThrottle); + spinLeft(); + }, + ), + + DriveDirection.right => _TimedOperationNode( + time: config.turnDelay, + operation: () { + setThrottle(config.turnThrottle); + spinRight(); + }, + ), + + DriveDirection.quarterLeft => _TimedOperationNode( + time: config.turnDelay * 0.5, + operation: () { + setThrottle(config.turnThrottle); + spinLeft(); + }, + ), + + DriveDirection.quarterRight => _TimedOperationNode( + time: config.turnDelay * 0.5, + operation: () { + setThrottle(config.turnThrottle); + spinRight(); + }, + ), + + DriveDirection.stop => throw UnimplementedError(), + }; @override Future init() async => true; diff --git a/lib/src/pathfinding/rover_pathfinding.dart b/lib/src/pathfinding/rover_pathfinding.dart index 6775282..8aa6efc 100644 --- a/lib/src/pathfinding/rover_pathfinding.dart +++ b/lib/src/pathfinding/rover_pathfinding.dart @@ -20,10 +20,9 @@ class RoverPathfinder extends PathfindingInterface { position: previous.position, goal: previous.goal, collection: collection, - instruction: - step.instruction == DriveDirection.quarterLeft - ? DriveDirection.left - : DriveDirection.right, + instruction: step.instruction == DriveDirection.quarterLeft + ? DriveDirection.left + : DriveDirection.right, orientation: step.orientation, depth: step.depth, ); diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index 686b48d..d5dad94 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -10,16 +10,12 @@ typedef GpsMeters = ({num lat, num long}); /// Utility math methods for GpsMeters extension GpsMetersUtil on GpsMeters { /// Add 2 [GpsMeters] together - GpsMeters operator +(GpsMeters other) => ( - lat: lat + other.lat, - long: long + other.long, - ); + GpsMeters operator +(GpsMeters other) => + (lat: lat + other.lat, long: long + other.long); /// Subtract 2 [GpsMeters] from each other - GpsMeters operator -(GpsMeters other) => ( - lat: lat - other.lat, - long: long - other.long, - ); + GpsMeters operator -(GpsMeters other) => + (lat: lat - other.lat, long: long - other.long); } extension GpsUtils on GpsCoordinates { diff --git a/test/fsm_test.dart b/test/fsm_test.dart index 36a1257..8f7639b 100644 --- a/test/fsm_test.dart +++ b/test/fsm_test.dart @@ -127,7 +127,11 @@ void main() => group("[State Machine]", tags: ["fsm"], () { final state2 = TrackingState(controller); final sequence = SequenceState( controller, - steps: [state1, DelayedState(controller, delayTime: delayTime), state2], + steps: [ + state1, + DelayedState(controller, delayTime: delayTime), + state2, + ], ); controller.pushState(sequence); diff --git a/test/test_util.dart b/test/test_util.dart index 506f222..1f64c5d 100644 --- a/test/test_util.dart +++ b/test/test_util.dart @@ -1,8 +1,11 @@ import "package:autonomy/autonomy.dart"; import "package:coordinate_converter/coordinate_converter.dart"; -final GpsCoordinates origin = - UTMCoordinates(x: 5, y: 5, zoneNumber: 31).toGps(); +final GpsCoordinates origin = UTMCoordinates( + x: 5, + y: 5, + zoneNumber: 31, +).toGps(); extension GpsTransformUtil on GpsCoordinates { GpsCoordinates plus({required double x, required double y}) => From 4b1780ee643f97c07646a3aa05d98a84c39a9c3e Mon Sep 17 00:00:00 2001 From: max-mazer Date: Tue, 27 May 2025 15:32:02 -0400 Subject: [PATCH 096/110] i did it this time --- lib/src/orchestrator/rover_orchestrator.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index d449323..f50a10c 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -178,8 +178,9 @@ class NavigationState extends RoverState { destination: destination, ), ); + return true; } - return true; + return false; } /// Check's the position and orientation of [state] before following it From 0dd5ed800e4a21bf9f88c2185c5d78f5a4c401fa Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 27 May 2025 21:04:13 -0400 Subject: [PATCH 097/110] Fix modification while iterating --- lib/src/fsm/fsm_controller.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/fsm/fsm_controller.dart b/lib/src/fsm/fsm_controller.dart index 752b55f..3928bf4 100644 --- a/lib/src/fsm/fsm_controller.dart +++ b/lib/src/fsm/fsm_controller.dart @@ -33,13 +33,17 @@ class FSMController { /// Clears all states from the stack void clearStates() { - for (var i = 0; i < _stateStack.length; i++) { + final originalLength = _stateStack.length; + for (var i = 0; i < originalLength; i++) { popState(); } } /// Pops the latest state from the top of the stack void popState() { + if (_stateStack.isEmpty) { + return; + } _stateStack.removeLast().exit(); } @@ -51,7 +55,8 @@ class FSMController { return; } - for (var i = 0; i < _stateStack.length - index - 1; i++) { + final amountToRemove = _stateStack.length - index - 1; + for (var i = 0; i < amountToRemove; i++) { popState(); } } From 00d3fe15e02db35b737fe4f01c7012e2bf8fef40 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 27 May 2025 21:12:33 -0400 Subject: [PATCH 098/110] Added simulator --- bin/simulator.dart | 15 +++++++++++++++ lib/simulator.dart | 9 +++++++++ 2 files changed, 24 insertions(+) create mode 100644 bin/simulator.dart diff --git a/bin/simulator.dart b/bin/simulator.dart new file mode 100644 index 0000000..91b1999 --- /dev/null +++ b/bin/simulator.dart @@ -0,0 +1,15 @@ +import "package:autonomy/rover.dart"; +import "package:autonomy/simulator.dart"; +import "package:burt_network/burt_network.dart"; + +void main() async { + Logger.level = Level.debug; + final simulator = AutonomySimulator(); + + simulator.pathfinder = RoverPathfinder(collection: simulator); + simulator.orchestrator = RoverOrchestrator(collection: simulator); + simulator.drive = DriveSimulator(collection: simulator, shouldDelay: true); + simulator.detector = NetworkDetector(collection: simulator); + + await simulator.init(); +} diff --git a/lib/simulator.dart b/lib/simulator.dart index b5f1ce4..5e10a26 100644 --- a/lib/simulator.dart +++ b/lib/simulator.dart @@ -1,4 +1,5 @@ export "src/detector/sim_detector.dart"; +export "src/detector/network_detector.dart"; export "src/drive/sim_drive.dart"; export "src/gps/sim_gps.dart"; export "src/imu/sim_imu.dart"; @@ -13,27 +14,35 @@ import "package:burt_network/burt_network.dart"; class AutonomySimulator extends AutonomyInterface { @override late final logger = BurtLogger(socket: server); + @override late final server = RoverSocket( port: 8003, device: Device.AUTONOMY, collection: this, ); + @override late GpsInterface gps = GpsSimulator(collection: this); + @override late ImuInterface imu = ImuSimulator(collection: this); + @override late DriveInterface drive = DriveSimulator(collection: this); + @override late PathfindingInterface pathfinder = PathfindingSimulator(collection: this); + @override late DetectorInterface detector = DetectorSimulator( collection: this, obstacles: [], ); + @override late VideoInterface video = VideoSimulator(collection: this); + @override late OrchestratorInterface orchestrator = OrchestratorSimulator( collection: this, From b6437e0238e0e597528739c69e62a920b55312a2 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 27 May 2025 21:26:54 -0400 Subject: [PATCH 099/110] Rename FSM to state machine --- dart_test.yaml | 2 +- lib/src/drive/drive_interface.dart | 4 ++-- lib/src/fsm/rover_fsm.dart | 9 --------- lib/src/orchestrator/orchestrator_interface.dart | 5 +++-- lib/src/{fsm => state_machine}/decorators.dart | 2 +- lib/src/{fsm => state_machine}/rover_state.dart | 8 ++++---- lib/src/state_machine/rover_state_machine.dart | 9 +++++++++ .../state_controller.dart} | 6 +++--- lib/src/{fsm => state_machine}/state_utils.dart | 2 +- lib/utils.dart | 2 +- test/{fsm_test.dart => state_machine_test.dart} | 6 +++--- 11 files changed, 28 insertions(+), 27 deletions(-) delete mode 100644 lib/src/fsm/rover_fsm.dart rename lib/src/{fsm => state_machine}/decorators.dart (93%) rename lib/src/{fsm => state_machine}/rover_state.dart (77%) create mode 100644 lib/src/state_machine/rover_state_machine.dart rename lib/src/{fsm/fsm_controller.dart => state_machine/state_controller.dart} (92%) rename lib/src/{fsm => state_machine}/state_utils.dart (97%) rename test/{fsm_test.dart => state_machine_test.dart} (95%) diff --git a/dart_test.yaml b/dart_test.yaml index c16bb01..4dae084 100644 --- a/dart_test.yaml +++ b/dart_test.yaml @@ -5,4 +5,4 @@ tags: simulator: orchestrator: network: - fsm: + state_machine: diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index ec469a4..f01795b 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -40,8 +40,8 @@ abstract class DriveInterface extends Service { /// The drive configuration for the rover it is running on DriveConfig config; - /// Getter to access the FSM controller - FSMController get controller => collection.orchestrator.controller; + /// Getter to access the state controller + StateController get controller => collection.orchestrator.controller; /// Constructor for Drive Interface DriveInterface({required this.collection, this.config = roverConfig}); diff --git a/lib/src/fsm/rover_fsm.dart b/lib/src/fsm/rover_fsm.dart deleted file mode 100644 index 5d9e9c8..0000000 --- a/lib/src/fsm/rover_fsm.dart +++ /dev/null @@ -1,9 +0,0 @@ -import "package:autonomy/src/fsm/fsm_controller.dart"; - -export "fsm_controller.dart"; -export "rover_state.dart"; -export "decorators.dart"; -export "state_utils.dart"; - -/// Callback for a state method that takes in a controller -typedef StateCallback = void Function(FSMController controller); diff --git a/lib/src/orchestrator/orchestrator_interface.dart b/lib/src/orchestrator/orchestrator_interface.dart index 6ccc41c..925925a 100644 --- a/lib/src/orchestrator/orchestrator_interface.dart +++ b/lib/src/orchestrator/orchestrator_interface.dart @@ -6,13 +6,14 @@ import "package:meta/meta.dart"; abstract class OrchestratorInterface extends Service { final AutonomyInterface collection; - OrchestratorInterface({required this.collection}); AutonomyCommand? currentCommand; AutonomyState currentState = AutonomyState.AUTONOMY_STATE_UNDEFINED; PeriodicTimer? executionTimer; - FSMController controller = FSMController(); + StateController controller = StateController(); + + OrchestratorInterface({required this.collection}); BaseNode behaviorRoot = Condition(() => true); diff --git a/lib/src/fsm/decorators.dart b/lib/src/state_machine/decorators.dart similarity index 93% rename from lib/src/fsm/decorators.dart rename to lib/src/state_machine/decorators.dart index 28f56b5..e1f927b 100644 --- a/lib/src/fsm/decorators.dart +++ b/lib/src/state_machine/decorators.dart @@ -1,4 +1,4 @@ -import "package:autonomy/src/fsm/rover_fsm.dart"; +import "package:autonomy/src/state_machine/rover_state_machine.dart"; /// State to add a timeout to a state /// diff --git a/lib/src/fsm/rover_state.dart b/lib/src/state_machine/rover_state.dart similarity index 77% rename from lib/src/fsm/rover_state.dart rename to lib/src/state_machine/rover_state.dart index d106bb7..e3b2730 100644 --- a/lib/src/fsm/rover_state.dart +++ b/lib/src/state_machine/rover_state.dart @@ -1,9 +1,9 @@ -import "package:autonomy/src/fsm/rover_fsm.dart"; +import "package:autonomy/src/state_machine/rover_state_machine.dart"; /// Abstracted version of a state abstract class StateInterface { /// The controller for the state machine - FSMController get controller; + StateController get controller; /// Called when the state is initially entered void enter(); @@ -17,10 +17,10 @@ abstract class StateInterface { /// An implementation of [StateInterface] to be used for the rover /// -/// Stores an own internal [FSMController] +/// Stores an own internal [StateController] class RoverState implements StateInterface { @override - final FSMController controller; + final StateController controller; /// Constructor for [RoverState] initializing its controller RoverState(this.controller); diff --git a/lib/src/state_machine/rover_state_machine.dart b/lib/src/state_machine/rover_state_machine.dart new file mode 100644 index 0000000..98a6d46 --- /dev/null +++ b/lib/src/state_machine/rover_state_machine.dart @@ -0,0 +1,9 @@ +import "package:autonomy/src/state_machine/state_controller.dart"; + +export "state_controller.dart"; +export "rover_state.dart"; +export "decorators.dart"; +export "state_utils.dart"; + +/// Callback for a state method that takes in a controller +typedef StateCallback = void Function(StateController controller); diff --git a/lib/src/fsm/fsm_controller.dart b/lib/src/state_machine/state_controller.dart similarity index 92% rename from lib/src/fsm/fsm_controller.dart rename to lib/src/state_machine/state_controller.dart index 3928bf4..3bb82df 100644 --- a/lib/src/fsm/fsm_controller.dart +++ b/lib/src/state_machine/state_controller.dart @@ -1,7 +1,7 @@ -import "package:autonomy/src/fsm/rover_fsm.dart"; +import "package:autonomy/src/state_machine/rover_state_machine.dart"; import "package:meta/meta.dart"; -/// Class to control a Finite State Machine, handling state pushes, +/// Class to control a Stack based State Machine, handling state pushes, /// pops, and transitions /// /// The class maintains a stack of [StateInterface], where the most recent @@ -12,7 +12,7 @@ import "package:meta/meta.dart"; /// states underneath will no longer be updated. /// /// When a state is popped, it is removed from the top of the stack, and will be exited. -class FSMController { +class StateController { @visibleForTesting List get stack => _stateStack; diff --git a/lib/src/fsm/state_utils.dart b/lib/src/state_machine/state_utils.dart similarity index 97% rename from lib/src/fsm/state_utils.dart rename to lib/src/state_machine/state_utils.dart index 5a87781..fbbff6a 100644 --- a/lib/src/fsm/state_utils.dart +++ b/lib/src/state_machine/state_utils.dart @@ -1,4 +1,4 @@ -import "package:autonomy/src/fsm/rover_fsm.dart"; +import "package:autonomy/src/state_machine/rover_state_machine.dart"; /// State that can only be ran for [delayTime] amount of time /// after entering before it automatically pops itself from the diff --git a/lib/utils.dart b/lib/utils.dart index 934bf0b..bf82279 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,4 +1,4 @@ -export "src/fsm/rover_fsm.dart"; +export "src/state_machine/rover_state_machine.dart"; export "src/utils/a_star.dart"; export "src/utils/corrector.dart"; export "src/utils/error.dart"; diff --git a/test/fsm_test.dart b/test/state_machine_test.dart similarity index 95% rename from test/fsm_test.dart rename to test/state_machine_test.dart index 8f7639b..097a9b9 100644 --- a/test/fsm_test.dart +++ b/test/state_machine_test.dart @@ -1,4 +1,4 @@ -import "package:autonomy/src/fsm/rover_fsm.dart"; +import "package:autonomy/src/state_machine/rover_state_machine.dart"; import "package:test/test.dart"; class TrackingState extends RoverState { @@ -18,8 +18,8 @@ class TrackingState extends RoverState { void exit() => exitCount++; } -void main() => group("[State Machine]", tags: ["fsm"], () { - final controller = FSMController(); +void main() => group("[State Machine]", tags: ["state_machine"], () { + final controller = StateController(); test("Pushing and popping", () { final state = TrackingState(controller); From f013b768aed0270b23e6422ffb31ddb92c05c561 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 27 May 2025 21:42:23 -0400 Subject: [PATCH 100/110] Move states to separate folder --- lib/src/drive/sensor_drive.dart | 71 ------ lib/src/orchestrator/rover_orchestrator.dart | 237 ------------------ .../state_machine/rover_state_machine.dart | 5 + .../rover_states/navigation.dart | 180 +++++++++++++ .../state_machine/rover_states/pathing.dart | 63 +++++ .../rover_states/sensor_drive_forward.dart | 35 +++ .../rover_states/sensor_drive_turn.dart | 42 ++++ 7 files changed, 325 insertions(+), 308 deletions(-) create mode 100644 lib/src/state_machine/rover_states/navigation.dart create mode 100644 lib/src/state_machine/rover_states/pathing.dart create mode 100644 lib/src/state_machine/rover_states/sensor_drive_forward.dart create mode 100644 lib/src/state_machine/rover_states/sensor_drive_turn.dart diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index d33773d..e032567 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -1,80 +1,9 @@ import "package:autonomy/autonomy.dart"; -import "package:autonomy/src/drive/drive_config.dart"; import "package:autonomy/src/utils/behavior_util.dart"; import "package:behavior_tree/behavior_tree.dart"; import "drive_commands.dart"; -class SensorForwardState extends RoverState { - final AutonomyInterface collection; - final GpsCoordinates position; - - final RoverDriveCommands drive; - - DriveConfig get config => collection.drive.config; - - SensorForwardState( - super.controller, { - required this.collection, - required this.position, - required this.drive, - }); - - @override - void update() { - drive.setThrottle(config.forwardThrottle); - drive.moveForward(); - - if (collection.gps.isNear(position, Constants.intermediateStepTolerance)) { - controller.popState(); - } - } - - @override - void exit() { - drive.stopMotors(); - } -} - -class SensorTurnState extends RoverState { - final AutonomyInterface collection; - final Orientation orientation; - - final RoverDriveCommands drive; - - DriveConfig get config => collection.drive.config; - - SensorTurnState( - super.controller, { - required this.collection, - required this.orientation, - required this.drive, - }); - - @override - void update() { - drive.setThrottle(config.turnThrottle); - - final current = collection.imu.heading; - final target = orientation.heading; - final error = (target - current).clampHalfAngle(); - if (error < 0) { - drive.spinRight(); - } else { - drive.spinLeft(); - } - - if (collection.imu.isNear(orientation)) { - controller.popState(); - } - } - - @override - void exit() { - drive.stopMotors(); - } -} - /// An implementation of [DriveInterface] that uses the rover's sensors to /// determine its direction to move in and whether or not it has moved in its /// desired direction/orientation diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index f50a10c..34d7639 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -8,243 +8,6 @@ import "dart:async"; import "package:coordinate_converter/coordinate_converter.dart"; -/// State for when the rover is finding a path -/// -/// When ran the state will attempt to plan a path to its given [destination], -/// if successful, it will transition to [NavigationState], if unsuccessful, -/// it will be popped from the stack. -class PathingState extends RoverState { - /// The autonomy collection for the state - final AutonomyInterface collection; - - /// The orchestrator for the state - final RoverOrchestrator orchestrator; - - /// The destination to plan a path to - final GpsCoordinates destination; - - /// Default constructor for [PathingState] - PathingState( - super.controller, { - required this.collection, - required this.orchestrator, - required this.destination, - }); - - @override - void enter() { - orchestrator.currentState = AutonomyState.PATHING; - orchestrator.findAndLockObstacles(); - } - - @override - void update() { - final current = collection.gps.coordinates; - orchestrator.currentPath = collection.pathfinder.getPath( - orchestrator.currentCommand!.destination, - ); - if (orchestrator.currentPath == null) { - collection.logger.error( - "Could not find a path", - body: - "No path found from ${current.prettyPrint()} to ${destination.prettyPrint()}", - ); - controller.popState(); - } else { - collection.logger.debug( - "Found a path from ${current.prettyPrint()} to ${destination.prettyPrint()}: ${orchestrator.currentPath!.length} steps", - ); - collection.logger.debug("Here is a summary of the path"); - for (final step in orchestrator.currentPath!) { - collection.logger.debug(step.toString()); - } - controller.transitionTo( - NavigationState( - controller, - collection: collection, - orchestrator: orchestrator, - destination: destination, - ), - ); - } - } -} - -/// State to manage the navigation of the rover -/// -/// This state should be pushed after [PathingState], as it depends -/// on having a path already made for the rover to follow. -/// -/// This state will manage following each individual step of the path as well -/// as performing necessary corrections and replanning. -/// -/// When the path has to be replanned, this state will transition to [PathingState] -class NavigationState extends RoverState { - /// The collection for the state - final AutonomyInterface collection; - - /// The orchestrator for the state - final RoverOrchestrator orchestrator; - - /// The final destination to navigate to - final GpsCoordinates destination; - - /// Whether or not the state has performed pre-step correction - bool hasCorrected = false; - - /// Whether or not the state has just completed following a path step - bool hasFollowed = false; - - /// The index of the waypoint being followed - int waypointIndex = 0; - - /// The current step of the path being followed - AutonomyAStarState? get currentPathState { - if (orchestrator.currentPath == null || - waypointIndex >= orchestrator.currentPath!.length) { - return null; - } - return orchestrator.currentPath![waypointIndex]; - } - - /// Default constructor for [NavigationState] - NavigationState( - super.controller, { - required this.collection, - required this.orchestrator, - required this.destination, - }); - - @override - void enter() { - waypointIndex = 0; - hasCorrected = false; - hasFollowed = false; - - orchestrator.currentState = AutonomyState.DRIVING; - } - - /// Checks if the rover is oriented properly before driving the [state] - /// - /// This is assuming that the step's instruction is to drive forward. - /// - /// If the rover is not facing the proper direction, a new state will be pushed - /// to re-correct the rover's orientation - bool checkOrientation(AutonomyAStarState state) { - Orientation targetOrientation; - // if it has RTK, point towards the next coordinate - if (collection.gps.coordinates.hasRTK) { - final difference = - state.position.toUTM() - collection.gps.coordinates.toUTM(); - - final angle = atan2(difference.y, difference.x) * 180 / pi; - - targetOrientation = Orientation(z: angle); - } else { - targetOrientation = state.orientation.orientation; - } - - if (!collection.imu.isNear( - targetOrientation, - Constants.driveRealignmentEpsilon, - )) { - collection.logger.info("Re-aligning IMU to correct orientation"); - controller.pushState( - collection.drive.faceOrientationState(targetOrientation), - ); - return true; - } - return false; - } - - /// Checks if the rover is within a certain distance of [state]'s starting position - /// - /// If the rover is not within [Constants.replanErrorMeters] of the state's starting - /// position, the path will be replanned - bool checkPosition(AutonomyAStarState state) { - final distanceError = collection.gps.coordinates.distanceTo( - state.startPostition, - ); - if (distanceError > Constants.replanErrorMeters) { - collection.logger.info( - "Replanning Path", - body: "Rover is $distanceError meters off the path", - ); - controller.transitionTo( - PathingState( - controller, - collection: collection, - orchestrator: orchestrator, - destination: destination, - ), - ); - return true; - } - return false; - } - - /// Check's the position and orientation of [state] before following it - /// - /// If the instruction of [state] is to move forward, it will check if the - /// orientation is correct using [checkOrientation], otherwise, it will check - /// the position using [checkPosition] - bool checkCurrentPosition(AutonomyAStarState state) { - if (state.instruction == DriveDirection.forward) { - return checkOrientation(state); - } else { - return checkPosition(state); - } - } - - @override - void update() { - if (currentPathState == null) { - controller.popState(); - return; - } - - if (!hasCorrected) { - hasCorrected = true; - if (checkCurrentPosition(currentPathState!)) return; - } - - if (!hasFollowed) { - hasFollowed = true; - collection.logger.debug(currentPathState!.toString()); - controller.pushState(collection.drive.driveStateState(currentPathState!)); - return; - } - - orchestrator.traversed.add(currentPathState!.position); - - if (waypointIndex >= orchestrator.currentPath!.length - 1 || - waypointIndex >= 5 || - orchestrator.findAndLockObstacles()) { - collection.drive.stop(); - controller.transitionTo( - PathingState( - controller, - collection: collection, - orchestrator: orchestrator, - destination: destination, - ), - ); - return; - } - - waypointIndex++; - hasCorrected = false; - hasFollowed = false; - } - - @override - void exit() { - if (currentPathState != null) { - orchestrator.traversed.add(currentPathState!.position); - } - } -} - class RoverOrchestrator extends OrchestratorInterface with ValueReporter { /// The GPS coordinates that the rover has traversed during the task final List traversed = []; diff --git a/lib/src/state_machine/rover_state_machine.dart b/lib/src/state_machine/rover_state_machine.dart index 98a6d46..42997d0 100644 --- a/lib/src/state_machine/rover_state_machine.dart +++ b/lib/src/state_machine/rover_state_machine.dart @@ -5,5 +5,10 @@ export "rover_state.dart"; export "decorators.dart"; export "state_utils.dart"; +export "rover_states/navigation.dart"; +export "rover_states/pathing.dart"; +export "rover_states/sensor_drive_forward.dart"; +export "rover_states/sensor_drive_turn.dart"; + /// Callback for a state method that takes in a controller typedef StateCallback = void Function(StateController controller); diff --git a/lib/src/state_machine/rover_states/navigation.dart b/lib/src/state_machine/rover_states/navigation.dart new file mode 100644 index 0000000..e3c8d5f --- /dev/null +++ b/lib/src/state_machine/rover_states/navigation.dart @@ -0,0 +1,180 @@ +import "dart:math"; + +import "package:autonomy/constants.dart"; +import "package:autonomy/interfaces.dart"; +import "package:autonomy/rover.dart"; + +/// State to manage the navigation of the rover +/// +/// This state should be pushed after [PathingState], as it depends +/// on having a path already made for the rover to follow. +/// +/// This state will manage following each individual step of the path as well +/// as performing necessary corrections and replanning. +/// +/// When the path has to be replanned, this state will transition to [PathingState] +class NavigationState extends RoverState { + /// The collection for the state + final AutonomyInterface collection; + + /// The orchestrator for the state + final RoverOrchestrator orchestrator; + + /// The final destination to navigate to + final GpsCoordinates destination; + + /// Whether or not the state has performed pre-step correction + bool hasCorrected = false; + + /// Whether or not the state has just completed following a path step + bool hasFollowed = false; + + /// The index of the waypoint being followed + int waypointIndex = 0; + + /// The current step of the path being followed + AutonomyAStarState? get currentPathState { + if (orchestrator.currentPath == null || + waypointIndex >= orchestrator.currentPath!.length) { + return null; + } + return orchestrator.currentPath![waypointIndex]; + } + + /// Default constructor for [NavigationState] + NavigationState( + super.controller, { + required this.collection, + required this.orchestrator, + required this.destination, + }); + + @override + void enter() { + waypointIndex = 0; + hasCorrected = false; + hasFollowed = false; + + orchestrator.currentState = AutonomyState.DRIVING; + } + + /// Checks if the rover is oriented properly before driving the [state] + /// + /// This is assuming that the step's instruction is to drive forward. + /// + /// If the rover is not facing the proper direction, a new state will be pushed + /// to re-correct the rover's orientation + bool checkOrientation(AutonomyAStarState state) { + Orientation targetOrientation; + // if it has RTK, point towards the next coordinate + if (collection.gps.coordinates.hasRTK) { + final difference = + state.position.toUTM() - collection.gps.coordinates.toUTM(); + + final angle = atan2(difference.y, difference.x) * 180 / pi; + + targetOrientation = Orientation(z: angle); + } else { + targetOrientation = state.orientation.orientation; + } + + if (!collection.imu.isNear( + targetOrientation, + Constants.driveRealignmentEpsilon, + )) { + collection.logger.info("Re-aligning IMU to correct orientation"); + controller.pushState( + collection.drive.faceOrientationState(targetOrientation), + ); + return true; + } + return false; + } + + /// Checks if the rover is within a certain distance of [state]'s starting position + /// + /// If the rover is not within [Constants.replanErrorMeters] of the state's starting + /// position, the path will be replanned + bool checkPosition(AutonomyAStarState state) { + final distanceError = collection.gps.coordinates.distanceTo( + state.startPostition, + ); + if (distanceError > Constants.replanErrorMeters) { + collection.logger.info( + "Replanning Path", + body: "Rover is $distanceError meters off the path", + ); + controller.transitionTo( + PathingState( + controller, + collection: collection, + orchestrator: orchestrator, + destination: destination, + ), + ); + return true; + } + return false; + } + + /// Check's the position and orientation of [state] before following it + /// + /// If the instruction of [state] is to move forward, it will check if the + /// orientation is correct using [checkOrientation], otherwise, it will check + /// the position using [checkPosition] + bool checkCurrentPosition(AutonomyAStarState state) { + if (state.instruction == DriveDirection.forward) { + return checkOrientation(state); + } else { + return checkPosition(state); + } + } + + @override + void update() { + if (currentPathState == null) { + controller.popState(); + return; + } + + if (!hasCorrected) { + hasCorrected = true; + if (checkCurrentPosition(currentPathState!)) return; + } + + if (!hasFollowed) { + hasFollowed = true; + collection.logger.debug(currentPathState!.toString()); + controller.pushState(collection.drive.driveStateState(currentPathState!)); + return; + } + + orchestrator.traversed.add(currentPathState!.position); + + if (waypointIndex >= orchestrator.currentPath!.length - 1 || + waypointIndex >= 5 || + orchestrator.findAndLockObstacles()) { + collection.drive.stop(); + controller.transitionTo( + PathingState( + controller, + collection: collection, + orchestrator: orchestrator, + destination: destination, + ), + ); + return; + } + + waypointIndex++; + hasCorrected = false; + hasFollowed = false; + } + + @override + void exit() { + if (currentPathState != null) { + orchestrator.traversed.add(currentPathState!.position); + } + } +} diff --git a/lib/src/state_machine/rover_states/pathing.dart b/lib/src/state_machine/rover_states/pathing.dart new file mode 100644 index 0000000..d1f07a1 --- /dev/null +++ b/lib/src/state_machine/rover_states/pathing.dart @@ -0,0 +1,63 @@ +import "package:autonomy/autonomy.dart"; + +/// State for when the rover is finding a path +/// +/// When ran the state will attempt to plan a path to its given [destination], +/// if successful, it will transition to [NavigationState], if unsuccessful, +/// it will be popped from the stack. +class PathingState extends RoverState { + /// The autonomy collection for the state + final AutonomyInterface collection; + + /// The orchestrator for the state + final RoverOrchestrator orchestrator; + + /// The destination to plan a path to + final GpsCoordinates destination; + + /// Default constructor for [PathingState] + PathingState( + super.controller, { + required this.collection, + required this.orchestrator, + required this.destination, + }); + + @override + void enter() { + orchestrator.currentState = AutonomyState.PATHING; + orchestrator.findAndLockObstacles(); + } + + @override + void update() { + final current = collection.gps.coordinates; + orchestrator.currentPath = collection.pathfinder.getPath( + orchestrator.currentCommand!.destination, + ); + if (orchestrator.currentPath == null) { + collection.logger.error( + "Could not find a path", + body: + "No path found from ${current.prettyPrint()} to ${destination.prettyPrint()}", + ); + controller.popState(); + } else { + collection.logger.debug( + "Found a path from ${current.prettyPrint()} to ${destination.prettyPrint()}: ${orchestrator.currentPath!.length} steps", + ); + collection.logger.debug("Here is a summary of the path"); + for (final step in orchestrator.currentPath!) { + collection.logger.debug(step.toString()); + } + controller.transitionTo( + NavigationState( + controller, + collection: collection, + orchestrator: orchestrator, + destination: destination, + ), + ); + } + } +} diff --git a/lib/src/state_machine/rover_states/sensor_drive_forward.dart b/lib/src/state_machine/rover_states/sensor_drive_forward.dart new file mode 100644 index 0000000..890b166 --- /dev/null +++ b/lib/src/state_machine/rover_states/sensor_drive_forward.dart @@ -0,0 +1,35 @@ +import "package:autonomy/constants.dart"; +import "package:autonomy/interfaces.dart"; +import "package:autonomy/src/drive/drive_commands.dart"; +import "package:autonomy/src/drive/drive_config.dart"; + +class SensorForwardState extends RoverState { + final AutonomyInterface collection; + final GpsCoordinates position; + + final RoverDriveCommands drive; + + DriveConfig get config => collection.drive.config; + + SensorForwardState( + super.controller, { + required this.collection, + required this.position, + required this.drive, + }); + + @override + void update() { + drive.setThrottle(config.forwardThrottle); + drive.moveForward(); + + if (collection.gps.isNear(position, Constants.intermediateStepTolerance)) { + controller.popState(); + } + } + + @override + void exit() { + drive.stopMotors(); + } +} diff --git a/lib/src/state_machine/rover_states/sensor_drive_turn.dart b/lib/src/state_machine/rover_states/sensor_drive_turn.dart new file mode 100644 index 0000000..e3bd8b5 --- /dev/null +++ b/lib/src/state_machine/rover_states/sensor_drive_turn.dart @@ -0,0 +1,42 @@ +import "package:autonomy/interfaces.dart"; +import "package:autonomy/src/drive/drive_commands.dart"; +import "package:autonomy/src/drive/drive_config.dart"; + +class SensorTurnState extends RoverState { + final AutonomyInterface collection; + final Orientation orientation; + + final RoverDriveCommands drive; + + DriveConfig get config => collection.drive.config; + + SensorTurnState( + super.controller, { + required this.collection, + required this.orientation, + required this.drive, + }); + + @override + void update() { + drive.setThrottle(config.turnThrottle); + + final current = collection.imu.heading; + final target = orientation.heading; + final error = (target - current).clampHalfAngle(); + if (error < 0) { + drive.spinRight(); + } else { + drive.spinLeft(); + } + + if (collection.imu.isNear(orientation)) { + controller.popState(); + } + } + + @override + void exit() { + drive.stopMotors(); + } +} From 1ba6576101eb611a73bfa5192188402e6c1a1b27 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Tue, 27 May 2025 22:11:20 -0400 Subject: [PATCH 101/110] Remove behavior trees --- lib/src/drive/drive_interface.dart | 30 -- lib/src/drive/rover_drive.dart | 42 -- lib/src/drive/sensor_drive.dart | 56 --- lib/src/drive/sim_drive.dart | 33 -- lib/src/drive/timed_drive.dart | 86 ---- .../orchestrator/orchestrator_interface.dart | 3 - lib/src/orchestrator/rover_orchestrator.dart | 408 +----------------- lib/src/utils/behavior_util.dart | 153 ------- pubspec.yaml | 1 - 9 files changed, 7 insertions(+), 805 deletions(-) delete mode 100644 lib/src/utils/behavior_util.dart diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index f01795b..4cb5b56 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -1,5 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:behavior_tree/behavior_tree.dart"; import "drive_config.dart"; @@ -72,35 +71,6 @@ abstract class DriveInterface extends Service { StateInterface turnStateState(AutonomyAStarState state) => faceOrientationState(state.orientation.orientation); - BaseNode driveStateNode(AutonomyAStarState state) { - if (state.instruction == DriveDirection.stop) { - return Task(() { - stop(); - return NodeStatus.success; - }); - } else if (state.instruction == DriveDirection.forward) { - return driveForwardNode(state.position); - } else { - return turnStateNode(state); - } - } - - BaseNode resolveOrientationNode() => - faceDirectionNode(collection.imu.nearest); - - BaseNode driveForwardNode(GpsCoordinates coordinates); - - BaseNode faceDirectionNode(CardinalDirection direction) => - faceOrientationNode(direction.orientation); - - BaseNode faceOrientationNode(Orientation orientation); - - BaseNode turnStateNode(AutonomyAStarState state) => - faceOrientationNode(state.orientation.orientation); - - BaseNode spinForArucoNode(int arucoId, {CameraName? desiredCamera}) => - Condition(() => false); - /// Stop the rover Future stop(); diff --git a/lib/src/drive/rover_drive.dart b/lib/src/drive/rover_drive.dart index ab5635a..9106471 100644 --- a/lib/src/drive/rover_drive.dart +++ b/lib/src/drive/rover_drive.dart @@ -1,6 +1,5 @@ import "package:autonomy/interfaces.dart"; import "package:autonomy/rover.dart"; -import "package:behavior_tree/behavior_tree.dart"; import "sensor_drive.dart"; import "timed_drive.dart"; @@ -105,47 +104,6 @@ class RoverDrive extends DriveInterface { } } - @override - BaseNode driveForwardNode(GpsCoordinates coordinates) { - if (useGps) { - return sensorDrive.driveForwardNode(coordinates); - } else { - return Sequence( - children: [ - timedDrive.driveForwardNode(coordinates), - simDrive.driveForwardNode(coordinates), - ], - ); - } - } - - @override - BaseNode turnStateNode(AutonomyAStarState state) { - if (useImu) { - return sensorDrive.turnStateNode(state); - } else { - return Sequence( - children: [ - timedDrive.turnStateNode(state), - simDrive.turnStateNode(state), - ], - ); - } - } - - @override - BaseNode faceOrientationNode(Orientation orientation) { - if (useImu) { - return sensorDrive.faceOrientationNode(orientation); - } else { - return simDrive.faceOrientationNode(orientation); - } - } - - @override - BaseNode spinForArucoNode(int arucoId, {CameraName? desiredCamera}) => - sensorDrive.spinForArucoNode(arucoId, desiredCamera: desiredCamera); - @override StateInterface driveForwardState(GpsCoordinates coordinates) { if (useGps) { diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index e032567..2f845f5 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -1,6 +1,4 @@ import "package:autonomy/autonomy.dart"; -import "package:autonomy/src/utils/behavior_util.dart"; -import "package:behavior_tree/behavior_tree.dart"; import "drive_commands.dart"; @@ -39,60 +37,6 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { drive: this, ); - @override - BaseNode driveForwardNode(GpsCoordinates coordinates) => Selector( - children: [ - Condition( - () => collection.gps.isNear( - coordinates, - Constants.intermediateStepTolerance, - ), - ), - Task(() { - if (collection.pathfinder.isObstacle(collection.gps.coordinates)) { - return NodeStatus.failure; - } - setThrottle(config.forwardThrottle); - moveForward(); - return NodeStatus.running; - }), - ], - ).withTimeout(Constants.driveGPSTimeout); - - @override - BaseNode faceOrientationNode(Orientation orientation) => Selector( - children: [ - Condition( - () => collection.imu.isNear(orientation, Constants.turnEpsilon), - ), - Task(() { - setThrottle(config.turnThrottle); - _tryToFace(orientation); - return NodeStatus.running; - }), - ], - ); - - @override - BaseNode spinForArucoNode(int arucoId, {CameraName? desiredCamera}) => - Selector( - children: [ - Condition( - () => - collection.video.getArucoDetection( - arucoId, - desiredCamera: desiredCamera, - ) != - null, - ), - Task(() { - setThrottle(config.turnThrottle); - spinLeft(); - return NodeStatus.running; - }), - ], - ).withTimeout(Constants.arucoSearchTimeout); - @override Future stop() async { stopMotors(); diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index 2a79d3d..06dac18 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -1,6 +1,4 @@ import "package:autonomy/interfaces.dart"; -import "package:autonomy/src/utils/behavior_util.dart"; -import "package:behavior_tree/behavior_tree.dart"; /// An implementation of [DriveInterface] that will not move the rover, /// and only update its sensor readings based on the desired values @@ -22,11 +20,6 @@ class DriveSimulator extends DriveInterface { super.config, }); - BaseNode _delayAndExecuteNode({ - required Duration delay, - required BaseNode child, - }) => Sequence(children: [DelayedNode(delay), child]); - RoverState _delayAndExecuteState({ required Duration delay, required StateInterface child, @@ -64,32 +57,6 @@ class DriveSimulator extends DriveInterface { ), ); - @override - BaseNode driveForwardNode(GpsCoordinates coordinates) { - final updateGps = Task(() { - collection.gps.update(coordinates); - return NodeStatus.success; - }); - return ConditionalNode( - condition: () => shouldDelay, - onTrue: _delayAndExecuteNode(delay: delay, child: updateGps), - onFalse: updateGps, - ); - } - - @override - BaseNode faceOrientationNode(Orientation orientation) { - final updateImu = Task(() { - collection.imu.update(orientation); - return NodeStatus.success; - }); - return ConditionalNode( - condition: () => shouldDelay, - onTrue: _delayAndExecuteNode(delay: delay, child: updateImu), - onFalse: updateImu, - ); - } - @override Future init() async => true; diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index c90c680..629ea44 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -2,7 +2,6 @@ import "dart:math"; import "package:autonomy/interfaces.dart"; import "package:autonomy/src/drive/drive_config.dart"; -import "package:behavior_tree/behavior_tree.dart"; import "drive_commands.dart"; @@ -38,33 +37,6 @@ class _TimedOperationState extends RoverState { void exit() => onDone?.call(); } -class _TimedOperationNode extends BaseNode { - DateTime? _start; - - final Duration time; - final void Function() operation; - - _TimedOperationNode({required this.time, required this.operation}); - - @override - void tick() { - _start ??= DateTime.now(); - status = NodeStatus.running; - - operation(); - - if (DateTime.now().difference(_start!) >= time) { - status = NodeStatus.success; - } - } - - @override - void reset() { - _start = null; - super.reset(); - } -} - /// An implementation of [DriveInterface] that drives for a specified amount of time without using sensors /// /// The time to drive/turn for is defined by [DriveConfig.oneMeterDelay] and [DriveConfig.turnDelay] @@ -73,13 +45,6 @@ class _TimedOperationNode extends BaseNode { class TimedDrive extends DriveInterface with RoverDriveCommands { TimedDrive({required super.collection, super.config}); - @override - BaseNode faceOrientationNode(Orientation orientation) { - throw UnsupportedError( - "Cannot face any arbitrary direction using TimedDrive", - ); - } - @override StateInterface faceOrientationState(Orientation orientation) { throw UnsupportedError( @@ -149,57 +114,6 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { DriveDirection.stop => throw UnimplementedError(), }; - @override - BaseNode driveForwardNode(GpsCoordinates coordinates) => _TimedOperationNode( - time: - config.oneMeterDelay * - (collection.imu.nearest.isPerpendicular ? 1 : sqrt2), - operation: () { - setThrottle(config.forwardThrottle); - moveForward(); - }, - ); - - @override - BaseNode turnStateNode(AutonomyAStarState state) => - switch (state.instruction) { - DriveDirection.forward => throw UnimplementedError(), - - DriveDirection.left => _TimedOperationNode( - time: config.turnDelay, - operation: () { - setThrottle(config.turnThrottle); - spinLeft(); - }, - ), - - DriveDirection.right => _TimedOperationNode( - time: config.turnDelay, - operation: () { - setThrottle(config.turnThrottle); - spinRight(); - }, - ), - - DriveDirection.quarterLeft => _TimedOperationNode( - time: config.turnDelay * 0.5, - operation: () { - setThrottle(config.turnThrottle); - spinLeft(); - }, - ), - - DriveDirection.quarterRight => _TimedOperationNode( - time: config.turnDelay * 0.5, - operation: () { - setThrottle(config.turnThrottle); - spinRight(); - }, - ), - - DriveDirection.stop => throw UnimplementedError(), - }; - @override Future init() async => true; diff --git a/lib/src/orchestrator/orchestrator_interface.dart b/lib/src/orchestrator/orchestrator_interface.dart index 925925a..1c2a291 100644 --- a/lib/src/orchestrator/orchestrator_interface.dart +++ b/lib/src/orchestrator/orchestrator_interface.dart @@ -1,7 +1,6 @@ import "dart:async"; import "package:autonomy/interfaces.dart"; -import "package:behavior_tree/behavior_tree.dart"; import "package:meta/meta.dart"; abstract class OrchestratorInterface extends Service { @@ -15,8 +14,6 @@ abstract class OrchestratorInterface extends Service { OrchestratorInterface({required this.collection}); - BaseNode behaviorRoot = Condition(() => true); - Future onCommand(AutonomyCommand command) async { collection.server.sendMessage(command); if (command.abort) return abort(); diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 34d7639..5f986f7 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -2,8 +2,6 @@ import "dart:math"; import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; -import "package:autonomy/src/utils/behavior_util.dart"; -import "package:behavior_tree/behavior_tree.dart"; import "dart:async"; import "package:coordinate_converter/coordinate_converter.dart"; @@ -18,18 +16,6 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { /// Whether or not the rover should replan the path, this is managed by the behavior tree bool replanPath = true; - /// The current waypoint index of the path that the rover is following - int waypointIndex = 0; - - /// Whether or not the rover has checked the waypoint orientation of the current path step - bool hasCheckedWaypointOrientation = false; - - /// Whether or not the rover is currently correction waypoint orientation - bool isCorrectingWaypointOrientation = false; - - /// Whether or not the rover has checked the waypoint error for the current step in the path - bool hasCheckedWaypointError = false; - RoverOrchestrator({required super.collection}); @override @@ -103,296 +89,6 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { return true; } - /// A node that will trigger a path replan when [condition] is true - /// - /// If [condition] is true, [replanPath] will be set to true, and the - /// node will fail. Otherwise, it will be successful. - BaseNode replanOnCondition(bool Function() condition) => Task(() { - if (condition()) { - replanPath = true; - return NodeStatus.failure; - } - return NodeStatus.success; - }); - - /// A node to plan a path towards [destination] - /// - /// This node will only create a new path towards [destination], and not follow it. - /// - /// This node will fail if either: - /// 1. There is a current path already planned - /// 2. There is no command currently running - /// 3. The GPS hasn't received a value - /// 4. The IMU hasn't received a value - /// 5. A path could not be planned - /// Otherwise, this node will be successful - /// - /// Since this node will fail if a path is already planned, - /// this should be wrapped in a decorator such as a selector - /// to prevent the entire tree from failing. - BaseNode planPath(GpsCoordinates destination) => Sequence( - children: [ - Condition( - () => - replanPath && - currentCommand != null && - collection.gps.hasValue && - collection.imu.hasValue, - ), - Task(() { - collection.logger.debug("Finding any new obstacles"); - findAndLockObstacles(); - return NodeStatus.success; - }), - Task(() { - collection.logger.debug("Finding a path"); - currentState = AutonomyState.PATHING; - replanPath = false; - return NodeStatus.success; - }), - Condition(() { - if (currentCommand == null) { - return false; - } - final current = collection.gps.coordinates; - currentPath = collection.pathfinder.getPath( - currentCommand!.destination, - ); - if (currentPath == null) { - collection.logger.error( - "Could not find a path", - body: - "No path found from ${current.prettyPrint()} to ${destination.prettyPrint()}", - ); - } else { - waypointIndex = 0; - hasCheckedWaypointError = false; - hasCheckedWaypointOrientation = false; - isCorrectingWaypointOrientation = false; - collection.logger.debug( - "Found a path from ${current.prettyPrint()} to ${destination.prettyPrint()}: ${currentPath!.length} steps", - ); - collection.logger.debug("Here is a summary of the path"); - for (final step in currentPath!) { - collection.logger.debug(step.toString()); - } - } - return currentPath != null; - }), - ], - ); - - /// Creates a node to follow a path towards [destination] - /// - /// This node will handle the logic of driving through the individual steps - /// of [currentPath], handling obstacle detection, replanning logic, and recorrection. - /// - /// This node will not plan a new path, see [planPath] - /// - /// This node will fail if either: - /// 1. There is no path planned - /// 2. The node for following the current path step failed - /// 3. A new obstacle was detected - /// 4. 5 steps of the path have been followed - /// 5. A step of the path was completed but the rover has not reached [destination] - /// - /// Since this node will fail if the rover isn't near [destination], this - /// should be wrapped in a decorator such as a selector or inverter to prevent - /// the entire tree from failing. - BaseNode followPath(GpsCoordinates destination) { - late AutonomyAStarState currentWaypoint; - // Orientation the rover should be facing before driving forward - var targetOrientation = collection.imu.nearest.orientation; - - return Sequence( - children: [ - Task(() { - if (currentPath == null) { - return NodeStatus.failure; - } - if (waypointIndex >= currentPath!.length) { - return NodeStatus.failure; - } - currentWaypoint = currentPath![waypointIndex]; - currentState = AutonomyState.DRIVING; - - if (!hasCheckedWaypointOrientation && - !isCorrectingWaypointOrientation) { - // if it has RTK, point towards the next coordinate - if (collection.gps.coordinates.hasRTK) { - final difference = - currentWaypoint.position.toUTM() - - collection.gps.coordinates.toUTM(); - - final angle = atan2(difference.y, difference.x) * 180 / pi; - - targetOrientation = Orientation(z: angle); - } else { - targetOrientation = currentWaypoint.orientation.orientation; - } - } - - return NodeStatus.success; - }), - Selector( - children: [ - Condition(() { - if (isCorrectingWaypointOrientation) { - return true; - } - if (!hasCheckedWaypointOrientation) { - return currentWaypoint.instruction == DriveDirection.forward && - (collection.imu.heading - targetOrientation.z) - .clampHalfAngle() - .abs() >= - Constants.driveRealignmentEpsilon; - } - return false; - }).inverted, - - // If the previous one fails, then it has to recorrect, run a - // task to set the state of recorrecting, this is inverted so - // it will continue with the selection - Task(() { - isCorrectingWaypointOrientation = true; - return NodeStatus.success; - }).inverted, - - // Face the desired orientation - SuppliedNode( - key: () => targetOrientation, - () => collection.drive.faceOrientationNode(targetOrientation), - ), - ], - ), - - // If it makes it through here, it either has corrected, or doesn't need to correct. - // Either way, assume that it has corrected its orientation - Task(() { - hasCheckedWaypointOrientation = true; - isCorrectingWaypointOrientation = false; - return NodeStatus.success; - }), - - // If the distance to the start of our waypoint is too large, replan - replanOnCondition(() { - if (!hasCheckedWaypointError) { - hasCheckedWaypointError = true; - return collection.gps.coordinates.distanceTo( - currentWaypoint.startPostition, - ) >= - Constants.replanErrorMeters; - } - return false; - }), - // ConditionalNode( - // condition: () { - // if (currentWaypoint.instruction != DriveDirection.forward) { - // return false; - // } - // return (collection.imu.heading - targetOrientation.z) - // .clampHalfAngle() > - // Constants.driveRealignmentEpsilon; - // }, - // onTrue: SuppliedNode( - // key: () => targetOrientation, - // () => collection.drive.faceOrientationNode(targetOrientation), - // ), - // ), - SuppliedNode( - key: () => waypointIndex, - () => collection.drive.driveStateNode(currentWaypoint), - ), - - // If the waypoint state has been driven, increase our waypoint index, - // and reset our correction state - Task(() { - traversed.add(currentWaypoint.position); - waypointIndex++; - hasCheckedWaypointOrientation = false; - isCorrectingWaypointOrientation = false; - hasCheckedWaypointError = false; - return NodeStatus.success; - }), - - // Replan if there are new obstacles or we've traversed 5 waypoints, - // we want to periodically replan the path to ensure we're on track - replanOnCondition(() => findAndLockObstacles() || waypointIndex >= 5), - - // This technically isn't needed for following the path, however, the tree root - // relies on the tree not being "success" until we have reached our destination, - // adding this here guarantees that this node will only be successful if we are - // done following - Condition( - () => collection.gps.isNear(destination, Constants.maxErrorMeters), - ), - ], - ); - } - - /// Creates a node to plan and follow a path towards [destination] - /// - /// This node combines [planPath] and [followPath] to dynamically plan - /// and follow a path to drive the rover towards [destination]. - /// - /// If a path could not be planned towards [destination], the node will - /// fail. If the rover has reached [destination], it will succeed, otherwise, - /// it will return running. - BaseNode pathToDestination(GpsCoordinates destination) { - var resolvedOrientation = false; - return Sequence( - children: [ - // If we haven't initially resolved our orientation, resolve the orientation - Selector( - children: [ - Condition(() => resolvedOrientation), - SuppliedNode(() => collection.drive.resolveOrientationNode()), - ], - ), - Task(() { - resolvedOrientation = true; - return NodeStatus.success; - }), - Selector( - children: [ - // Success if we are near the destination - Condition( - () => - collection.gps.isNear(destination, Constants.maxErrorMeters), - ), - - // Plan the path, if there is already a path, - // this will fail and the selection will continue - planPath(destination), - - // Follow the path, this will fail if it hasn't reached the destination, - // this failure scenario is "caught" in the next step - followPath(destination), - - // Only runs if plan path failed, and follow path failed, indicating 2 scenarios: - // 1. Couldn't find a path at all - // 2. Couldn't follow a specific step of the path - Task(() { - // Failed to find a path (Scenario 1) - if (!replanPath && currentPath == null) { - return NodeStatus.failure; - } else { - // Either a timeout or new obstacle was found, replan path and continue (Scenario 2) - return NodeStatus.running; - } - }), - ], - ), - Task(() { - if (collection.gps.isNear(destination, Constants.maxErrorMeters)) { - return NodeStatus.success; - } - return NodeStatus.running; - }), - ], - ); - } - Future calculateAndFollowPath( GpsCoordinates goal, { bool abortOnError = true, @@ -509,12 +205,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { ); traversed.clear(); collection.drive.setLedStrip(ProtoColor.RED); - waypointIndex = 0; - hasCheckedWaypointError = false; - hasCheckedWaypointOrientation = false; - isCorrectingWaypointOrientation = false; - replanPath = true; - behaviorRoot = pathToDestination(destination); + replanPath = false; controller.pushState( SequenceState( controller, @@ -557,20 +248,6 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); return; } - // behaviorRoot.tick(); - // if (behaviorRoot.status == NodeStatus.failure) { - // behaviorRoot.reset(); - // currentState = AutonomyState.NO_SOLUTION; - // currentCommand = null; - // timer.cancel(); - // } else if (behaviorRoot.status == NodeStatus.success) { - // behaviorRoot.reset(); - // timer.cancel(); - // collection.logger.info("Task complete"); - // currentState = AutonomyState.AT_DESTINATION; - // collection.drive.setLedStrip(ProtoColor.GREEN, blink: true); - // currentCommand = null; - // } }); // detect obstacles before and after resolving orientation, as a "scan" // collection.detector.findObstacles(); @@ -591,77 +268,6 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { DetectedObject? detectedAruco; - behaviorRoot = Sequence( - children: [ - // Go to initial coordinates given - Selector( - children: [ - Condition( - () => - command.destination != - GpsCoordinates(latitude: 0, longitude: 0), - ).inverted, - pathToDestination(command.destination), - // If failed to reach - Task(() { - collection.logger.error( - "Failed to follow path towards initial destination", - ); - currentState = AutonomyState.NO_SOLUTION; - currentCommand = null; - return NodeStatus.failure; - }), - ], - ), - Task(() { - currentState = AutonomyState.SEARCHING; - collection.logger.info("Searching for ArUco tag"); - return NodeStatus.success; - }), - // Try to spin and find a tag - Selector( - children: [ - collection.drive.spinForArucoNode( - command.arucoId, - desiredCamera: Constants.arucoDetectionCamera, - ), - Task(() { - collection.logger.error("Could not find desired Aruco tag"); - currentState = AutonomyState.NO_SOLUTION; - currentCommand = null; - return NodeStatus.failure; - }), - ], - ), - Condition(() { - detectedAruco = collection.video.getArucoDetection( - command.arucoId, - desiredCamera: Constants.arucoDetectionCamera, - ); - return detectedAruco != null; - }), - // Face towards aruco tag - SuppliedNode( - () => collection.drive.faceOrientationNode( - Orientation(z: collection.imu.heading - detectedAruco!.yaw), - ), - ), - Selector( - children: [ - Condition( - () => - collection.video.getArucoDetection( - command.arucoId, - desiredCamera: Constants.arucoDetectionCamera, - ) != - null, - ), - Task(() => NodeStatus.running), - ], - ).withTimeout(const Duration(seconds: 3)), - ], - ); - if (command.destination != GpsCoordinates(latitude: 0, longitude: 0)) { if (!await calculateAndFollowPath( command.destination, @@ -697,7 +303,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.logger.info("Found aruco"); currentState = AutonomyState.APPROACHING; final arucoOrientation = Orientation( - z: collection.imu.heading - detectedAruco!.yaw, + z: collection.imu.heading - detectedAruco.yaw, ); await collection.drive.faceOrientation(arucoOrientation); detectedAruco = await collection.video.waitForAruco( @@ -706,7 +312,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { timeout: const Duration(seconds: 3), ); - if (detectedAruco == null || !detectedAruco!.hasBestPnpResult()) { + if (detectedAruco == null || !detectedAruco.hasBestPnpResult()) { // TODO: handle this condition properly collection.logger.error( "Could not find desired Aruco tag after rotating towards it", @@ -718,7 +324,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { collection.logger.debug( "Planning path to Aruco ID ${command.arucoId}", - body: "Detection: ${detectedAruco!.toProto3Json()}", + body: "Detection: ${detectedAruco.toProto3Json()}", ); // In theory we could just find the relative position with the translation x and z, @@ -726,7 +332,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { // when facing it head on), then it will be extremely innacurate. Since the SolvePnP's // distance is always extremely accurate, it is more reliable to use the distance // hypotenuse to the camera combined with trig of the tag's angle relative to the camera. - final cameraToTag = detectedAruco!.bestPnpResult.cameraToTarget; + final cameraToTag = detectedAruco.bestPnpResult.cameraToTarget; final distanceToTag = sqrt( pow(cameraToTag.translation.z, 2) + pow(cameraToTag.translation.x, 2), @@ -743,10 +349,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { final relativeX = -distanceToTag * - sin((collection.imu.heading - detectedAruco!.yaw) * pi / 180); + sin((collection.imu.heading - detectedAruco.yaw) * pi / 180); final relativeY = distanceToTag * - cos((collection.imu.heading - detectedAruco!.yaw) * pi / 180); + cos((collection.imu.heading - detectedAruco.yaw) * pi / 180); final destinationCoordinates = (collection.gps.coordinates.toUTM() + diff --git a/lib/src/utils/behavior_util.dart b/lib/src/utils/behavior_util.dart deleted file mode 100644 index 4db780f..0000000 --- a/lib/src/utils/behavior_util.dart +++ /dev/null @@ -1,153 +0,0 @@ -import "package:behavior_tree/behavior_tree.dart"; - -class TimeLimit extends BaseNode { - DateTime? _start; - - final BaseNode child; - final Duration timeout; - final NodeStatus statusAfterTimeout; - - TimeLimit({ - required this.child, - required this.timeout, - this.statusAfterTimeout = NodeStatus.failure, - }); - - @override - void tick() { - _start ??= DateTime.now(); - - if (DateTime.now().difference(_start!) >= timeout) { - status = statusAfterTimeout; - return; - } - - child.tick(); - status = child.status; - } - - @override - void reset() { - _start = null; - child.reset(); - - super.reset(); - } -} - -class DelayedNode extends BaseNode { - DateTime? _start; - - final Duration delay; - - DelayedNode(this.delay); - - @override - void tick() { - _start ??= DateTime.now(); - - if (DateTime.now().difference(_start!) >= delay) { - status = NodeStatus.success; - } else { - status = NodeStatus.running; - } - } - - @override - void reset() { - _start = null; - super.reset(); - } -} - -class ConditionalNode extends BaseNode { - final bool Function() condition; - - final BaseNode? onTrue; - final BaseNode? onFalse; - - bool? _initialConditionState; - - ConditionalNode({required this.condition, this.onTrue, this.onFalse}) - : assert( - onTrue != null || onFalse != null, - "onTrue() or onFalse() must be non-null, if there is no intended child node, use a Condition() instead!", - ); - - @override - void tick() { - _initialConditionState ??= condition(); - - if (_initialConditionState!) { - if (onTrue != null) { - onTrue!.tick(); - status = onTrue!.status; - } else if (onFalse != null) { - status = NodeStatus.success; - } - } else { - if (onFalse != null) { - onFalse!.tick(); - status = onFalse!.status; - } else if (onTrue != null) { - status = NodeStatus.success; - } - } - } - - @override - void reset() { - _initialConditionState = null; - onTrue?.reset(); - onFalse?.reset(); - super.reset(); - } -} - -class SuppliedNode extends BaseNode { - BaseNode? _child; - - final BaseNode Function() supplier; - final Object? Function()? key; - - Object? _lastKey; - - SuppliedNode(this.supplier, {this.key}) : _lastKey = key?.call(); - - @override - void tick() { - if (_child == null) { - _child = supplier(); - } else if (key != null) { - final currentKey = key!.call(); - if (currentKey != _lastKey) { - _child?.reset(); - _child = supplier(); - } - _lastKey = currentKey; - } - - _child!.tick(); - status = _child!.status; - } - - @override - void reset() { - _child?.reset(); - _child = null; - super.reset(); - } -} - -extension BehaviorDecorators on BaseNode { - BaseNode get inverted => Inverter(this); - - BaseNode withTimeout( - Duration timeout, { - NodeStatus statusAfterTimeout = NodeStatus.failure, - }) => TimeLimit( - child: this, - timeout: timeout, - statusAfterTimeout: statusAfterTimeout, - ); -} diff --git a/pubspec.yaml b/pubspec.yaml index 71c866d..6d60ec4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,6 @@ dependencies: meta: ^1.11.0 collection: ^1.19.1 coordinate_converter: ^1.1.2 - behavior_tree: ^0.1.3+2 dev_dependencies: test: ^1.21.0 From fe1c767bde8cba11a487880435974d31048b5f8a Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Wed, 28 May 2025 00:36:57 -0400 Subject: [PATCH 102/110] Realistic simulation! --- bin/simulator.dart | 11 +++- bin/task.dart | 7 ++- lib/src/drive/drive_config.dart | 15 +++++ lib/src/drive/drive_interface.dart | 5 ++ lib/src/drive/sim_drive.dart | 59 +++++++++---------- .../state_machine/rover_state_machine.dart | 2 + .../simulation_drive_forward.dart | 58 ++++++++++++++++++ .../rover_states/simulation_drive_turn.dart | 53 +++++++++++++++++ lib/src/utils/gps_utils.dart | 8 +-- 9 files changed, 179 insertions(+), 39 deletions(-) create mode 100644 lib/src/state_machine/rover_states/simulation_drive_forward.dart create mode 100644 lib/src/state_machine/rover_states/simulation_drive_turn.dart diff --git a/bin/simulator.dart b/bin/simulator.dart index 91b1999..933a185 100644 --- a/bin/simulator.dart +++ b/bin/simulator.dart @@ -1,5 +1,6 @@ import "package:autonomy/rover.dart"; import "package:autonomy/simulator.dart"; +import "package:autonomy/src/drive/drive_config.dart"; import "package:burt_network/burt_network.dart"; void main() async { @@ -8,7 +9,15 @@ void main() async { simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.orchestrator = RoverOrchestrator(collection: simulator); - simulator.drive = DriveSimulator(collection: simulator, shouldDelay: true); + simulator.drive = DriveSimulator( + collection: simulator, + method: SimulationMethod.intermediate, + config: roverConfig.copyWith( + oneMeterDelay: const Duration(milliseconds: 500), + turnDelay: const Duration(milliseconds: 750), + subsystemsAddress: "127.0.0.1", + ), + ); simulator.detector = NetworkDetector(collection: simulator); await simulator.init(); diff --git a/bin/task.dart b/bin/task.dart index 621110f..7d1c77a 100644 --- a/bin/task.dart +++ b/bin/task.dart @@ -15,8 +15,11 @@ void main() async { simulator.detector = NetworkDetector(collection: simulator); simulator.pathfinder = RoverPathfinder(collection: simulator); simulator.orchestrator = RoverOrchestrator(collection: simulator); - // simulator.drive = RoverDrive(collection: simulator, useGps: false, useImu: false); - simulator.drive = DriveSimulator(collection: simulator, shouldDelay: true); + simulator.drive = RoverDrive( + collection: simulator, + useGps: false, + useImu: false, + ); simulator.gps = GpsSimulator(collection: simulator); simulator.imu = ImuSimulator(collection: simulator); simulator.video = VideoSimulator(collection: simulator); diff --git a/lib/src/drive/drive_config.dart b/lib/src/drive/drive_config.dart index 54068b9..2f37a72 100644 --- a/lib/src/drive/drive_config.dart +++ b/lib/src/drive/drive_config.dart @@ -35,6 +35,21 @@ class DriveConfig { required this.subsystemsAddress, }); + /// Creates a copy of this drive config with the provided fields replaced + DriveConfig copyWith({ + double? forwardThrottle, + double? turnThrottle, + Duration? oneMeterDelay, + Duration? turnDelay, + String? subsystemsAddress, + }) => DriveConfig( + forwardThrottle: forwardThrottle ?? this.forwardThrottle, + turnThrottle: turnThrottle ?? this.turnThrottle, + turnDelay: turnDelay ?? this.turnDelay, + oneMeterDelay: oneMeterDelay ?? this.oneMeterDelay, + subsystemsAddress: subsystemsAddress ?? this.subsystemsAddress, + ); + /// The [SocketInfo] for Subsystems, created using [subsystemsAddress] and port 8001 SocketInfo get subsystems => SocketInfo(address: InternetAddress(subsystemsAddress), port: 8001); diff --git a/lib/src/drive/drive_interface.dart b/lib/src/drive/drive_interface.dart index 4cb5b56..c87ca3b 100644 --- a/lib/src/drive/drive_interface.dart +++ b/lib/src/drive/drive_interface.dart @@ -58,16 +58,21 @@ abstract class DriveInterface extends Service { } } + /// State to turn the rover towards the nearest orientation StateInterface resolveOrientationState() => faceDirectionState(collection.imu.nearest); + /// State to drive the rover forward towards [coordinates] StateInterface driveForwardState(GpsCoordinates coordinates); + /// State to turn the rover to face towards [direction] StateInterface faceDirectionState(CardinalDirection direction) => faceOrientationState(direction.orientation); + /// State to face the rover towards [orientation] StateInterface faceOrientationState(Orientation orientation); + /// State to execute actions relating to the turning of an [AutonomyAStarState] StateInterface turnStateState(AutonomyAStarState state) => faceOrientationState(state.orientation.orientation); diff --git a/lib/src/drive/sim_drive.dart b/lib/src/drive/sim_drive.dart index 06dac18..3626b49 100644 --- a/lib/src/drive/sim_drive.dart +++ b/lib/src/drive/sim_drive.dart @@ -1,5 +1,18 @@ import "package:autonomy/interfaces.dart"; +/// Enum to represent the method to use for drive simulation +enum SimulationMethod { + /// Will instantly simulate moving to a location with no delay + instant, + + /// Will wait a period of time before updating + constantDelay, + + /// Will continuously update with intermediate states during its + /// delay, as if the rover was actually moving + intermediate, +} + /// An implementation of [DriveInterface] that will not move the rover, /// and only update its sensor readings based on the desired values /// @@ -9,52 +22,36 @@ class DriveSimulator extends DriveInterface { /// The amount of time to wait before updating the virtual sensor readings static const delay = Duration(milliseconds: 500); + /// The method to use for the drive simulation + final SimulationMethod method; + /// Whether or not to wait before updating virtual sensor readings, /// this can be useful when simulating the individual steps of a path - final bool shouldDelay; + bool get shouldDelay => method != SimulationMethod.instant; /// Constructor for DriveSimulator, initializing the default fields, and whether or not it should delay DriveSimulator({ required super.collection, - this.shouldDelay = false, + this.method = SimulationMethod.instant, super.config, }); - RoverState _delayAndExecuteState({ - required Duration delay, - required StateInterface child, - }) => SequenceState( - child.controller, - steps: [ - DelayedState(child.controller, delayTime: delay), - child, - ], - ); - @override StateInterface driveForwardState(GpsCoordinates coordinates) => - _delayAndExecuteState( - delay: shouldDelay ? delay : Duration.zero, - child: FunctionalState( - controller, - onUpdate: (controller) { - collection.gps.update(coordinates); - controller.popState(); - }, - ), + SimulationDriveForward( + controller, + collection: collection, + method: method, + destination: coordinates, ); @override StateInterface faceOrientationState(Orientation orientation) => - _delayAndExecuteState( - delay: shouldDelay ? delay : Duration.zero, - child: FunctionalState( - controller, - onUpdate: (controller) { - collection.imu.update(orientation); - controller.popState(); - }, - ), + SimulationDriveTurn( + controller, + collection: collection, + method: method, + goalOrientation: orientation, ); @override diff --git a/lib/src/state_machine/rover_state_machine.dart b/lib/src/state_machine/rover_state_machine.dart index 42997d0..801b0d5 100644 --- a/lib/src/state_machine/rover_state_machine.dart +++ b/lib/src/state_machine/rover_state_machine.dart @@ -9,6 +9,8 @@ export "rover_states/navigation.dart"; export "rover_states/pathing.dart"; export "rover_states/sensor_drive_forward.dart"; export "rover_states/sensor_drive_turn.dart"; +export "rover_states/simulation_drive_forward.dart"; +export "rover_states/simulation_drive_turn.dart"; /// Callback for a state method that takes in a controller typedef StateCallback = void Function(StateController controller); diff --git a/lib/src/state_machine/rover_states/simulation_drive_forward.dart b/lib/src/state_machine/rover_states/simulation_drive_forward.dart new file mode 100644 index 0000000..d8c760b --- /dev/null +++ b/lib/src/state_machine/rover_states/simulation_drive_forward.dart @@ -0,0 +1,58 @@ +import "package:autonomy/autonomy.dart"; +import "package:coordinate_converter/coordinate_converter.dart"; + +class SimulationDriveForward extends RoverState { + final SimulationMethod method; + final AutonomyInterface collection; + final GpsCoordinates destination; + + Duration get delay => collection.drive.config.oneMeterDelay; + + DateTime _startTime = DateTime(0); + GpsCoordinates _startCoordinates = GpsCoordinates(); + + SimulationDriveForward( + super.controller, { + required this.collection, + required this.method, + required this.destination, + }); + + @override + void enter() { + _startTime = DateTime.now(); + _startCoordinates = collection.gps.coordinates; + } + + @override + void update() { + if (method == SimulationMethod.instant) { + collection.gps.update(destination); + controller.popState(); + return; + } + + if (DateTime.now().difference(_startTime) >= delay) { + collection.gps.update(destination); + controller.popState(); + return; + } + + if (method == SimulationMethod.intermediate) { + final deltaCoordinates = destination.toUTM() - _startCoordinates.toUTM(); + final deltaTime = DateTime.now().difference(_startTime); + final timeFraction = + 1.0 * deltaTime.inMicroseconds / delay.inMicroseconds; + + final intermediateCoordinates = + _startCoordinates.toUTM() + + UTMCoordinates( + x: deltaCoordinates.x * timeFraction, + y: deltaCoordinates.y * timeFraction, + zoneNumber: 1, + ); + + collection.gps.update(intermediateCoordinates.toGps()); + } + } +} diff --git a/lib/src/state_machine/rover_states/simulation_drive_turn.dart b/lib/src/state_machine/rover_states/simulation_drive_turn.dart new file mode 100644 index 0000000..ebcdd24 --- /dev/null +++ b/lib/src/state_machine/rover_states/simulation_drive_turn.dart @@ -0,0 +1,53 @@ +import "package:autonomy/autonomy.dart"; + +class SimulationDriveTurn extends RoverState { + final SimulationMethod method; + final AutonomyInterface collection; + final Orientation goalOrientation; + + Duration get delay => collection.drive.config.turnDelay; + + DateTime _startTime = DateTime(0); + Orientation _startOrientation = Orientation(); + + SimulationDriveTurn( + super.controller, { + required this.collection, + required this.method, + required this.goalOrientation, + }); + + @override + void enter() { + _startTime = DateTime.now(); + _startOrientation = collection.imu.raw; + } + + @override + void update() { + if (method == SimulationMethod.instant) { + collection.imu.update(goalOrientation); + controller.popState(); + return; + } + + if (DateTime.now().difference(_startTime) >= delay) { + collection.imu.update(goalOrientation); + controller.popState(); + return; + } + + if (method == SimulationMethod.intermediate) { + final deltaOrientation = (goalOrientation.z - _startOrientation.z) + .clampHalfAngle(); + final deltaTime = DateTime.now().difference(_startTime); + final timeFraction = + 1.0 * deltaTime.inMicroseconds / delay.inMicroseconds; + + final intermediateRotation = + _startOrientation.z + deltaOrientation * timeFraction; + + collection.imu.update(Orientation(z: intermediateRotation)); + } + } +} diff --git a/lib/src/utils/gps_utils.dart b/lib/src/utils/gps_utils.dart index d5dad94..8b53271 100644 --- a/lib/src/utils/gps_utils.dart +++ b/lib/src/utils/gps_utils.dart @@ -103,16 +103,14 @@ extension GpsUtils on GpsCoordinates { bool isNear(GpsCoordinates other, [double? tolerance]) { tolerance ??= Constants.maxErrorMeters; - return distanceTo(other) < tolerance; + return distanceTo(other) <= tolerance; } GpsCoordinates operator +(GpsCoordinates other) => (toUTM() + other.toUTM()).toGps(); - GpsCoordinates operator -(GpsCoordinates other) => GpsCoordinates( - latitude: latitude - other.latitude, - longitude: longitude - other.longitude, - ); + GpsCoordinates operator -(GpsCoordinates other) => + (toUTM() - other.toUTM()).toGps(); String prettyPrint() => toProto3Json().toString(); From 934d0e812d79bf4939a0c6d3a4b1700b9687e29f Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Wed, 28 May 2025 10:56:34 -0400 Subject: [PATCH 103/110] Trigger replan on drive timeout --- lib/src/drive/sensor_drive.dart | 7 ++++++- lib/src/orchestrator/rover_orchestrator.dart | 5 +++++ lib/src/state_machine/rover_states/navigation.dart | 13 +++++++++++++ lib/src/state_machine/rover_states/pathing.dart | 1 + .../rover_states/sensor_drive_forward.dart | 8 ++++++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/src/drive/sensor_drive.dart b/lib/src/drive/sensor_drive.dart index 2f845f5..3b5302f 100644 --- a/lib/src/drive/sensor_drive.dart +++ b/lib/src/drive/sensor_drive.dart @@ -24,7 +24,12 @@ class SensorDrive extends DriveInterface with RoverDriveCommands { position: coordinates, drive: this, ), - onTimeout: (controller) => controller.popState(), + onTimeout: (controller) { + if (collection.orchestrator is RoverOrchestrator) { + (collection.orchestrator as RoverOrchestrator).triggerPathReplan(); + } + controller.popState(); + }, timeout: Constants.driveGPSTimeout, ); diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 5f986f7..94b3a6e 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -37,6 +37,7 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { void onCommandEnd() { super.onCommandEnd(); currentPath = null; + replanPath = false; } @override @@ -59,6 +60,10 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { @override Message getMessage() => statusMessage; + /// Triggers a path replan, calling this method will signal the + /// orchestrator to replan a new path while path following + void triggerPathReplan() => replanPath = true; + /// Finds new obstacles and locks them if any intersect with the current path /// /// If the implementation of the obstacle detector has detected any obstacles, diff --git a/lib/src/state_machine/rover_states/navigation.dart b/lib/src/state_machine/rover_states/navigation.dart index e3c8d5f..b12834e 100644 --- a/lib/src/state_machine/rover_states/navigation.dart +++ b/lib/src/state_machine/rover_states/navigation.dart @@ -151,6 +151,19 @@ class NavigationState extends RoverState { orchestrator.traversed.add(currentPathState!.position); + if (orchestrator.replanPath) { + collection.drive.stop(); + controller.transitionTo( + PathingState( + controller, + collection: collection, + orchestrator: orchestrator, + destination: destination, + ), + ); + return; + } + if (waypointIndex >= orchestrator.currentPath!.length - 1 || waypointIndex >= 5 || orchestrator.findAndLockObstacles()) { diff --git a/lib/src/state_machine/rover_states/pathing.dart b/lib/src/state_machine/rover_states/pathing.dart index d1f07a1..2446487 100644 --- a/lib/src/state_machine/rover_states/pathing.dart +++ b/lib/src/state_machine/rover_states/pathing.dart @@ -25,6 +25,7 @@ class PathingState extends RoverState { @override void enter() { + orchestrator.replanPath = false; orchestrator.currentState = AutonomyState.PATHING; orchestrator.findAndLockObstacles(); } diff --git a/lib/src/state_machine/rover_states/sensor_drive_forward.dart b/lib/src/state_machine/rover_states/sensor_drive_forward.dart index 890b166..c19e7b6 100644 --- a/lib/src/state_machine/rover_states/sensor_drive_forward.dart +++ b/lib/src/state_machine/rover_states/sensor_drive_forward.dart @@ -2,6 +2,7 @@ import "package:autonomy/constants.dart"; import "package:autonomy/interfaces.dart"; import "package:autonomy/src/drive/drive_commands.dart"; import "package:autonomy/src/drive/drive_config.dart"; +import "package:autonomy/src/orchestrator/rover_orchestrator.dart"; class SensorForwardState extends RoverState { final AutonomyInterface collection; @@ -23,6 +24,13 @@ class SensorForwardState extends RoverState { drive.setThrottle(config.forwardThrottle); drive.moveForward(); + if (collection.pathfinder.isObstacle(collection.gps.coordinates)) { + if (collection.orchestrator is RoverOrchestrator) { + (collection.orchestrator as RoverOrchestrator).triggerPathReplan(); + } + controller.popState(); + } + if (collection.gps.isNear(position, Constants.intermediateStepTolerance)) { controller.popState(); } From b885dbdd04b7d0f4567c03b349d964ff09528c43 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 29 May 2025 13:38:58 -0400 Subject: [PATCH 104/110] Send traversed coordinates before the current path --- bin/simulator.dart | 3 +-- lib/constants.dart | 2 +- lib/src/orchestrator/rover_orchestrator.dart | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/bin/simulator.dart b/bin/simulator.dart index 933a185..c0d6e12 100644 --- a/bin/simulator.dart +++ b/bin/simulator.dart @@ -1,5 +1,4 @@ -import "package:autonomy/rover.dart"; -import "package:autonomy/simulator.dart"; +import "package:autonomy/autonomy.dart"; import "package:autonomy/src/drive/drive_config.dart"; import "package:burt_network/burt_network.dart"; diff --git a/lib/constants.dart b/lib/constants.dart index 414320a..b6e121f 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -17,7 +17,7 @@ class Constants { /// The closest distance the pathfinding algorithm will allow /// the rover to go near an obstacle - static const double obstacleAvoidanceRadius = 0.5; + static const double obstacleAvoidanceRadius = 0.75; /// How close the rover should get to a drive coordinate before /// continuing with the path diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index 94b3a6e..d68c55a 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -49,9 +49,9 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { ...collection.pathfinder.lockedObstacles, ], path: { + ...traversed, for (final transition in currentPath ?? []) transition.position, - ...traversed, }, task: currentCommand?.task, crash: false, // TODO: Investigate if this is used and how to use it better From a00bebd8240ee1f197111d685d9288b21a60366d Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 29 May 2025 18:54:40 -0400 Subject: [PATCH 105/110] Make periodic execution async - Prevents long path generation from clogging up background tasks --- lib/src/orchestrator/rover_orchestrator.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/orchestrator/rover_orchestrator.dart b/lib/src/orchestrator/rover_orchestrator.dart index d68c55a..8e05835 100644 --- a/lib/src/orchestrator/rover_orchestrator.dart +++ b/lib/src/orchestrator/rover_orchestrator.dart @@ -225,7 +225,9 @@ class RoverOrchestrator extends OrchestratorInterface with ValueReporter { ], ), ); - executionTimer = PeriodicTimer(const Duration(milliseconds: 10), (timer) { + executionTimer = PeriodicTimer(const Duration(milliseconds: 10), ( + timer, + ) async { if (currentCommand == null) { collection.logger.warning( "Execution timer running while command is null", From 32f86c7c9f84aa1827934885ef8887d626b051e7 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 12 Oct 2025 13:44:43 -0400 Subject: [PATCH 106/110] Fixes from testing - Fix crash when locking obstacles - Fix intermediate stops when driving - Only stop when path could not be found --- lib/src/detector/rover_detector.dart | 2 +- lib/src/state_machine/rover_states/navigation.dart | 2 -- lib/src/state_machine/rover_states/pathing.dart | 1 + lib/src/state_machine/rover_states/sensor_drive_forward.dart | 5 ----- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/src/detector/rover_detector.dart b/lib/src/detector/rover_detector.dart index 307e141..48dedd4 100644 --- a/lib/src/detector/rover_detector.dart +++ b/lib/src/detector/rover_detector.dart @@ -98,7 +98,7 @@ class RoverDetector extends DetectorInterface { .clampHalfAngle(); return relativeAngle > -135 && relativeAngle < 135; - }); + }).toList(); collection.pathfinder.obstacles.removeAll(toRemove); temporaryObstacles.removeAll(toRemove); diff --git a/lib/src/state_machine/rover_states/navigation.dart b/lib/src/state_machine/rover_states/navigation.dart index b12834e..c20f81d 100644 --- a/lib/src/state_machine/rover_states/navigation.dart +++ b/lib/src/state_machine/rover_states/navigation.dart @@ -152,7 +152,6 @@ class NavigationState extends RoverState { orchestrator.traversed.add(currentPathState!.position); if (orchestrator.replanPath) { - collection.drive.stop(); controller.transitionTo( PathingState( controller, @@ -167,7 +166,6 @@ class NavigationState extends RoverState { if (waypointIndex >= orchestrator.currentPath!.length - 1 || waypointIndex >= 5 || orchestrator.findAndLockObstacles()) { - collection.drive.stop(); controller.transitionTo( PathingState( controller, diff --git a/lib/src/state_machine/rover_states/pathing.dart b/lib/src/state_machine/rover_states/pathing.dart index 2446487..8116603 100644 --- a/lib/src/state_machine/rover_states/pathing.dart +++ b/lib/src/state_machine/rover_states/pathing.dart @@ -42,6 +42,7 @@ class PathingState extends RoverState { body: "No path found from ${current.prettyPrint()} to ${destination.prettyPrint()}", ); + collection.drive.stop(); controller.popState(); } else { collection.logger.debug( diff --git a/lib/src/state_machine/rover_states/sensor_drive_forward.dart b/lib/src/state_machine/rover_states/sensor_drive_forward.dart index c19e7b6..e0c7782 100644 --- a/lib/src/state_machine/rover_states/sensor_drive_forward.dart +++ b/lib/src/state_machine/rover_states/sensor_drive_forward.dart @@ -35,9 +35,4 @@ class SensorForwardState extends RoverState { controller.popState(); } } - - @override - void exit() { - drive.stopMotors(); - } } From 174d96442ac80c63577fa4fca896a64af6c00b97 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 12 Oct 2025 13:44:56 -0400 Subject: [PATCH 107/110] Upgrade Dart to 3.9.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6d60ec4..0c65f9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.6.0 + sdk: ^3.9.0 # Try not to edit this section by hand. Use `dart pub add my_package` dependencies: From 10bb47fb354e1fa1c9dd69c61b71adf75351074b Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Sun, 12 Oct 2025 13:59:33 -0400 Subject: [PATCH 108/110] Fix unit tests - Also don't stop driving in timed drive --- .github/workflows/analyze.yml | 2 +- .github/workflows/documentation.yml | 2 +- lib/src/drive/timed_drive.dart | 1 - test/network_test.dart | 31 +++++++++++++++++++++++++---- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index b6fc1c5..6761abc 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -19,7 +19,7 @@ jobs: - name: Setup dart uses: dart-lang/setup-dart@v1 with: - sdk: 3.6.0 + sdk: 3.9.0 # This package is part of a Pub Workspace. However, CI still needs to # run on this repo by itself, so we want to override burt_network to use diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index b152de3..4d21499 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -24,7 +24,7 @@ jobs: - name: Install Dart uses: dart-lang/setup-dart@v1 with: - sdk: 3.6.0 + sdk: 3.9.0 - name: Install dependencies run: dart pub get diff --git a/lib/src/drive/timed_drive.dart b/lib/src/drive/timed_drive.dart index 629ea44..131ff8a 100644 --- a/lib/src/drive/timed_drive.dart +++ b/lib/src/drive/timed_drive.dart @@ -63,7 +63,6 @@ class TimedDrive extends DriveInterface with RoverDriveCommands { setThrottle(config.forwardThrottle); moveForward(); }, - onDone: stopMotors, ); @override diff --git a/test/network_test.dart b/test/network_test.dart index fd7588f..ab28e54 100644 --- a/test/network_test.dart +++ b/test/network_test.dart @@ -54,6 +54,18 @@ class MockSubsystems extends Service { } } +class StopState extends RoverState { + final AutonomyInterface collection; + + StopState(super.controller, {required this.collection}); + + @override + void update() { + collection.drive.stop(); + controller.popState(); + } +} + void main() => group("[Network]", tags: ["network"], () { var subsystems = MockSubsystems(); final rover = RoverAutonomy(); @@ -161,15 +173,26 @@ void main() => group("[Network]", tags: ["network"], () { await Future.delayed(Duration.zero); - expect(subsystems.throttleFlag, isFalse); - expect(subsystems.throttle, 0); - expect(subsystems.left, 0); - expect(subsystems.right, 0); + expect(subsystems.throttleFlag, isTrue); + expect(subsystems.throttle, isNot(0)); + expect(subsystems.left, isNot(0)); + expect(subsystems.right, isNot(0)); + + simulator.orchestrator.controller.pushState( + StopState(simulator.orchestrator.controller, collection: simulator), + ); simulator.orchestrator.controller.update(); simulator.orchestrator.controller.update(); simulator.orchestrator.controller.update(); + // Future events need to pump for the call to drive.stop() to execute + await Future.delayed(Duration.zero); + + expect(subsystems.throttle, 0); + expect(subsystems.left, 0); + expect(subsystems.right, 0); + expect(simulator.gps.isNear(origin, 0.5), isFalse); expect(simulator.gps.isNear(oneMeter), isTrue); From 2f4b579e0fcc8baa82a9e8770d36f16e8c97cecf Mon Sep 17 00:00:00 2001 From: logan Date: Tue, 28 Oct 2025 13:43:17 -0400 Subject: [PATCH 109/110] removed sensorless.dart and test.dart --- bin/sensorless.dart | 63 --------------------------------------------- bin/test.dart | 22 ---------------- 2 files changed, 85 deletions(-) delete mode 100644 bin/sensorless.dart delete mode 100644 bin/test.dart diff --git a/bin/sensorless.dart b/bin/sensorless.dart deleted file mode 100644 index 1605430..0000000 --- a/bin/sensorless.dart +++ /dev/null @@ -1,63 +0,0 @@ -// import "package:burt_network/logging.dart"; -// import "package:burt_network/protobuf.dart"; - -// import "package:autonomy/simulator.dart"; -// import "package:autonomy/rover.dart"; - -// void main() async { -// Logger.level = LogLevel.debug; -// final simulator = AutonomySimulator(); -// simulator.drive = SensorlessDrive(collection: simulator, useGps: false, useImu: false); -// await simulator.init(); -// await simulator.server.waitForConnection(); - -// final message = AutonomyData( -// destination: GpsCoordinates(latitude: 0, longitude: 0), -// state: AutonomyState.DRIVING, -// task: AutonomyTask.GPS_ONLY, -// obstacles: [], -// path: [ -// for (double x = 0; x < 3; x++) -// for (double y = 0; y < 3; y++) -// GpsCoordinates(latitude: y, longitude: x), -// ], -// ); -// simulator.server.sendMessage(message); - -// // "Snakes" around a 3x3 meter box. (0, 0), North -// await simulator.drive.goForward(); // (1, 0), North -// await simulator.drive.goForward(); // (2, 0), North -// await simulator.drive.turnRight(); // (2, 0), East -// await simulator.drive.goForward(); // (2, 1), East -// await simulator.drive.turnRight(); // (2, 1), South -// await simulator.drive.goForward(); // (1, 1), South -// await simulator.drive.goForward(); // (0, 1), South -// await simulator.drive.turnLeft(); // (0, 1), East -// await simulator.drive.goForward(); // (0, 2), East -// await simulator.drive.turnLeft(); // (0, 2), North -// await simulator.drive.goForward(); // (1, 2), North -// await simulator.drive.goForward(); // (2, 2), North -// await simulator.drive.turnLeft(); // (2, 2), West -// await simulator.drive.goForward(); // (2, 1), West -// await simulator.drive.goForward(); // (2, 0), West -// await simulator.drive.turnLeft(); // (2, 0), South -// await simulator.drive.goForward(); // (1, 0), South -// await simulator.drive.goForward(); // (0, 0), South -// await simulator.drive.turnLeft(); // (0, 0), East -// await simulator.drive.turnLeft(); // (0, 0), North - -// final message2 = AutonomyData( -// state: AutonomyState.AT_DESTINATION, -// task: AutonomyTask.AUTONOMY_TASK_UNDEFINED, -// obstacles: [], -// path: [ -// for (double x = 0; x < 3; x++) -// for (double y = 0; y < 3; y++) -// GpsCoordinates(latitude: y, longitude: x), -// ], -// ); -// simulator.server.sendMessage(message2); -// simulator.server.sendMessage(message2); - -// await simulator.dispose(); -// } diff --git a/bin/test.dart b/bin/test.dart deleted file mode 100644 index 63fa627..0000000 --- a/bin/test.dart +++ /dev/null @@ -1,22 +0,0 @@ -// import "package:autonomy/autonomy.dart"; -// import "package:burt_network/logging.dart"; - -// void main() async { -// Logger.level = LogLevel.all; -// final rover = RoverAutonomy(); -// rover.gps = RoverGps(collection: rover); -// rover.imu = RoverImu(collection: rover); -// rover.drive = RoverDrive(collection: rover, useGps: false); - -// await rover.init(); -// rover.logger.info("Waiting for readings"); -// // await rover.waitForReadings(); -// // await rover.waitForConnection(); - -// rover.logger.info("Starting"); -// await rover.drive.turnLeft(); -// await rover.drive.turnRight(); - -// rover.logger.info("Done"); -// await rover.dispose(); -// } From e3a837b4bb2b454e37fe509c4d81f67e814a7066 Mon Sep 17 00:00:00 2001 From: lregueiferos <124931111+lregueiferos@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:41:04 -0400 Subject: [PATCH 110/110] Increase retry count for Rover drive test --- test/network_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/network_test.dart b/test/network_test.dart index ab28e54..78912f2 100644 --- a/test/network_test.dart +++ b/test/network_test.dart @@ -121,7 +121,7 @@ void main() => group("[Network]", tags: ["network"], () { await Future.delayed(const Duration(seconds: 1)); }); - test("Rover can drive", retry: 5, () async { + test("Rover can drive", retry: 10, () async { subsystems.enabled = true; final simulator = AutonomySimulator(); simulator.gps = GpsSimulator(collection: simulator);