Skip to content

Commit

Permalink
Upgraded network library and corresponding API (#165)
Browse files Browse the repository at this point in the history
Co-authored-by: Levi Lesches <[email protected]>
  • Loading branch information
Gold872 and Levi-Lesches authored Sep 26, 2024
1 parent ccdc445 commit c4ba0ee
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 435 deletions.
17 changes: 9 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,15 +16,15 @@ on:
jobs:
build:
runs-on: windows-latest
env:
env:
windows_certificate: ${{ secrets.WINDOWS_CERTIFICATE }}
steps:
- name: Clone repository
uses: actions/checkout@v3

- name: Setup Java
uses: actions/setup-java@v1
with:
with:
java-version: '12.x'

- name: Load certificate
Expand All @@ -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

Expand All @@ -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/[email protected]
with:
with:
files: |
build/windows/x64/runner/Release/Dashboard.msix
build/app/outputs/apk/release/app-release.apk
Expand Down
1 change: 0 additions & 1 deletion lib/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
34 changes: 18 additions & 16 deletions lib/src/models/data/serial.dart
Original file line number Diff line number Diff line change
@@ -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<String, SerialDevice> devices = {};
Map<String, BurtFirmwareSerial> devices = {};

/// Whether the given port is connected.
bool isConnected(String port) => devices.containsKey(port);
Expand All @@ -25,19 +26,18 @@ class SerialModel extends Model {
Future<void> 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<void> 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();
Expand All @@ -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);
}
}
Expand Down
176 changes: 83 additions & 93 deletions lib/src/models/data/sockets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<DashboardSocket> get sockets => [data, video, autonomy];
Expand All @@ -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<void> 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<void> 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<void> 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<void> 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<void> 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<void> reset() async {
for (final socket in sockets) {
await socket.dispose();
await socket.init();
}
}

/// Set the right IP addresses for the rover or tank.
Future<void> 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<void> 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<void> 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<void> setRover(RoverType? value) async {
if (value == null) return;
rover = value;
models.home.setMessage(severity: Severity.info, text: "Using: ${rover.name}");
await reset();
notifyListeners();
}
notifyListeners();
}
}
25 changes: 20 additions & 5 deletions lib/src/models/view/footer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Listenable> 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.
Expand Down
Loading

0 comments on commit c4ba0ee

Please sign in to comment.