Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/src/data/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ class NetworkSettings {
/// The address and port of the autonomy program.
final SocketInfo autonomySocket;

/// The address to use for time synchronization
final SocketInfo timesyncSocket;

/// The address of the tank. The port is ignored.
///
/// The Tank is a model rover that has all the same programs as the rover. This field does not
Expand All @@ -175,6 +178,7 @@ class NetworkSettings {
required this.subsystemsSocket,
required this.videoSocket,
required this.autonomySocket,
required this.timesyncSocket,
required this.tankSocket,
required this.baseSocket,
required this.connectionTimeout,
Expand All @@ -185,6 +189,7 @@ class NetworkSettings {
subsystemsSocket = json?.getSocket("subsystemsSocket") ?? SocketInfo.raw("192.168.1.20", 8001),
videoSocket = json?.getSocket("videoSocket") ?? SocketInfo.raw("192.168.1.30", 8002),
autonomySocket = json?.getSocket("autonomySocket") ?? SocketInfo.raw("192.168.1.30", 8003),
timesyncSocket = json?.getSocket("timesyncSocket") ?? SocketInfo.raw("192.168.1.20", 8020),
tankSocket = json?.getSocket("tankSocket") ?? SocketInfo.raw("192.168.1.40", 8000),
baseSocket = json?.getSocket("baseSocket") ?? SocketInfo.raw("192.168.1.50", 8005),
connectionTimeout = json?["connectionTimeout"] ?? 5;
Expand All @@ -194,6 +199,7 @@ class NetworkSettings {
"subsystemsSocket": subsystemsSocket.toJson(),
"videoSocket": videoSocket.toJson(),
"autonomySocket": autonomySocket.toJson(),
"timesyncSocket": timesyncSocket.toJson(),
"tankSocket": tankSocket.toJson(),
"baseSocket": baseSocket.toJson(),
"connectionTimeout": connectionTimeout,
Expand Down
139 changes: 103 additions & 36 deletions lib/src/models/data/sockets.dart
Original file line number Diff line number Diff line change
@@ -1,27 +1,63 @@
import "dart:io";

import "package:burt_network/logging.dart";
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";

/// Coordinates all the sockets to point to the right [RoverType].
class Sockets extends Model {
/// A UDP socket for handling time synchronization with the rover
late final timesync = TimesyncSocket(
timesyncAddress: models.settings.network.timesyncSocket,
);

DashboardSocket _registerSocket({
required Device device,
required SocketInfo Function() info,
}) {
final socket = DashboardSocket(device: device);
_deviceSocketMap[device] = socket;
_deviceSocketInfoMap[device] = info;
return socket;
}

/// A UDP socket for sending and receiving Protobuf data.
late final data = DashboardSocket(device: Device.SUBSYSTEMS);
late final DashboardSocket data = _registerSocket(
device: Device.SUBSYSTEMS,
info: () => models.settings.network.subsystemsSocket,
);

/// A UDP socket for receiving video.
late final video = DashboardSocket(device: Device.VIDEO);
late final DashboardSocket video = _registerSocket(
device: Device.VIDEO,
info: () => models.settings.network.videoSocket,
);

/// A UDP socket for controlling autonomy.
late final autonomy = DashboardSocket(device: Device.AUTONOMY);
late final DashboardSocket autonomy = _registerSocket(
device: Device.AUTONOMY,
info: () => models.settings.network.autonomySocket,
);

/// A UDP socket for controlling the base station
late final baseStation = DashboardSocket(device: Device.BASE_STATION);
late final DashboardSocket baseStation = _registerSocket(
device: Device.BASE_STATION,
info: () => models.settings.network.baseSocket,
);

/// A list of all the sockets this model manages.
List<DashboardSocket> get sockets => [data, video, autonomy, baseStation];

/// Maps each device to its corresponding socket
final Map<Device, DashboardSocket> _deviceSocketMap = {};

/// Maps each device to its setting for its socket destination
final Map<Device, SocketInfo Function()> _deviceSocketInfoMap = {};

/// The timestamp to use for sending messages with all sockets
DateTime get timestamp => timesync.timestamp;

/// The rover-like system currently in use.
RoverType rover = RoverType.rover;

Expand All @@ -36,7 +72,9 @@ class Sockets extends Model {
String get connectionSummary {
final result = StringBuffer();
for (final socket in sockets) {
result.write("${socket.device.humanName}: ${(socket.connectionStrength.value * 100).toStringAsFixed(0)}%\n");
result.write(
"${socket.device.humanName}: ${(socket.connectionStrength.value * 100).toStringAsFixed(0)}%\n",
);
}
return result.toString().trim();
}
Expand All @@ -47,33 +85,45 @@ class Sockets extends Model {
/// 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,
Device.BASE_STATION => baseStation,
_ => null,
};
DashboardSocket? socketForDevice(Device device) => _deviceSocketMap[device];

@override
Future<void> init() async {
for (final socket in sockets) {
socket.connectionStatus.addListener(() => socket.connectionStatus.value
? onConnect(socket.device)
: onDisconnect(socket.device),
@override
Future<void> init() async {
// Hacky way to make sure all calls to [_registerSocket] are completed
// this is due to the way Dart does lazy initialization: https://dart.dev/null-safety/understanding-null-safety#lazy-initialization
sockets;

// Make sure that all sockets are properly created and mapped
for (final device in _deviceSocketMap.keys) {
assert(
sockets.contains(_deviceSocketMap[device]),
"Socket for device $device is added to List<DashboardSocket> get sockets",
);
assert(
_deviceSocketInfoMap.containsKey(device),
"Device $device has a corresponding destination function in _deviceSocketInfoMap",
);
}

await timesync.init();
for (final socket in sockets) {
socket.connectionStatus.addListener(
() => socket.connectionStatus.value
? onConnect(socket.device)
: onDisconnect(socket.device),
);
socket.messages.listen((message) {
if (!socket.isEnabled) return;
models.messages.addMessage(message);
});
await socket.init();
}
final level = Logger.level;
Logger.level = LogLevel.warning;
await updateSockets();
Logger.level = level;
}
final level = Logger.level;
Logger.level = LogLevel.warning;
await updateSockets();
Logger.level = level;
notifyListeners();
}
}

/// Enables all sockets without restarting them
void enable() {
Expand All @@ -85,9 +135,9 @@ class Sockets extends Model {

/// Disconnects from all sockets without restarting them
void disable() {
for (final socket in sockets) {
socket.disable();
}
for (final socket in sockets) {
socket.disable();
}
notifyListeners();
}

Expand All @@ -96,12 +146,18 @@ class Sockets extends Model {
for (final socket in sockets) {
await socket.dispose();
}
await timesync.dispose();
_deviceSocketMap.clear();
_deviceSocketInfoMap.clear();
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");
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();
Expand All @@ -113,19 +169,27 @@ class Sockets extends Model {

/// 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;
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);
baseStation.destination = settings.baseSocket.copyWith(address: addressOverride);
timesync.timesyncDestination = settings.timesyncSocket.copyWith(
address: addressOverride,
);
for (final device in _deviceSocketMap.keys) {
socketForDevice(device)!.destination = _deviceSocketInfoMap[device]!()
.copyWith(address: addressOverride);
}
}

/// Resets all the sockets.
Expand All @@ -145,7 +209,10 @@ class Sockets extends Model {
Future<void> setRover(RoverType? value) async {
if (value == null) return;
rover = value;
models.home.setMessage(severity: Severity.info, text: "Using: ${rover.name}");
models.home.setMessage(
severity: Severity.info,
text: "Using: ${rover.name}",
);
await reset();
notifyListeners();
}
Expand Down
16 changes: 14 additions & 2 deletions lib/src/models/view/builders/settings_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class NetworkSettingsBuilder extends ValueBuilder<NetworkSettings> {
/// The view model representing the [SocketInfo] for the autonomy program.
final SocketBuilder autonomySocket;

/// The view model representing the [SocketInfo] for the timesync server.
final SocketBuilder timesyncSocket;

/// The view model representing the [SocketInfo] for the tank.
///
/// Since the tank runs multiple programs, the port is discarded and only the address is used.
Expand All @@ -50,13 +53,20 @@ class NetworkSettingsBuilder extends ValueBuilder<NetworkSettings> {
final NumberBuilder<double> connectionTimeout;

@override
List<SocketBuilder> get otherBuilders => [dataSocket, videoSocket, autonomySocket, tankSocket];
List<SocketBuilder> get otherBuilders => [
dataSocket,
videoSocket,
autonomySocket,
timesyncSocket,
tankSocket,
];

/// Creates the view model based on the current [Settings].
NetworkSettingsBuilder(NetworkSettings initial) :
dataSocket = SocketBuilder(initial.subsystemsSocket),
videoSocket = SocketBuilder(initial.videoSocket),
autonomySocket = SocketBuilder(initial.autonomySocket),
timesyncSocket = SocketBuilder(initial.timesyncSocket),
tankSocket = SocketBuilder(initial.tankSocket),
baseSocket = SocketBuilder(initial.baseSocket),
connectionTimeout = NumberBuilder<double>(initial.connectionTimeout, min: 0);
Expand All @@ -66,13 +76,15 @@ class NetworkSettingsBuilder extends ValueBuilder<NetworkSettings> {
&& videoSocket.isValid
&& autonomySocket.isValid
&& tankSocket.isValid
&& baseSocket.isValid;
&& baseSocket.isValid
&& timesyncSocket.isValid;

@override
NetworkSettings get value => NetworkSettings(
subsystemsSocket: dataSocket.value,
videoSocket: videoSocket.value,
autonomySocket: autonomySocket.value,
timesyncSocket: timesyncSocket.value,
tankSocket: tankSocket.value,
baseSocket: baseSocket.value,
connectionTimeout: connectionTimeout.value,
Expand Down
1 change: 1 addition & 0 deletions lib/src/pages/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class SettingsPage extends ReactiveWidget<SettingsBuilder> {
SocketEditor(name: "Video socket", model: model.network.videoSocket),
SocketEditor(name: "Autonomy socket", model: model.network.autonomySocket),
SocketEditor(name: "Base station socket", model: model.network.baseSocket),
SocketEditor(name: "Timesync destination", model: model.network.timesyncSocket),
SocketEditor(name: "Tank IP address", model: model.network.tankSocket, editPort: false),
NumberEditor(name: "Heartbeats per second", model: model.network.connectionTimeout),
if (Platform.isWindows) ListTile(
Expand Down
17 changes: 16 additions & 1 deletion lib/src/services/socket.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,23 @@ class DashboardSocket extends BurtSocket {
/// Number of times to check heart beat per seconds based on `models.settings.network.connectionTimeout`.
double get frequency => models.settings.network.connectionTimeout;

/// The destination this socket is set to
SocketInfo? get destination =>
destinations.isNotEmpty ? destinations.first : null;

/// Sets the destination of this socket
set destination(SocketInfo? address) {
if (address == null) return;
destinations.clear();
destinations.add(address);
}

@override
DateTime get timestamp => models.sockets.timestamp;

/// Listens for incoming messages on a UDP socket and sends heartbeats to the [device].
DashboardSocket({required super.device}) : super(port: null, quiet: true, keepDestination: true);
DashboardSocket({required super.device})
: super(port: null, quiet: true, keepDestination: true, maxClients: 1);

@override
Duration get heartbeatInterval => Duration(milliseconds: 1000 ~/ frequency);
Expand Down
Loading