diff --git a/lib/src/data/metrics/position.dart b/lib/src/data/metrics/position.dart index 30d051420..d124a22fd 100644 --- a/lib/src/data/metrics/position.dart +++ b/lib/src/data/metrics/position.dart @@ -34,12 +34,21 @@ class PositionMetrics extends Metrics { } } + /// Gets the human friendly name of the RTK fix mode + String getRTKString(RTKMode mode) => switch(mode) { + RTKMode.RTK_FIXED => "Fixed", + RTKMode.RTK_FLOAT => "Float", + RTKMode.RTK_NONE => "None", + _ => "None", + }; + @override List get allMetrics => [ MetricLine("GPS: "), - MetricLine(" Latitude: ${data.gps.latitude.toStringAsFixed(6)}°",), - MetricLine(" Longitude: ${data.gps.longitude.toStringAsFixed(6)}°",), + MetricLine(" Latitude: ${data.gps.latitude.toStringAsFixed(10)}°",), + MetricLine(" Longitude: ${data.gps.longitude.toStringAsFixed(10)}°",), MetricLine(" Altitude: ${data.gps.altitude.toStringAsFixed(2)} m"), + MetricLine(" RTK Mode: ${getRTKString(data.gps.rtkMode)}"), MetricLine("Orientation:",), MetricLine(" X: ${data.orientation.x.toStringAsFixed(2)}°", severity: getRotationSeverity(data.orientation.x)), MetricLine(" Y: ${data.orientation.y.toStringAsFixed(2)}°", severity: getRotationSeverity(data.orientation.y)), diff --git a/lib/src/models/data/views.dart b/lib/src/models/data/views.dart index c2bd1733f..cbf4af68f 100644 --- a/lib/src/models/data/views.dart +++ b/lib/src/models/data/views.dart @@ -88,10 +88,13 @@ class ViewsModel extends Model with PresetsModel { // Wait for all views to reset so as not to cause overflow issues await nextFrame(); setNumViews(preset.views.length); + notifyListeners(); // Wait 3 frames for flutter_resizable container to load await nextFrame(); await nextFrame(); await nextFrame(); + await nextFrame(); + await nextFrame(); if (preset.horizontal1.isNotEmpty) horizontalController1.setRatios(preset.horizontal1); if (preset.horizontal2.isNotEmpty) horizontalController2.setRatios(preset.horizontal2); if (preset.horizontal3.isNotEmpty) horizontalController3.setRatios(preset.horizontal3); diff --git a/lib/src/models/rover/rover.dart b/lib/src/models/rover/rover.dart index b2e287419..6b134dd87 100644 --- a/lib/src/models/rover/rover.dart +++ b/lib/src/models/rover/rover.dart @@ -7,7 +7,7 @@ import "package:rover_dashboard/models.dart"; import "settings.dart"; /// The model to control the entire rover. -/// +/// /// Find more specific functionality in this class's fields. class Rover extends Model { /// Monitors metrics coming from the rover. @@ -41,13 +41,13 @@ class Rover extends Model { Iterable get controllers => [controller1, controller2, controller3]; /// Whether the rover is connected. - bool get isConnected => models.sockets.data.isConnected; + bool get isConnected => models.sockets.sockets.any((socket) => socket.isConnected); /// The current status of the rover. ValueNotifier status = ValueNotifier(RoverStatus.DISCONNECTED); @override - Future init() async { + Future init() async { setDefaultControls(); await metrics.init(); await controller1.init(); diff --git a/lib/src/models/view/bad_apple.dart b/lib/src/models/view/bad_apple.dart new file mode 100644 index 000000000..d39b3275b --- /dev/null +++ b/lib/src/models/view/bad_apple.dart @@ -0,0 +1,133 @@ +import "dart:async"; +import "dart:ui"; + +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:just_audio/just_audio.dart"; +import "package:rover_dashboard/data.dart"; +import "package:rover_dashboard/models.dart"; + +/// The Bad Apple Easter Egg. +/// +/// This Easter Egg renders the Bad Apple video in the map page by grabbing +/// each frame and assigning an obstacle to each black pixel. +mixin BadAppleViewModel on ChangeNotifier { + /// Whether the UI is currently playing Bad Apple + bool isPlayingBadApple = false; + + /// Which frame in the Bad Apple video we are up to right now + int badAppleFrame = 0; + + /// The zoom of the map before playing bad apple + late int _originalZoom = gridSize; + + /// The audio player for the Bad Apple music + final badAppleAudioPlayer = AudioPlayer(); + + /// How many frames in a second are being shown + static const badAppleFps = 1; + + /// The last frame of Bad Apple + static const badAppleLastFrame = 6570; + + /// A stopwatch to track the current time in the bad apple video + final Stopwatch _badAppleStopwatch = Stopwatch(); + + /// Cleans up any resources used by Bad Apple. + void disposeBadApple() => badAppleAudioPlayer.dispose(); + + /// The amount of blocks in the width and height of the grid. + /// + /// Keep this an odd number to keep the rover in the center. + int get gridSize; + + /// Sets the zoom of the map. + void zoom(int newSize); + + /// Sets the grid data of the map. + set data(AutonomyData value); + + /// Starts playing Bad Apple. + Future startBadApple() async { + isPlayingBadApple = true; + notifyListeners(); + _originalZoom = gridSize; + zoom(50); + badAppleFrame = 0; + Timer.run(() async { + await badAppleAudioPlayer.setAsset("assets/bad_apple2.mp3", preload: false); + badAppleAudioPlayer.play().ignore(); + _badAppleStopwatch.start(); + }); + + while (isPlayingBadApple) { + await Future.delayed(Duration.zero); + var sampleTime = _badAppleStopwatch.elapsed; + if (badAppleAudioPlayer.position != Duration.zero) { + sampleTime = badAppleAudioPlayer.position; + } + badAppleFrame = ((sampleTime.inMicroseconds.toDouble() / 1e6) * 30.0).round(); + if (badAppleFrame >= badAppleLastFrame) { + stopBadApple(); + break; + } + final obstacles = await _loadBadAppleFrame(badAppleFrame); + if (obstacles == null) { + continue; + } + if (!isPlayingBadApple) { + break; + } + data = AutonomyData(obstacles: obstacles); + notifyListeners(); + } + } + + Future?> _loadBadAppleFrame(int videoFrame) async { + // final filename = "assets/bad_apple/image_480.jpg"; + final filename = "assets/bad_apple/image_$videoFrame.jpg"; + final buffer = await rootBundle.loadBuffer(filename); + final codec = await instantiateImageCodecWithSize(buffer); + final frame = await codec.getNextFrame(); + final image = frame.image; + if (image.height != 50 || image.width != 50) { + models.home.setMessage(severity: Severity.error, text: "Wrong Bad Apple frame size"); + stopBadApple(); + return null; + } + final imageData = await image.toByteData(); + if (imageData == null) { + models.home.setMessage(severity: Severity.error, text: "Could not load Bad Apple frame"); + stopBadApple(); + return null; + } + var offset = 0; + final obstacles = []; + for (var row = 0; row < image.height; row++) { + for (var col = 0; col < image.width; col++) { + final pixel = imageData.getUint8(offset++); + imageData.getUint8(offset++); + imageData.getUint8(offset++); + imageData.getUint8(offset++); + final isBlack = pixel < 100; // dealing with lossy compression, not 255 and 0 + final coordinate = GpsCoordinates(latitude: (row - image.height).abs().toDouble(), longitude: (image.width - col - 1).abs().toDouble()); + if (isBlack) obstacles.add(coordinate); + } + } + if (!isPlayingBadApple) { + return null; + } + return obstacles; + } + + /// Stops playing Bad Apple and resets the UI. + void stopBadApple() { + isPlayingBadApple = false; + data = AutonomyData(); + badAppleAudioPlayer.stop(); + _badAppleStopwatch.stop(); + _badAppleStopwatch.reset(); + zoom(_originalZoom); + notifyListeners(); + } +} diff --git a/lib/src/models/view/builders/autonomy_command.dart b/lib/src/models/view/builders/autonomy_command.dart index 54e86bda6..79fc07ae9 100644 --- a/lib/src/models/view/builders/autonomy_command.dart +++ b/lib/src/models/view/builders/autonomy_command.dart @@ -58,7 +58,7 @@ class AutonomyCommandBuilder extends ValueBuilder { } /// Sends this command to the rover using [Sockets.autonomy]. - Future submit() async { + Future submit(AutonomyCommand value) async { _handshake = null; isLoading = true; notifyListeners(); diff --git a/lib/src/models/view/builders/gps.dart b/lib/src/models/view/builders/gps.dart index 165049a9d..a2d133e2d 100644 --- a/lib/src/models/view/builders/gps.dart +++ b/lib/src/models/view/builders/gps.dart @@ -57,8 +57,8 @@ class GpsBuilder extends ValueBuilder { GpsCoordinates get value => switch (type) { GpsType.decimal => GpsCoordinates(longitude: longDecimal.value, latitude: latDecimal.value), GpsType.degrees => GpsCoordinates( - longitude: longDegrees.value + (longMinutes.value / 60) + (longSeconds.value / 3600), - latitude: latDegrees.value + (latMinutes.value / 60) + (latSeconds.value / 3600), + longitude: longDegrees.value + (longMinutes.value / 60.0) + (longSeconds.value / 3600.0), + latitude: latDegrees.value + (latMinutes.value / 60.0) + (latSeconds.value / 3600.0), ), }; diff --git a/lib/src/models/view/map.dart b/lib/src/models/view/map.dart index f9344a963..d033c20c5 100644 --- a/lib/src/models/view/map.dart +++ b/lib/src/models/view/map.dart @@ -1,13 +1,13 @@ import "dart:async"; -import "dart:ui"; +import "package:burt_network/burt_network.dart"; import "package:flutter/material.dart"; -import "package:flutter/services.dart"; -import "package:just_audio/just_audio.dart"; import "package:rover_dashboard/data.dart"; import "package:rover_dashboard/models.dart"; import "package:rover_dashboard/services.dart"; +import "bad_apple.dart"; + /// Represents the state of a cell on the autonomy map. enum AutonomyCell { /// This is where the rover currently is. @@ -38,6 +38,22 @@ class GridOffset { const GridOffset(this.x, this.y); } +extension _GpsCoordinatesToBlock on GpsCoordinates { + GpsCoordinates get toGridBlock { + final (:lat, :long) = inMeters; + return GpsCoordinates( + latitude: (lat / models.settings.dashboard.mapBlockSize).roundToDouble(), + longitude: (long / models.settings.dashboard.mapBlockSize).roundToDouble(), + ); + } +} + +/// A record representing data necessary to display a cell in the map +typedef MapCellData = ({GpsCoordinates coordinates, AutonomyCell cellType}); + +/// A 2D array of [MapCellData] to represent a coordinate grid +typedef AutonomyGrid = List>; + /// A view model for the autonomy page to render a grid map. /// /// Shows a bird's-eye map of where the rover is, what's around it, where the goal is, and the path @@ -45,10 +61,8 @@ class GridOffset { /// The [grid] is a 2D map of width and height [gridSize] that keeps the [roverPosition] in the /// center (by keeping track of its [offset]) and filling the other cells with [AutonomyCell]s. /// -class AutonomyModel with ChangeNotifier { - /// The amount of blocks in the width and height of the grid. - /// - /// Keep this an odd number to keep the rover in the center. +class AutonomyModel with ChangeNotifier, BadAppleViewModel { + @override int gridSize = 11; /// The offset to add to all other coordinates, based on [roverPosition]. See [recenterRover]. @@ -62,7 +76,6 @@ class AutonomyModel with ChangeNotifier { /// Initializes the view model. Future init() async { recenterRover(); - await Future.delayed(const Duration(seconds: 1)); _subscription = models.messages.stream.onMessage( name: AutonomyData().messageName, constructor: AutonomyData.fromBuffer, @@ -80,26 +93,52 @@ class AutonomyModel with ChangeNotifier { _subscription?.cancel(); models.settings.removeListener(notifyListeners); models.rover.metrics.position.removeListener(recenterRover); - badAppleAudioPlayer.dispose(); + disposeBadApple(); super.dispose(); } /// An empty grid of size [gridSize]. - List> get empty => [ - for (int i = 0; i < gridSize; i++) [ - for (int j = 0; j < gridSize; j++) - (GpsCoordinates(), AutonomyCell.empty), - ], - ]; + AutonomyGrid get empty => [ + for (int latitude = 0; latitude < gridSize; latitude++) [ + for (int longitude = 0; longitude < gridSize; longitude++) ( + coordinates: ( + lat: (latitude.toDouble() - offset.y) * precisionMeters, + long: (longitude.toDouble() + offset.x) * precisionMeters + ).toGps(), + cellType: AutonomyCell.empty + ), + ], + ]; /// A list of markers manually placed by the user. Useful for the Extreme Retrieval Mission. List markers = []; + /// The view model to edit the coordinate of the marker. GpsBuilder markerBuilder = GpsBuilder(); /// The rover's current position. GpsCoordinates get roverPosition => models.rover.metrics.position.data.gps; + /// The precision of the grid + double get precisionMeters => models.settings.dashboard.mapBlockSize; + + /// The cell type of the rover that isn't [AutonomyCell.rover] + AutonomyCell get roverCellType { + final roverCoordinates = roverPosition.toGridBlock; + + if (data.hasDestination() && data.destination.toGridBlock == roverCoordinates) { + return AutonomyCell.destination; + } else if (data.obstacles.map((e) => e.toGridBlock).contains(roverCoordinates)) { + return AutonomyCell.obstacle; + } else if (markers.map((e) => e.toGridBlock).contains(roverCoordinates)) { + return AutonomyCell.marker; + } else if (data.path.map((e) => e.toGridBlock).contains(roverCoordinates)) { + return AutonomyCell.path; + } + + return AutonomyCell.empty; + } + /// The rover's heading double get roverHeading => models.rover.metrics.position.angle; @@ -107,17 +146,21 @@ class AutonomyModel with ChangeNotifier { AutonomyData data = AutonomyData(); /// The grid of size [gridSize] with the rover in the center, ready to draw on the UI. - List> get grid { + AutonomyGrid get grid { final result = empty; for (final obstacle in data.obstacles) { markCell(result, obstacle, AutonomyCell.obstacle); } if (isPlayingBadApple) return result; - for (final path in data.path) { - markCell(result, path, AutonomyCell.path); - } + for (final path in data.path) { + if (!data.obstacles.contains(path)) { + markCell(result, path, AutonomyCell.path); + } + } for (final marker in markers) { - markCell(result, marker, AutonomyCell.marker); + if (!data.obstacles.contains(marker)) { + markCell(result, marker, AutonomyCell.marker); + } } // Marks the rover and destination -- these should be last if (data.hasDestination()) markCell(result, data.destination, AutonomyCell.destination); @@ -128,20 +171,21 @@ class AutonomyModel with ChangeNotifier { /// Converts a decimal GPS coordinate to an index representing the block in the grid. int gpsToBlock(double value) => (value / models.settings.dashboard.mapBlockSize).round(); - /// Calculates a new position for [gps] based on [offset] and adds it to the [list]. + /// Calculates a new position for [gps] based on [offset] and adds it to the [grid]. /// /// This function filters out any coordinates that shouldn't be shown based on [gridSize]. - void markCell(List> list, GpsCoordinates gps, AutonomyCell value) { + void markCell(AutonomyGrid grid, GpsCoordinates gps, AutonomyCell value) { // Latitude is y-axis, longitude is x-axis // The rover will occupy the center of the grid, so // - rover.longitude => (gridSize - 1) / 2 // - rover.latitude => (gridSize - 1) / 2 // Then, everything else should be offset by that - final x = -1 * gpsToBlock(gps.longitude) + offset.x; - final y = gpsToBlock(gps.latitude) + offset.y; + final (:lat, :long) = gps.inMeters; + final x = (long / precisionMeters).round() - offset.x; + final y = (lat / precisionMeters).round() + offset.y; if (x < 0 || x >= gridSize) return; if (y < 0 || y >= gridSize) return; - list[y][x] = (gps, value); + grid[y][x] = (coordinates: gps, cellType: value); } /// Determines the new [offset] based on the current [roverPosition]. @@ -156,15 +200,16 @@ class AutonomyModel with ChangeNotifier { /// so it remains `(-1, -1)` away from the rover's new position, yielding `(4, 4)`. void recenterRover() { // final position = isPlayingBadApple ? GpsCoordinates() : roverPosition; - final position = isPlayingBadApple ? GpsCoordinates(latitude: (gridSize ~/ 2).toDouble(), longitude: (gridSize ~/ 2).toDouble()) : roverPosition; + final position = roverPosition; final midpoint = ((gridSize - 1) / 2).floor(); - final offsetX = midpoint - -1 * gpsToBlock(position.longitude); - final offsetY = midpoint - gpsToBlock(position.latitude); + final (:lat, :long) = position.inMeters; + final offsetX = -midpoint + (long / precisionMeters).round(); + final offsetY = midpoint - (lat / precisionMeters).round(); offset = GridOffset(offsetX, offsetY); notifyListeners(); } - /// Zooms in or out by modifying [gridSize]. + @override void zoom(int newSize) { gridSize = newSize; recenterRover(); @@ -172,27 +217,29 @@ class AutonomyModel with ChangeNotifier { /// A handler to call when new data arrives. Updates [data] and the UI. void onNewData(AutonomyData value) { - data = value; + if (!isPlayingBadApple) { + data = value; + } services.files.logData(value); notifyListeners(); } - /// Places the marker in [markerBuilder]. - void placeMarker() { - markers.add(markerBuilder.value); - markerBuilder.clear(); + /// Places the marker at [coordinates]. + void placeMarker(GpsCoordinates coordinates) { + markers.add(coordinates.deepCopy()); notifyListeners(); } /// Places a marker at the rover's current position. void placeMarkerOnRover() { - markers.add(roverPosition); - notifyListeners(); + if (!markers.any((e) => e.toGridBlock == roverPosition.toGridBlock)) { + placeMarker(roverPosition); + } } - /// Removes a marker in [gps] - void updateMarker(GpsCoordinates gps) { - if(markers.remove(gps)){ + /// Removes a marker from [gps] + void removeMarker(GpsCoordinates gps) { + if (markers.remove(gps)) { notifyListeners(); } else { models.home.setMessage(severity: Severity.info, text: "Marker not found"); @@ -206,79 +253,45 @@ class AutonomyModel with ChangeNotifier { notifyListeners(); } - // ==================== Bad Apple Easter Egg ==================== - // - // This Easter Egg renders the Bad Apple video in the map page by grabbing - // each frame and assigning an obstacle to each black pixel. - - /// Whether the UI is currently playing Bad Apple - bool isPlayingBadApple = false; - /// A timer to update the map for every frame of Bad Apple - Timer? badAppleTimer; - /// Which frame in the Bad Apple video we are up to right now - int badAppleFrame = 0; - /// The audio player for the Bad Apple music - final badAppleAudioPlayer = AudioPlayer(); - /// How many frames in a second are being shown - static const badAppleFps = 1; - /// The last frame of Bad Apple - static const badAppleLastFrame = 6570; - - /// Starts playing Bad Apple. - Future startBadApple() async { - isPlayingBadApple = true; - notifyListeners(); - zoom(50); - badAppleFrame = 0; - badAppleTimer = Timer.periodic(const Duration(milliseconds: 1000 ~/ 30), _loadBadAppleFrame); - await badAppleAudioPlayer.setAsset("assets/bad_apple2.mp3"); - badAppleAudioPlayer.play().ignore(); - } + /// Builder for autonomy commands + final AutonomyCommandBuilder commandBuilder = AutonomyCommandBuilder(); - Future _loadBadAppleFrame(_) async { - // final filename = "assets/bad_apple/image_480.jpg"; - final filename = "assets/bad_apple/image_$badAppleFrame.jpg"; - final buffer = await rootBundle.loadBuffer(filename); - final codec = await instantiateImageCodecWithSize(buffer); - final frame = await codec.getNextFrame(); - final image = frame.image; - if (image.height != 50 || image.width != 50) { - models.home.setMessage(severity: Severity.error, text: "Wrong Bad Apple frame size"); - stopBadApple(); - return; - } - final imageData = await image.toByteData(); - if (imageData == null) { - models.home.setMessage(severity: Severity.error, text: "Could not load Bad Apple frame"); - stopBadApple(); - return; - } - var offset = 0; - final obstacles = []; - for (var row = 0; row < image.height; row++) { - for (var col = 0; col < image.width; col++) { - final pixel = [ - for (var channel = 0; channel < 4; channel++) - imageData.getUint8(offset++), - ]; - final isBlack = pixel.first < 100; // dealing with lossy compression, not 255 and 0 - final coordinate = GpsCoordinates(latitude: (row - image.height).abs().toDouble(), longitude: col.toDouble()); - if (isBlack) obstacles.add(coordinate); - } + /// Adds or removes a marker at the given location. + void toggleMarker(MapCellData cell) { + if (markers.contains(cell.coordinates)) { + removeMarker(cell.coordinates); + } else { + placeMarker(cell.coordinates); } - data = AutonomyData(obstacles: obstacles); - notifyListeners(); - badAppleFrame += badAppleFps; - if (badAppleFrame >= badAppleLastFrame) stopBadApple(); } - /// Stops playing Bad Apple and resets the UI. - void stopBadApple() { - isPlayingBadApple = false; - badAppleTimer?.cancel(); - data = AutonomyData(); - badAppleAudioPlayer.stop(); - zoom(11); - notifyListeners(); + /// Handles when a specific tile was dropped onto a grid cell. + /// + /// - If it's a destination tile, then the rover will go there + /// - If it's an obstacle tile, the rover will avoid it + /// - If it's a marker tile, draws or removes a Dashboard marker + void handleDrag(AutonomyCell data, MapCellData cell) { + switch (data) { + case AutonomyCell.destination: + if (models.rover.isConnected && RoverStatus.AUTONOMOUS != models.rover.status.value) { + models.home.setMessage(severity: Severity.error, text: "You must be in autonomy mode"); + return; + } + final command = AutonomyCommand( + task: AutonomyTask.GPS_ONLY, + destination: GpsCoordinates( + latitude: cell.coordinates.latitude, + longitude: cell.coordinates.longitude, + ), + ); + commandBuilder.submit(command); + case AutonomyCell.obstacle: + final obstacleData = AutonomyData(obstacles: [cell.coordinates]); + models.sockets.autonomy.sendMessage(obstacleData); + case AutonomyCell.marker: toggleMarker(cell); + case AutonomyCell.rover: break; + case AutonomyCell.path: break; + case AutonomyCell.empty: break; + } } } diff --git a/lib/src/pages/map.dart b/lib/src/pages/map.dart index da154ff91..b47b0bf9e 100644 --- a/lib/src/pages/map.dart +++ b/lib/src/pages/map.dart @@ -1,37 +1,54 @@ import "dart:math"; +import "package:burt_network/generated.dart"; import "package:flutter/material.dart"; import "package:rover_dashboard/models.dart"; import "package:rover_dashboard/widgets.dart"; +import "map/bad_apple.dart"; +import "map/header.dart"; +import "map/legend.dart"; + /// The UI for the autonomy subsystem. -/// +/// /// Displays a bird's-eye view of the rover and its path to the goal. class MapPage extends ReactiveWidget { - /// Gets the color for a given [AutonomyCell]. - Color? getColor(AutonomyCell cell) => switch(cell) { - AutonomyCell.destination => Colors.green, - AutonomyCell.obstacle => Colors.black, - AutonomyCell.path => Colors.blueGrey, - AutonomyCell.empty => Colors.white, - AutonomyCell.marker => Colors.red, - AutonomyCell.rover => Colors.transparent, - }; + /// Gets the color for a given [AutonomyCell]. + Color? getColor(AutonomyCell cell, AutonomyModel model) => switch (cell) { + AutonomyCell.destination => Colors.green, + AutonomyCell.obstacle => Colors.black, + AutonomyCell.path => Colors.blueGrey, + AutonomyCell.empty => Colors.white, + AutonomyCell.marker => Colors.red, + AutonomyCell.rover => getColor(model.roverCellType, model), + }; + + /// Places a marker at the given location and closes the dialog opened by [promptForMarker]. + void placeMarker(BuildContext context, AutonomyModel model, GpsCoordinates coordinates) { + model.placeMarker(model.markerBuilder.value); + model.markerBuilder.clear(); + Navigator.of(context).pop(); + } - /// Opens a dialog to prompt the user for GPS coordinates and places a marker there. - void placeMarker(BuildContext context, AutonomyModel model) => showDialog( - context: context, + /// Opens a dialog to prompt the user for GPS coordinates and calls [placeMarker]. + void promptForMarker(BuildContext context, AutonomyModel model) => showDialog( + context: context, builder: (_) => AlertDialog( title: const Text("Add a Marker"), content: Column( mainAxisSize: MainAxisSize.min, - children: [ GpsEditor(model.markerBuilder) ], + children: [GpsEditor(model.markerBuilder)], ), actions: [ - TextButton(child: const Text("Cancel"), onPressed: () => Navigator.of(context).pop()), + TextButton( + child: const Text("Cancel"), + onPressed: () => Navigator.of(context).pop(), + ), ElevatedButton( - onPressed: model.markerBuilder.isValid ? () { model.placeMarker(); Navigator.of(context).pop(); } : null, - child: const Text("Add"), + onPressed: model.markerBuilder.isValid + ? () => placeMarker(context, model, model.markerBuilder.value) + : null, + child: const Text("Add"), ), ], ), @@ -39,117 +56,152 @@ class MapPage extends ReactiveWidget { /// The index of this view. final int index; + /// A const constructor. const MapPage({required this.index}); @override AutonomyModel createModel() => AutonomyModel(); - @override - Widget build(BuildContext context, AutonomyModel model) => Stack(children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 48), - for (final row in model.grid.reversed) Expanded( - child: Row(children: [ - for (final cell in row) Expanded( - child: GestureDetector( - onTap: () => cell.$2 != AutonomyCell.marker ? () : model.updateMarker(cell.$1), - child: Container( - width: 24, - decoration: BoxDecoration(color: getColor(cell.$2), border: Border.all()), - child: cell.$2 != AutonomyCell.rover ? null : Container( - color: Colors.blue, - width: double.infinity, - height: double.infinity, - margin: const EdgeInsets.all(4), - child: Transform.rotate( - angle: -model.roverHeading * pi / 180, - child: const Icon(Icons.arrow_upward, size: 24), - ), + /// Creates a widget to display the cell data for the provided [cell] + Widget createCell(AutonomyModel model, MapCellData cell) => Expanded( + child: DragTarget( + onAcceptWithDetails: (details) => model.handleDrag(details.data, cell), + builder: (context, candidates, rejects) => GestureDetector( + onTap: () => model.toggleMarker(cell), + child: Container( + width: 24, + decoration: BoxDecoration( + color: getColor(cell.cellType, model), + border: Border.all(), + ), + child: cell.cellType != AutonomyCell.rover ? null : LayoutBuilder( + builder: (context, constraints) => Container( + color: Colors.blue, + width: double.infinity, + height: double.infinity, + margin: EdgeInsets.all(constraints.maxWidth / 15), + child: Transform.rotate( + angle: -model.roverHeading * pi / 180, + child: Icon( + Icons.arrow_upward, + size: constraints.maxWidth * 24 / 30, ), ), ), ), - ],), - ), - const SizedBox(height: 4), - if (!model.isPlayingBadApple) Row(children: [ // Legend - const SizedBox(width: 4), - Text("Legend:", style: context.textTheme.titleLarge), - const SizedBox(width: 8), - Container(width: 24, height: 24, color: Colors.blue), - const SizedBox(width: 4), - Text("Rover", style: context.textTheme.titleMedium), - const SizedBox(width: 24), - Container(width: 24, height: 24, color: Colors.green), - const SizedBox(width: 4), - Text("Destination", style: context.textTheme.titleMedium), - const SizedBox(width: 24), - Container(width: 24, height: 24, color: Colors.black), - const SizedBox(width: 4), - Text("Obstacle", style: context.textTheme.titleMedium), - const SizedBox(width: 24), - Container(width: 24, height: 24, color: Colors.blueGrey), - const SizedBox(width: 4), - Text("Path", style: context.textTheme.titleMedium), - const SizedBox(width: 24), - Container(width: 24, height: 24, color: Colors.red), - const SizedBox(width: 4), - Text("Marker", style: context.textTheme.titleMedium), - const Spacer(), - Text("Zoom: ", style: context.textTheme.titleLarge), - Expanded(flex: 2, child: Slider( - value: model.gridSize.toDouble(), - min: 1, - max: 41, - divisions: 20, - label: "${model.gridSize}x${model.gridSize}", - onChanged: (value) => model.zoom(value.toInt()), - ),), - ],), - if (!model.isPlayingBadApple) Row(children: [ // Controls - const SizedBox(width: 4), - Text("Place marker: ", style: context.textTheme.titleLarge), - const SizedBox(width: 8), - ElevatedButton.icon( - icon: const Icon(Icons.add), - label: const Text("Add Marker"), - onPressed: () => placeMarker(context, model), ), - const SizedBox(width: 8), - ElevatedButton.icon( - icon: const Icon(Icons.location_on), - label: const Text("Drop marker here"), - onPressed: model.placeMarkerOnRover, - ), - const SizedBox(width: 8), - ElevatedButton.icon(icon: const Icon(Icons.clear), label: const Text("Clear all"), onPressed: model.clearMarkers), - const Spacer(), - ],), + ), + ), + ); + + /// The controls for creating, placing, and removing markers + Widget markerControls(BuildContext context, AutonomyModel model) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Controls + Text("Place Marker: ", style: context.textTheme.titleLarge), + const SizedBox(height: 8), + ElevatedButton.icon( + icon: const Icon(Icons.add), + label: const Text("Add Marker"), + onPressed: () => promptForMarker(context, model), + ), const SizedBox(height: 8), - AutonomyCommandEditor(model), - const VerticalDivider(), - const SizedBox(height: 4), - ],), - Container( - color: context.colorScheme.surface, - height: 50, - child: Row(children: [ // The header at the top - const SizedBox(width: 8), - Text("Map", style: context.textTheme.headlineMedium), - if (models.settings.easterEggs.badApple) IconButton( - iconSize: 48, - icon: CircleAvatar( - backgroundImage: const AssetImage("assets/bad_apple_thumbnail.webp"), - child: model.isPlayingBadApple ? const Icon(Icons.block, color: Colors.red, size: 36) : null, + ElevatedButton.icon( + icon: const Icon(Icons.location_on), + label: const Text("Drop Marker Here"), + onPressed: model.placeMarkerOnRover, + ), + const SizedBox(height: 8), + ElevatedButton.icon( + icon: const Icon(Icons.clear), + label: const Text("Clear All"), + onPressed: model.clearMarkers, + ), + ], + ); + + @override + Widget build(BuildContext context, AutonomyModel model) => LayoutBuilder( + builder: (context, constraints) => Column( + children: [ + MapPageHeader(model: model, index: index), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (constraints.maxWidth > 700) ...[ + const SizedBox(width: 16), + const MapLegend(), + ], + const SizedBox(width: 16), + Flexible( + flex: 10, + child: AspectRatio( + aspectRatio: 1, + child: (!model.isPlayingBadApple) + ? Column( + children: [ + for (final row in model.grid.reversed) + Expanded( + child: Row( + children: [ + for (final cell in row) + createCell(model, cell), + ], + ), + ), + ], + ) + : CustomPaint( + painter: BadApplePainter( + frameNumber: model.badAppleFrame, + obstacles: model.data.obstacles, + ), + ), + ), + ), + const SizedBox(width: 16), + const Spacer(), + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 250, + ), + child: AbsorbPointer( + absorbing: model.isPlayingBadApple, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + markerControls(context, model), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Scale:", + style: context.textTheme.titleLarge, + ), + Slider( + value: model.gridSize.clamp(1, 41).toDouble(), + min: 1, + max: 41, + divisions: 20, + label: "${model.gridSize}x${model.gridSize}", + onChanged: (value) => model.zoom(value.toInt()), + ), + ], + ), + AutonomyCommandEditor(model.commandBuilder, model), + ], + ), + ), + ), + const SizedBox(width: 16), + ], ), - onPressed: model.isPlayingBadApple ? model.stopBadApple : model.startBadApple, ), - const Spacer(), - ViewsSelector(index: index), - ],), + ], ), - ],); + ); } diff --git a/lib/src/pages/map/bad_apple.dart b/lib/src/pages/map/bad_apple.dart new file mode 100644 index 000000000..5f0ff3394 --- /dev/null +++ b/lib/src/pages/map/bad_apple.dart @@ -0,0 +1,95 @@ + +import "package:flutter/material.dart"; + +import "package:rover_dashboard/data.dart"; + +/// A [CustomPainter] that can efficiently render [Bad Apple](https://www.youtube.com/watch?v=MVrNn5TuMkY). +/// +/// This class is not responsible for loading the data, only for the final render of the frames. +class BadApplePainter extends CustomPainter { + /// The current frame this painter is rendering. + final int frameNumber; + + /// The list of "obstacles" to render, in the shape of the current frame. + final List obstacles; + + /// Creates a painter that renders the given frame of Bad Apple. + BadApplePainter({required this.frameNumber, required this.obstacles}); + + @override + void paint(Canvas canvas, Size size) { + canvas.drawRect( + Rect.fromLTRB(0, 0, size.width, size.height), + Paint() + ..color = Colors.white + ..style = PaintingStyle.fill, + ); + drawGrid(canvas, size); + drawPixels(canvas, size); + } + + /// Draws an empty grid for this frame. + void drawGrid(Canvas canvas, Size size) { + final linePaint = Paint() + ..color = Colors.black + ..strokeWidth = 2 + ..strokeCap = StrokeCap.butt; + + final borderPaint = Paint() + ..color = Colors.black + ..strokeWidth = 1 + ..strokeCap = StrokeCap.square + ..style = PaintingStyle.stroke; + + // Columns + for (var i = 1; i <= 49; i++) { + canvas.drawLine( + Offset(i * size.width / 50, 0), + Offset(i * size.width / 50, size.height), + linePaint, + ); + } + + // Rows + for (var i = 1; i <= 49; i++) { + canvas.drawLine( + Offset(0, i * size.height / 50), + Offset(size.width, i * size.height / 50), + linePaint, + ); + } + + canvas.drawRect( + Rect.fromCenter( + center: Offset(size.width, size.height) / 2, + width: size.width - 1, + height: size.height - 1, + ), + borderPaint, + ); + } + + /// Draws pixels on the grid according to [obstacles]. + void drawPixels(Canvas canvas, Size size) { + final boxWidth = size.width / 50; + final boxHeight = size.height / 50; + + final paint = Paint() + ..color = Colors.black + ..style = PaintingStyle.fill; + + for (final coordinates in obstacles) { + final rect = Rect.fromLTWH( + size.width - (coordinates.longitude + 1) * boxHeight, + size.height - coordinates.latitude * boxWidth, + boxWidth, + boxHeight, + ); + canvas.drawRect(rect, paint); + } + } + + @override + bool shouldRepaint(BadApplePainter oldDelegate) => + frameNumber != oldDelegate.frameNumber; +} diff --git a/lib/src/pages/map/header.dart b/lib/src/pages/map/header.dart new file mode 100644 index 000000000..245ff91a4 --- /dev/null +++ b/lib/src/pages/map/header.dart @@ -0,0 +1,72 @@ + +import "package:flutter/material.dart"; +import "package:rover_dashboard/data.dart"; + +import "package:rover_dashboard/models.dart"; +import "package:rover_dashboard/widgets.dart"; + +/// The header to appear at the top of the map page, displays a button to play bad apple... +class MapPageHeader extends StatelessWidget { + /// The model to control the status of bad apple... + final AutonomyModel model; + + /// Index of the view the header is in + final int index; + + /// Const constructor for the map page header + const MapPageHeader({required this.index, required this.model, super.key}); + + /// The icon for the status of RTK + Widget rtkStateIcon(BuildContext context) { + final rtkMode = model.roverPosition.rtkMode; + + final icon = switch (rtkMode) { + RTKMode.RTK_FIXED => Icons.signal_wifi_4_bar, + RTKMode.RTK_FLOAT => Icons.network_wifi_2_bar, + _ => Icons.signal_wifi_off_outlined, + }; + + return Tooltip( + message: models.rover.metrics.position.getRTKString(rtkMode), + waitDuration: const Duration(milliseconds: 500), + child: Icon( + icon, + color: context.colorScheme.onSurface, + size: 28, + ), + ); + } + + @override + Widget build(BuildContext context) => Container( + color: context.colorScheme.surface, + height: 50, + child: Row( + children: [ + const SizedBox(width: 8), + Text("Map", style: context.textTheme.headlineMedium), + if (models.settings.easterEggs.badApple) IconButton( + iconSize: 48, + icon: CircleAvatar( + backgroundImage: const AssetImage("assets/bad_apple_thumbnail.webp"), + child: model.isPlayingBadApple + ? const Icon(Icons.block, color: Colors.red, size: 36) + : null, + ), + onPressed: model.isPlayingBadApple + ? model.stopBadApple + : model.startBadApple, + ), + const SizedBox(width: 5), + Row( + children: [ + const Text("RTK Status: "), + rtkStateIcon(context), + ], + ), + const Spacer(), + ViewsSelector(index: index), + ], + ), + ); +} diff --git a/lib/src/pages/map/legend.dart b/lib/src/pages/map/legend.dart new file mode 100644 index 000000000..4ae836694 --- /dev/null +++ b/lib/src/pages/map/legend.dart @@ -0,0 +1,83 @@ + +import "package:flutter/material.dart"; +import "package:rover_dashboard/models.dart"; +import "package:rover_dashboard/widgets.dart"; + +/// Displays the legend for what each cell color represents on the map +class MapLegend extends StatelessWidget { + /// Const constructor for the map legend + const MapLegend({super.key}); + + @override + Widget build(BuildContext context) => Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text("Legend:", style: context.textTheme.titleLarge), + Column( + children: [ + Container(width: 24, height: 24, color: Colors.blue), + const SizedBox(height: 2), + Text("Rover", style: context.textTheme.titleMedium), + ], + ), + Draggable( + data: AutonomyCell.destination, + dragAnchorStrategy: (draggable, context, position) => + const Offset(12, 12), + feedback: Container( + width: 24, + height: 24, + color: Colors.green.withOpacity(0.75), + ), + child: Column( + children: [ + Container(width: 24, height: 24, color: Colors.green), + const SizedBox(height: 2), + Text("Destination", style: context.textTheme.titleMedium), + ], + ), + ), + Draggable( + data: AutonomyCell.obstacle, + dragAnchorStrategy: (draggable, context, position) => + const Offset(12, 12), + feedback: Container( + width: 24, + height: 24, + color: Colors.black.withOpacity(0.75), + ), + child: Column( + children: [ + Container(width: 24, height: 24, color: Colors.black), + const SizedBox(height: 2), + Text("Obstacle", style: context.textTheme.titleMedium), + ], + ), + ), + Column( + children: [ + Container(width: 24, height: 24, color: Colors.blueGrey), + const SizedBox(height: 2), + Text("Path", style: context.textTheme.titleMedium), + ], + ), + Draggable( + data: AutonomyCell.marker, + dragAnchorStrategy: (draggable, context, position) => + const Offset(12, 12), + feedback: Container( + width: 24, + height: 24, + color: Colors.red.withOpacity(0.75), + ), + child: Column( + children: [ + Container(width: 24, height: 24, color: Colors.red), + const SizedBox(height: 2), + Text("Marker", style: context.textTheme.titleMedium), + ], + ), + ), + ], + ); +} diff --git a/lib/src/pages/settings.dart b/lib/src/pages/settings.dart index c770c7e65..6da5c7354 100644 --- a/lib/src/pages/settings.dart +++ b/lib/src/pages/settings.dart @@ -130,7 +130,7 @@ class SettingsPage extends ReactiveWidget { ), NumberEditor( name: "Block size", - subtitle: "The precision of the GPS grid", + subtitle: "The precision of the GPS grid in meters", model: model.dashboard.blockSize, ), SwitchListTile( diff --git a/lib/src/widgets/atomic/autonomy_command.dart b/lib/src/widgets/atomic/autonomy_command.dart index aabece948..d8441ee8a 100644 --- a/lib/src/widgets/atomic/autonomy_command.dart +++ b/lib/src/widgets/atomic/autonomy_command.dart @@ -5,14 +5,11 @@ import "package:rover_dashboard/models.dart"; import "package:rover_dashboard/widgets.dart"; /// A widget to edit an [AutonomyCommand]. -class AutonomyCommandEditor extends ReactiveWidget { +class AutonomyCommandEditor extends ReusableReactiveWidget { /// The autonomy view model. final AutonomyModel dataModel; /// A const constructor. - const AutonomyCommandEditor(this.dataModel); - - @override - AutonomyCommandBuilder createModel() => AutonomyCommandBuilder(); + const AutonomyCommandEditor(super.model, this.dataModel); /// Opens a dialog to prompt the user to create an [AutonomyCommand] and sends it to the rover. void createTask(BuildContext context, AutonomyCommandBuilder command) => showDialog( @@ -38,7 +35,7 @@ class AutonomyCommandEditor extends ReactiveWidget { actions: [ TextButton(child: const Text("Cancel"), onPressed: () => Navigator.of(context).pop()), ElevatedButton( - onPressed: command.isLoading ? null : () { command.submit(); Navigator.of(context).pop(); }, + onPressed: command.isLoading ? null : () { command.submit(command.value); Navigator.of(context).pop(); }, child: const Text("Submit"), ), ], @@ -46,33 +43,37 @@ class AutonomyCommandEditor extends ReactiveWidget { ); @override - Widget build(BuildContext context, AutonomyCommandBuilder model) => Row(mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(width: 4), - Text("Autonomy: ", style: context.textTheme.titleLarge), - const SizedBox(width: 8), - ElevatedButton.icon( - icon: const Icon(Icons.add), - label: const Text("New Task"), - onPressed: () { - if (RoverStatus.AUTONOMOUS == models.rover.status.value) { - createTask(context, model); - } else { - models.home.setMessage( - severity: Severity.error, - text: "You must be in autonomy mode to do that", - ); - } - }, - ), - const SizedBox(width: 8), - ElevatedButton( - style: const ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.red)), - onPressed: model.abort, - child: const Text("ABORT"), - ), - const Spacer(), - if (!dataModel.isPlayingBadApple) - Text("${dataModel.data.state.humanName}, ${dataModel.data.task.humanName}", style: context.textTheme.titleLarge), - const SizedBox(width: 8), - ],); + Widget build(BuildContext context, AutonomyCommandBuilder model) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Autonomy:", style: context.textTheme.titleLarge), + Text( + "${dataModel.data.state.humanName}, ${dataModel.data.task.humanName}", + style: context.textTheme.titleMedium, + ), + const SizedBox(height: 8), + ElevatedButton.icon( + icon: const Icon(Icons.add), + label: const Text("New Task"), + onPressed: () { + if (!models.rover.isConnected || RoverStatus.AUTONOMOUS == models.rover.status.value) { + createTask(context, model); + } else { + models.home.setMessage( + severity: Severity.error, + text: "You must be in autonomy mode to do that", + ); + } + }, + ), + const SizedBox(height: 8), + ElevatedButton( + style: const ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.red), + ), + onPressed: model.abort, + child: const Text("ABORT"), + ), + ], + ); } diff --git a/lib/src/widgets/navigation/views.dart b/lib/src/widgets/navigation/views.dart index 0d529d1dd..5fc757521 100644 --- a/lib/src/widgets/navigation/views.dart +++ b/lib/src/widgets/navigation/views.dart @@ -66,7 +66,7 @@ class ViewsWidget extends ReusableReactiveWidget { ], ), 2 => ResizableContainer( - key: const ValueKey(1), + key: ValueKey("1-${models.settings.dashboard.splitMode.name}"), direction: switch (models.settings.dashboard.splitMode) { SplitMode.horizontal => Axis.vertical, SplitMode.vertical => Axis.horizontal, diff --git a/pubspec.lock b/pubspec.lock index 6269064ca..ac885a6d1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -44,12 +44,10 @@ packages: burt_network: dependency: "direct main" description: - path: "." - ref: "2.1.0" - resolved-ref: "61c1952afe2210099ccde59220356db647dc79ae" - url: "https://github.com/BinghamtonRover/Networking.git" - source: git - version: "2.1.0" + path: "../SubsystemsPi/Networking" + relative: true + source: path + version: "2.3.1" characters: dependency: transitive description: @@ -86,10 +84,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" console: dependency: transitive description: @@ -186,12 +184,11 @@ packages: flutter_libserialport: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: "6d8b2807c132041da9fa06c34bce8a6f831ff1f5" - url: "https://github.com/snabble/flutter_libserialport.git" - source: git - version: "0.5.0" + name: flutter_libserialport + sha256: "20c320dcde8592a16f9badc0cacad61b1fb283dbec647b6ebfc1020f8274c67b" + url: "https://pub.dev" + source: hosted + version: "0.4.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -203,12 +200,11 @@ packages: flutter_resizable_container: dependency: "direct main" description: - path: "." - ref: set-children-public - resolved-ref: cf2f237653a48389a99081dbb5086c1acce03f7f - url: "https://github.com/Levi-Lesches/flutter_resizable_container" - source: git - version: "3.0.0" + name: flutter_resizable_container + sha256: ccca2c4da19acabd7456cc6ca6724ed2d1545d97b53b87fc67cfa1c3fa754acd + url: "https://pub.dev" + source: hosted + version: "3.0.3" flutter_sdl_gamepad: dependency: "direct main" description: @@ -279,10 +275,10 @@ packages: dependency: "direct main" description: name: just_audio - sha256: b41646a8241688f1d99c2e69c4da2bb26aa4b3a99795f6ff205c2a165e033fda + sha256: a49e7120b95600bd357f37a2bb04cd1e88252f7cdea8f3368803779b925b1049 url: "https://pub.dev" source: hosted - version: "0.9.41" + version: "0.9.42" just_audio_platform_interface: dependency: transitive description: @@ -299,22 +295,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.13" + just_audio_windows: + dependency: "direct main" + description: + name: just_audio_windows + sha256: b1ba5305d841c0e3883644e20fc11aaa23f28cfdd43ec20236d1e119a402ef29 + url: "https://pub.dev" + source: hosted + version: "0.2.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -519,7 +523,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -540,10 +544,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -556,10 +560,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -572,10 +576,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" typed_data: dependency: transitive description: @@ -684,10 +688,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 388fb3802..861a5cb33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: burt_network: git: url: https://github.com/BinghamtonRover/Networking.git - ref: 2.1.0 + ref: 2.2.0 file_picker: ^8.0.0+1 fl_chart: ^0.69.0 flutter_libserialport: ^0.4.0 @@ -22,9 +22,10 @@ dependencies: path_provider: ^2.0.14 protobuf: ^3.0.0 url_launcher: ^6.1.10 - just_audio: ^0.9.36 + just_audio: ^0.9.42 collection: ^1.18.0 flutter_sdl_gamepad: ^1.0.0 + just_audio_windows: ^0.2.2 dependency_overrides: flutter_libserialport: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index f15090eeb..59a4548e1 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterLibserialportPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterLibserialportPlugin")); + JustAudioWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("JustAudioWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 9cab37959..6c76dbda1 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_libserialport + just_audio_windows url_launcher_windows )