From c4ba0eed9f509f8b7a7f409a98da983b7065e545 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 26 Sep 2024 03:16:29 -0400 Subject: [PATCH] Upgraded network library and corresponding API (#165) Co-authored-by: Levi Lesches --- .github/workflows/publish.yml | 17 +-- lib/services.dart | 1 - lib/src/models/data/serial.dart | 34 ++--- lib/src/models/data/sockets.dart | 176 ++++++++++++------------- lib/src/models/view/footer.dart | 25 +++- lib/src/services/serial.dart | 106 --------------- lib/src/services/serial_errors.dart | 86 ------------ lib/src/services/socket.dart | 101 +++++++------- lib/src/widgets/navigation/footer.dart | 5 +- pubspec.lock | 118 ++++++++--------- pubspec.yaml | 4 +- 11 files changed, 238 insertions(+), 435 deletions(-) delete mode 100644 lib/src/services/serial.dart delete mode 100644 lib/src/services/serial_errors.dart diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c1672dd82c..05c47e0421 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,9 +1,9 @@ name: Publish Windows and Android apps # Builds a Windows App Installer (.msix) for the Dashboard -# +# # To use this action, add your Windows Certificate file, in base64 format, to a -# repository secret called WINDOWS_CERTIFICATE. This action then: +# repository secret called WINDOWS_CERTIFICATE. This action then: # - Installs Flutter and clones your repository # - Decodes your text certificate into a binary .pfx file # - Runs flutter pub run msix:create to build and sign your Flutter app @@ -16,7 +16,7 @@ on: jobs: build: runs-on: windows-latest - env: + env: windows_certificate: ${{ secrets.WINDOWS_CERTIFICATE }} steps: - name: Clone repository @@ -24,7 +24,7 @@ jobs: - name: Setup Java uses: actions/setup-java@v1 - with: + with: java-version: '12.x' - name: Load certificate @@ -34,7 +34,7 @@ jobs: - name: Install Flutter uses: subosito/flutter-action@v2 - with: + with: cache: true cache-key: "flutter-windows" # we don't need *the* most recent build @@ -45,12 +45,13 @@ jobs: dart analyze dart run msix:create - - name: Build APK - run: flutter build apk + # Temporarily removed because Android builds are broken + # - name: Build APK + # run: flutter build apk - name: Create Release uses: softprops/action-gh-release@v0.1.15 - with: + with: files: | build/windows/x64/runner/Release/Dashboard.msix build/app/outputs/apk/release/app-release.apk diff --git a/lib/services.dart b/lib/services.dart index 1bfd3b9a51..7c9fda0975 100644 --- a/lib/services.dart +++ b/lib/services.dart @@ -22,7 +22,6 @@ export "src/services/files.dart"; export "src/services/gamepad/gamepad.dart"; export "src/services/gamepad/service.dart"; export "src/services/gamepad/state.dart"; -export "src/services/serial.dart"; /// A dependency injection service that manages the lifecycle of other services. /// diff --git a/lib/src/models/data/serial.dart b/lib/src/models/data/serial.dart index 0f9659f173..2384091302 100644 --- a/lib/src/models/data/serial.dart +++ b/lib/src/models/data/serial.dart @@ -1,20 +1,21 @@ +import "package:burt_network/burt_network.dart"; + import "package:rover_dashboard/data.dart"; import "package:rover_dashboard/models.dart"; -import "package:rover_dashboard/services.dart"; /// A data model to manage all connected serial devices. -/// +/// /// Each connected device is represented by a [SerialDevice] object in the [devices] map. /// This model offers an API to connect, disconnect, and query devices using their port -/// names instead. -/// +/// names instead. +/// /// Send messages to the connected devices using the [sendMessage] method, and all messages /// received all ports are forwarded to [MessagesModel.onMessage]. class SerialModel extends Model { /// All the connected devices and their respective serial ports. - /// + /// /// Devices listed here are assumed to have successfully connected. - Map devices = {}; + Map devices = {}; /// Whether the given port is connected. bool isConnected(String port) => devices.containsKey(port); @@ -25,19 +26,18 @@ class SerialModel extends Model { Future init() async { } /// Connects to the given serial port and adds an entry to [devices]. - /// + /// /// If the connection or handshake fails, a message is logged to the home screen /// and the device is not added to [devices]. Future connect(String port) async { models.home.setMessage(severity: Severity.info, text: "Connecting to $port..."); - final device = SerialDevice(port: port, onMessage: models.messages.onMessage); - try { - await device.connect(); - } on SerialException catch(error) { - device.dispose(); - models.home.setMessage(severity: Severity.error, text: error.toString()); + final device = BurtFirmwareSerial(port: port, logger: BurtLogger()); + if (!await device.init()) { + await device.dispose(); + models.home.setMessage(severity: Severity.error, text: "Could not connect to $port"); return; } + device.messages.listen(models.messages.onMessage); models.home.setMessage(severity: Severity.info, text: "Connected to $port"); devices[port] = device; notifyListeners(); @@ -53,11 +53,13 @@ class SerialModel extends Model { notifyListeners(); } - /// Sends a message to all connected devices. - /// - /// [SerialDevice.sendMessage] ensures that only the correct messages get sent to each device. + /// Sends a message to all connected devices. + /// + /// This also ensures that only the correct messages get sent to each device. void sendMessage(Message message) { for (final device in devices.values) { + final thisDeviceAccepts = getCommandName(device.device); + if (message.messageName != thisDeviceAccepts) return; device.sendMessage(message); } } diff --git a/lib/src/models/data/sockets.dart b/lib/src/models/data/sockets.dart index d233467b0e..ae1f10ea0b 100644 --- a/lib/src/models/data/sockets.dart +++ b/lib/src/models/data/sockets.dart @@ -7,29 +7,14 @@ import "package:rover_dashboard/services.dart"; /// Coordinates all the sockets to point to the right [RoverType]. class Sockets extends Model { - /// A UDP socket for sending and receiving Protobuf data. - late final data = DashboardSocket( - device: Device.SUBSYSTEMS, - onConnect: onConnect, - onDisconnect: onDisconnect, - messageHandler: models.messages.onMessage, - ); - - /// A UDP socket for receiving video. - late final video = DashboardSocket( - device: Device.VIDEO, - onConnect: onConnect, - onDisconnect: onDisconnect, - messageHandler: models.messages.onMessage, - ); - - /// A UDP socket for controlling autonomy. - late final autonomy = DashboardSocket( - device: Device.AUTONOMY, - onConnect: onConnect, - onDisconnect: onDisconnect, - messageHandler: models.messages.onMessage, - ); + /// A UDP socket for sending and receiving Protobuf data. + late final data = DashboardSocket(device: Device.SUBSYSTEMS); + + /// A UDP socket for receiving video. + late final video = DashboardSocket(device: Device.VIDEO); + + /// A UDP socket for controlling autonomy. + late final autonomy = DashboardSocket(device: Device.AUTONOMY); /// A list of all the sockets this model manages. List get sockets => [data, video, autonomy]; @@ -39,96 +24,101 @@ class Sockets extends Model { /// The [InternetAddress] to use instead of the address on the rover. InternetAddress? get addressOverride => switch (rover) { - RoverType.rover => null, - RoverType.tank => models.settings.network.tankSocket.address, - RoverType.localhost => InternetAddress.loopbackIPv4, + RoverType.rover => null, + RoverType.tank => models.settings.network.tankSocket.address, + RoverType.localhost => InternetAddress.loopbackIPv4, }; /// A rundown of the connection strength of each device. String get connectionSummary { - final result = StringBuffer(); - for (final socket in sockets) { - result.write("${socket.device.humanName}: ${(socket.connectionStrength.value*100).toStringAsFixed(0)}%\n"); - } - return result.toString().trim(); + final result = StringBuffer(); + for (final socket in sockets) { + result.write("${socket.device.humanName}: ${(socket.connectionStrength.value * 100).toStringAsFixed(0)}%\n"); + } + return result.toString().trim(); } /// Returns the corresponding [DashboardSocket] for the [device] /// /// Returns null if no device is passed or there is no corresponding socket DashboardSocket? socketForDevice(Device device) => switch (device) { - Device.SUBSYSTEMS => data, - Device.VIDEO => video, - Device.AUTONOMY => autonomy, - _ => null, - }; - - @override - Future init() async { - for (final socket in sockets) { - await socket.init(); - } - final level = Logger.level; - Logger.level = LogLevel.warning; - await updateSockets(); - Logger.level = level; - } - - @override - Future dispose() async { - for (final socket in sockets) { - await socket.dispose(); - } - super.dispose(); - } - - /// Notifies the user when a new device has connected. - void onConnect(Device device) { - models.home.setMessage(severity: Severity.info, text: "The ${device.humanName} has connected"); - if (device == Device.SUBSYSTEMS) { + Device.SUBSYSTEMS => data, + Device.VIDEO => video, + Device.AUTONOMY => autonomy, + _ => null, + }; + + @override + Future init() async { + for (final socket in sockets) { + socket.connectionStatus.addListener(() => socket.connectionStatus.value + ? onConnect(socket.device) + : onDisconnect(socket.device), + ); + socket.messages.listen(models.messages.onMessage); + await socket.init(); + } + final level = Logger.level; + Logger.level = LogLevel.warning; + await updateSockets(); + Logger.level = level; + } + + @override + Future dispose() async { + for (final socket in sockets) { + await socket.dispose(); + } + super.dispose(); + } + + /// Notifies the user when a new device has connected. + void onConnect(Device device) { + models.home.setMessage(severity: Severity.info, text: "The ${device.humanName} has connected"); + if (device == Device.SUBSYSTEMS) { models.rover.status.value = models.rover.settings.status; models.rover.controller1.gamepad.pulse(); models.rover.controller2.gamepad.pulse(); models.rover.controller3.gamepad.pulse(); } notifyListeners(); - } + } - /// Notifies the user when a device has disconnected. - void onDisconnect(Device device) { - models.home.setMessage(severity: Severity.critical, text: "The ${device.humanName} has disconnected"); - if (device == Device.SUBSYSTEMS) models.rover.status.value = RoverStatus.DISCONNECTED; - if (device == Device.VIDEO) models.video.reset(); + /// Notifies the user when a device has disconnected. + void onDisconnect(Device device) { + models.home.setMessage(severity: Severity.critical, text: "The ${device.humanName} has disconnected"); + if (device == Device.SUBSYSTEMS) models.rover.status.value = RoverStatus.DISCONNECTED; + if (device == Device.VIDEO) models.video.reset(); notifyListeners(); - } - - /// Set the right IP addresses for the rover or tank. - Future updateSockets() async { - final settings = models.settings.network; - data.destination = settings.subsystemsSocket.copyWith(address: addressOverride); - video.destination = settings.videoSocket.copyWith(address: addressOverride); - autonomy.destination = settings.autonomySocket.copyWith(address: addressOverride); - } - - /// Resets all the sockets. - /// - /// When working with localhost, even UDP sockets can throw errors when the remote is unreachable. - /// Resetting the sockets will bypass these errors. - Future reset() async { - for (final socket in sockets) { - await socket.dispose(); - await socket.init(); - } + } + + /// Set the right IP addresses for the rover or tank. + Future updateSockets() async { + final settings = models.settings.network; + data.destination = settings.subsystemsSocket.copyWith(address: addressOverride); + video.destination = settings.videoSocket.copyWith(address: addressOverride); + autonomy.destination = settings.autonomySocket.copyWith(address: addressOverride); + } + + /// Resets all the sockets. + /// + /// When working with localhost, even UDP sockets can throw errors when the remote is unreachable. + /// Resetting the sockets will bypass these errors. + Future reset() async { + for (final socket in sockets) { + await socket.dispose(); + await socket.init(); + } // Sockets lose their destination when disposed, so we restore it. await updateSockets(); - } + } - /// Change which rover is being used. - Future setRover(RoverType? value) async { - if (value == null) return; - rover = value; - models.home.setMessage(severity: Severity.info, text: "Using: ${rover.name}"); + /// Change which rover is being used. + Future setRover(RoverType? value) async { + if (value == null) return; + rover = value; + models.home.setMessage(severity: Severity.info, text: "Using: ${rover.name}"); await reset(); - notifyListeners(); - } + notifyListeners(); + } } diff --git a/lib/src/models/view/footer.dart b/lib/src/models/view/footer.dart index 6aa055be4c..3a2df69bf0 100644 --- a/lib/src/models/view/footer.dart +++ b/lib/src/models/view/footer.dart @@ -4,13 +4,28 @@ import "package:rover_dashboard/models.dart"; /// A view model for the footer that updates when needed. class FooterViewModel with ChangeNotifier { + /// A list of other listenable models to subscribe to. + List get otherModels => [ + models.rover.metrics.drive, + models.rover.status, + models.sockets.data.connectionStrength, + models.sockets.video.connectionStrength, + models.sockets.autonomy.connectionStrength, + ]; + /// Listens to all the relevant data sources. FooterViewModel() { - models.rover.metrics.drive.addListener(notifyListeners); - models.rover.status.addListener(notifyListeners); - models.sockets.data.connectionStrength.addListener(notifyListeners); - models.sockets.video.connectionStrength.addListener(notifyListeners); - models.sockets.autonomy.connectionStrength.addListener(notifyListeners); + for (final model in otherModels) { + model.addListener(notifyListeners); + } + } + + @override + void dispose() { + for (final model in otherModels) { + model.removeListener(notifyListeners); + } + super.dispose(); } /// Access to the drive metrics. diff --git a/lib/src/services/serial.dart b/lib/src/services/serial.dart deleted file mode 100644 index 9d8631b86a..0000000000 --- a/lib/src/services/serial.dart +++ /dev/null @@ -1,106 +0,0 @@ -import "dart:typed_data"; - -import "package:flutter_libserialport/flutter_libserialport.dart"; -import "package:protobuf/protobuf.dart"; - -import "package:rover_dashboard/data.dart"; - -import "serial_errors.dart"; - -export "serial_errors.dart"; - -/// A service to connect to a single serial device. -/// -/// Create a new [SerialDevice] instance for every device you wish to connect to. You must call -/// [connect] before you can use the device, and you must call [dispose] when you are done. -/// -/// To send a message, use [sendMessage]. To handle messages, pass an [onMessage] callback. -class SerialDevice { - /// Sending this code resets the Teensy to its "unconnected" state. - static const resetCode = [0, 0, 0, 0]; - /// A list of all available ports to connect to. - static List get availablePorts => SerialPort.availablePorts; - - /// The port to connect to. - final String port; - /// A callback to run whenever a message is received by this device. - final WrappedMessageHandler onMessage; - /// Manages a connection to a Serial device. - SerialDevice({required this.port, required this.onMessage}); - - /// The device we're connected to. - /// - /// Initially, the device is just a generic "firmware", but after calling [Connect], the Teensy - /// will identify itself more specifically. - Device device = Device.FIRMWARE; - - /// Writes data to the serial port. - late final SerialPort _writer; - /// Reads data from the serial port. - late final SerialPortReader _reader; - - /// Opens the Serial port and identifies the device on the other end. - Future connect() async { - await Future(_setupConnection); - await _identifyDevice(); - _reader.stream.listen(_onData); - } - - /// Closes the connection and resets the device. - void dispose() { - _writer..close()..dispose(); - _reader.close(); - } - - /// Sends a message to the device, if the device accepts it. - /// - /// The firmware on the rover cannot handle [WrappedMessage]s and instead assume that all commands - /// they receive are the type they expect. This function checks [getCommandName] to ensure that - /// the [message] is of the correct type before sending it. - void sendMessage(Message message) { - final thisDeviceAccepts = getCommandName(device); - if (message.messageName != thisDeviceAccepts) return; - _sendRaw(message.writeToBuffer()); - } - - /// Sends raw data over the serial port. - void _sendRaw(List data) => _writer.write(Uint8List.fromList(data), timeout: 500); - - /// Sets up the connection and throws an error if the port fails to open. - void _setupConnection() { - _writer = SerialPort(port); - _reader = SerialPortReader(_writer); - final didOpen = _writer.openReadWrite(); - if (!didOpen) { - dispose(); - throw SerialCannotOpen(port); - } - _sendRaw(resetCode); - } - - /// Sends a handshake to the Teensy and decodes the response to identify the device. - Future _identifyDevice() async { - final handshake = Connect(sender: Device.DASHBOARD, receiver: Device.FIRMWARE); - _sendRaw(handshake.writeToBuffer()); - await Future.delayed(const Duration(milliseconds: 2000)); - final received = _writer.read(4); // nice way to read X bytes at a time - if (received.isEmpty) throw MalformedSerialPacket(packet: received); - try { - final message = Connect.fromBuffer(received); - final isValid = message.receiver == Device.DASHBOARD; - if (!isValid) { - dispose(); - throw SerialHandshakeFailed(); - } - device = message.sender; - } on InvalidProtocolBufferException { - dispose(); - throw MalformedSerialPacket(packet: received); - } - } - - /// Wraps the data in a [WrappedMessage] using [getDataName] as [WrappedMessage.name]. - void _onData(Uint8List data) => onMessage( - WrappedMessage(name: getDataName(device), data: data), - ); -} diff --git a/lib/src/services/serial_errors.dart b/lib/src/services/serial_errors.dart deleted file mode 100644 index 60eb3d3572..0000000000 --- a/lib/src/services/serial_errors.dart +++ /dev/null @@ -1,86 +0,0 @@ -import "dart:typed_data"; - -import "package:flutter_libserialport/flutter_libserialport.dart"; -import "package:rover_dashboard/errors.dart"; - -/// The base class for all exceptions relating to using Serial devices. -class SerialException extends DashboardException { - /// Provides a const constructor. - const SerialException(); -} - -/// Indicates that no devices are available to connect to. -class NoDeviceFound extends SerialException { - @override - String toString() => "No available serial device found."; -} - -/// Indicates that multiple devices are available to connect to. -class MultipleDevicesFound extends SerialException { - /// The list of available devices. - final List devices; - - /// Creates an error that contains the list of available devices. - const MultipleDevicesFound(this.devices); - - @override - String toString() => "Multiple serial devices were found: ${devices.join(', ')}"; -} - -/// Indicates that no device has been connected. -class DeviceNotConnected extends SerialException { - @override - String toString() => "No device was chosen. Please connect by calling Serial.connect() first."; -} - -/// Indicates that a data packet has come malformed. -/// -/// Similar to a [FormatException], but this usually indicates a hardware or -/// physical connection issue to the serial device. -class MalformedSerialPacket extends SerialException { - /// The malformed packet. - final Uint8List packet; - - /// Creates an error about a malformed packet. - const MalformedSerialPacket({required this.packet}); - - @override - String toString() => "Malformed serial packet: $packet."; -} - -/// Indicates that the Serial device did not reciprocate the handshake. -/// -/// In particular, the device sent back a "Connect" message, but the fields -/// weren't set properly. -class SerialHandshakeFailed extends SerialException { - @override - String toString() => "Connection handshake failed"; -} - -/// Indicates that the port could not be opened. -/// -/// This usually means another process has an open handle on the port. -class SerialCannotOpen extends SerialException { - /// The port that failed to open. - final String port; - - /// Creates an error about a port that won't open. - const SerialCannotOpen(this.port); - - @override - String toString() => "Could not open port $port"; -} - -/// Indicates that the device is unreachable. -/// -/// This is simply a [SerialPortError] wrapped up as a [SerialException]. -class SerialIOError extends SerialException { - /// The underlying IO error thrown by the `libserialport` library. - final SerialPortError error; - - /// Creates an [SerialException] to represent a [SerialPortError]. - const SerialIOError(this.error); - - @override - String toString() => error.toString(); -} diff --git a/lib/src/services/socket.dart b/lib/src/services/socket.dart index 9580ef2816..295949c33e 100644 --- a/lib/src/services/socket.dart +++ b/lib/src/services/socket.dart @@ -1,87 +1,74 @@ +import "dart:async"; + import "package:burt_network/burt_network.dart"; -import "package:flutter/foundation.dart"; // <-- Used for ValueNotifier +import "package:flutter/foundation.dart"; // <-- Used for ValueNotifier -import "package:rover_dashboard/data.dart"; import "package:rover_dashboard/models.dart"; -/// A service to send and receive Protobuf messages over a UDP socket, using [ProtoSocket]. +/// A service to send and receive Protobuf messages over a UDP socket, using [BurtSocket]. /// /// This class monitors its connection to the given [device] by sending heartbeats periodically and -/// logging the response (or lack thereof). To be notified of connection events, pass in -/// [onConnect] and [onDisconnect] callbacks. To be notified of incoming messages, pass in an -/// [onMessage] callback that accepts a [WrappedMessage]. +/// logging the response (or lack thereof). To be notified of connection events, add a listener to [connectionStatus]. +/// To be notified of incoming messages, listen to the [messages] stream that streams incoming [WrappedMessage]. /// /// To use this class: /// - Call [init] to open the socket. /// - Check [connectionStrength] or [isConnected] for the connection to the given [device]. /// - To send a message, call [sendMessage]. /// - Call [dispose] to close the socket. -class DashboardSocket extends BurtUdpProtocol { - /// A callback to run when the [device] has connected. - void Function(Device device) onConnect; - /// A callback to run when the [device] has disconnected. - void Function(Device device) onDisconnect; - - /// The handler to call when a [WrappedMessage] comes in. Used by [onMessage]. - final WrappedMessageHandler messageHandler; +class DashboardSocket extends BurtSocket { + /// Notifier for when the socket connects or disconnects + final ValueNotifier connectionStatus = ValueNotifier(false); /// Number of times to check heart beat per seconds based on [settings.network.connectionTimeout]. double get frequency => models.settings.network.connectionTimeout; - /// Listens for incoming messages on a UDP socket and sends heartbeats to the [device]. - DashboardSocket({ - required this.onConnect, - required this.onDisconnect, - required this.messageHandler, - required super.device, - }) : super( - port: null, - quiet: true, - ); + /// Listens for incoming messages on a UDP socket and sends heartbeats to the [device]. + DashboardSocket({required super.device}) : super(port: null, quiet: true); @override Duration get heartbeatInterval => Duration(milliseconds: 1000 ~/ frequency); - /// The connection strength, as a percentage to this [device]. - final connectionStrength = ValueNotifier(0); + /// The connection strength, as a percentage to this [device]. + final connectionStrength = ValueNotifier(0); - /// The number of heartbeats received since the last heartbeat was sent. - int _heartbeats = 0; + /// The number of heartbeats received since the last heartbeat was sent. + int _heartbeats = 0; - /// Whether [checkHeartbeats] is still running. - bool _isChecking = false; + /// Whether [checkHeartbeats] is still running. + bool _isChecking = false; - /// Whether this socket has a stable connection to the [device]. + /// Whether this socket has a stable connection to the [device]. @override - bool get isConnected => connectionStrength.value > 0; + bool get isConnected => connectionStrength.value > 0; - @override - void onMessage(WrappedMessage wrapper) => messageHandler(wrapper); + @override + void onHeartbeat(Connect heartbeat, SocketInfo source) => _heartbeats++; - @override - void onHeartbeat(Connect heartbeat, SocketInfo source) => _heartbeats++; + @override + Future onSettings(NetworkSettings settings) async {} - @override - Future checkHeartbeats() async { - if (_isChecking) return; - // 1. Clear state and send a heartbeat - _isChecking = true; - _heartbeats = 0; - final wasConnected = isConnected; - sendMessage(Connect(sender: Device.DASHBOARD, receiver: device)); - // 2. Wait a bit and count the number of responses - await Future.delayed(heartbeatWaitDelay); - if (_heartbeats > 0) { - connectionStrength.value += connectionIncrement * _heartbeats; - } else { - connectionStrength.value -= connectionIncrement; - } - // 3. Assess the current state - connectionStrength.value = connectionStrength.value.clamp(0, 1); - if (isConnected && !wasConnected) onConnect(device); - if (wasConnected && !isConnected) onDisconnect(device); - _isChecking = false; - } + @override + Future checkHeartbeats() async { + if (_isChecking) return; + // 1. Clear state and send a heartbeat + _isChecking = true; + _heartbeats = 0; + final wasConnected = isConnected; + sendMessage(Connect(sender: Device.DASHBOARD, receiver: device)); + // 2. Wait a bit and count the number of responses + await Future.delayed(heartbeatWaitDelay); + if (_heartbeats > 0) { + connectionStrength.value += connectionIncrement * _heartbeats; + } else { + connectionStrength.value -= connectionIncrement; + } + // 3. Assess the current state + connectionStrength.value = connectionStrength.value.clamp(0, 1); + if (isConnected && !wasConnected) connectionStatus.value = true; + if (wasConnected && !isConnected) connectionStatus.value = false; + _isChecking = false; + } /// How much each successful/missed handshake is worth, as a percent. double get connectionIncrement => 1 / frequency; diff --git a/lib/src/widgets/navigation/footer.dart b/lib/src/widgets/navigation/footer.dart index 6ea1abfd16..7148a58ace 100644 --- a/lib/src/widgets/navigation/footer.dart +++ b/lib/src/widgets/navigation/footer.dart @@ -1,9 +1,10 @@ import "package:flutter/material.dart"; +import "package:burt_network/serial.dart"; + import "package:rover_dashboard/data.dart"; import "package:rover_dashboard/models.dart"; import "package:rover_dashboard/pages.dart"; -import "package:rover_dashboard/services.dart"; import "package:rover_dashboard/widgets.dart"; /// The footer, responsible for showing vitals and logs. @@ -213,7 +214,7 @@ class SerialButton extends ReusableReactiveWidget { tooltip: "Select device", onSelected: model.toggle, itemBuilder: (_) => [ - for (final String port in SerialDevice.availablePorts) PopupMenuItem( + for (final String port in DelegateSerialPort.allPorts) PopupMenuItem( value: port, child: ListTile( title: Text(port), diff --git a/pubspec.lock b/pubspec.lock index 94ce076716..b977052d04 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: archive - sha256: "6bd38d335f0954f5fad9c79e614604fbf03a0e5b975923dd001b6ea965ef5b4b" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.6.1" args: dependency: transitive description: @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: audio_session - sha256: a49af9981eec5d7cd73b37bacb6ee73f8143a6a9f9bd5b6021e6c346b9b6cf4e + sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261" url: "https://pub.dev" source: hosted - version: "0.1.19" + version: "0.1.21" boolean_selector: dependency: transitive description: @@ -46,10 +46,10 @@ packages: description: path: "." ref: HEAD - resolved-ref: ae18e281f3b327a7d3fbfe0e64e6078189f6dddc + resolved-ref: ceeb9602a332d612bd9392d2b865403eea76b465 url: "https://github.com/BinghamtonRover/Networking.git" source: git - version: "1.1.0" + version: "2.0.0" characters: dependency: transitive description: @@ -102,18 +102,18 @@ packages: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.4+2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" dylib: dependency: transitive description: @@ -150,10 +150,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" + sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "8.1.2" fixnum: dependency: transitive description: @@ -195,10 +195,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.0.22" flutter_resizable_container: dependency: "direct main" description: @@ -229,10 +229,10 @@ packages: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_parser: dependency: transitive description: @@ -269,10 +269,10 @@ packages: dependency: "direct main" description: name: just_audio - sha256: "5abfab1d199e01ab5beffa61b3e782350df5dad036cb8c83b79fa45fc656614e" + sha256: d8e8aaf417d33e345299c17f6457f72bd4ba0c549dc34607abb5183a354edc4d url: "https://pub.dev" source: hosted - version: "0.9.38" + version: "0.9.40" just_audio_platform_interface: dependency: transitive description: @@ -285,10 +285,10 @@ packages: dependency: transitive description: name: just_audio_web - sha256: "0edb481ad4aa1ff38f8c40f1a3576013c3420bf6669b686fe661627d49bc606c" + sha256: b163878529d9b028c53a6972fcd58cae2405bcd11cbfcea620b6fb9f151429d6 url: "https://pub.dev" source: hosted - version: "0.4.11" + version: "0.4.12" leak_tracker: dependency: transitive description: @@ -325,10 +325,10 @@ packages: dependency: transitive description: name: logger - sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4 + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" matcher: dependency: transitive description: @@ -357,10 +357,10 @@ packages: dependency: "direct dev" description: name: msix - sha256: "519b183d15dc9f9c594f247e2d2339d855cf0eaacc30e19b128e14f3ecc62047" + sha256: c50d6bd1aafe0d071a3c1e5a5ccb056404502935cb0a549e3178c4aae16caf33 url: "https://pub.dev" source: hosted - version: "3.16.7" + version: "3.16.8" package_config: dependency: transitive description: @@ -373,18 +373,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.0.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" path: dependency: transitive description: @@ -397,18 +397,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.10" path_provider_foundation: dependency: transitive description: @@ -437,10 +437,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: @@ -453,10 +453,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -485,10 +485,10 @@ packages: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" sdl3: dependency: transitive description: @@ -586,42 +586,42 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.6" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.10" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: @@ -634,26 +634,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: transitive description: name: uuid - sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.0" vector_math: dependency: transitive description: @@ -682,18 +682,18 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" win32: dependency: transitive description: name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "5.5.4" xdg_directories: dependency: transitive description: @@ -720,4 +720,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.5.2 <4.0.0" - flutter: ">=3.19.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2aea0c3a6a..0662f8c529 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: rover_dashboard description: Graphical application for remotely operating the rover. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 2024.9.18+11 # Always increment the build number, never go down, even on new versions +version: 2024.9.26+15 # Always increment the build number, never go down, even on new versions environment: sdk: ^3.2.2 @@ -52,7 +52,7 @@ flutter_launcher_icons: # Builds a Windows .msix App Installer file for the Dashboard. # Command: dart run msix:create msix_config: - msix_version: 2024.9.18.11 + msix_version: 2024.9.26.15 display_name: Dashboard publisher_display_name: Binghamton University Rover Team identity_name: edu.binghamton.rover