Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Networking #25

Merged
merged 21 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
9 changes: 8 additions & 1 deletion .github/workflows/analyze.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,17 @@ jobs:
- uses: actions/checkout@v2
with:
path: repo # keeps Flutter separate from repo
submodules: recursive

# Has to be after checkout since repo won't exist
- name: Add Flutter to path
run: echo "../flutter/bin" >> $GITHUB_PATH
run: echo "$GITHUB_WORKSPACE/flutter/bin" >> $GITHUB_PATH

- name: Generate Protobuf files
run: |
flutter clean
flutter pub get
dart run build_runner build

- name: Analyze
run: flutter analyze --dartdocs
5 changes: 4 additions & 1 deletion .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
with:
fetch-depth: 0
path: repo # keep Flutter separate
submodules: recursive

- name: Git Setup
run: |
Expand All @@ -52,7 +53,9 @@ jobs:
flutter --version

- name: Analyze code
run: flutter analyze --dartdocs
run: |
dart run build_runner build
flutter analyze --dartdocs

- name: Output error
if: failure()
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Protobuf code is generated here
lib/src/data/generated

# Miscellaneous
*.class
*.log
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "lib/src/data/Protobuf"]
path = lib/src/data/Protobuf
url = https://github.com/BinghamtonRover/Protobuf.git
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ A Dart library is technically just a file. To add more complexity, classes and f

## Compiling

Firstly, we're using [Protobuf](https://developers.google.com/protocol-buffers), which means we need to invoke the Protobuf compiler to generate Dart code before we can compile the dashboard. Thankfully, that's all handled by `package:build_runner`, the de facto code generator for Dart.
```
dart run build_runner build
```

To run a debug build, run the appropriate command for your computer:

```
Expand All @@ -46,7 +51,7 @@ flutter build linux
flutter build macos
```

Note that Flutter is not cross-platform, which means you can only output executables for the platform you compile on. The location of the executable differs by platform:
Note that Flutter's compiler is not cross-platform, which means you can only output executables for the platform you compile on. The location of the executable differs by platform:

- Windows: `build\windows\runner\Release`
- Linux: `build/linux/x64/release/bundle`
Expand Down
12 changes: 6 additions & 6 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
# check for errors, warnings, and lints. See the following for docs:
# https://dart.dev/guides/language/analysis-options
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
Expand All @@ -9,6 +10,10 @@
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

analyzer:
exclude:
- lib/src/data/generated/**.dart

linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
Expand Down Expand Up @@ -240,8 +245,3 @@ linter:
# Including a `key` parameter in widgets is good practice for public APIs.
# However, this project is not being imported by third parties.
use_key_in_widget_constructors: false



# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
19 changes: 19 additions & 0 deletions build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
targets:
$default:
sources:
- $package$
- lib/$lib$
- lib/src/data/Protobuf/**.proto
builders:
protoc_builder:
options:
# Directory which is treated as the root of all Protobuf files.
# (Default: "proto/")
root_dir: "lib/src/data/Protobuf/"
# Include paths given to the Protobuf compiler during compilation.
# (Default: ["proto/"])
proto_paths:
- "lib/src/data/Protobuf/"
# The root directory for generated Dart output files.
# (Default: "lib/src/proto")
out_dir: "lib/src/data/generated"
5 changes: 5 additions & 0 deletions lib/data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@
/// library should import any other library.
library data;

export "src/data/generated/drive_control.pb.dart";
export "src/data/generated/sensor_control.pb.dart";
export "src/data/generated/video_control.pb.dart";
export "src/data/generated/wrapper.pb.dart";
export "src/data/metrics.dart";
export "src/data/wrapped_message.dart";
2 changes: 2 additions & 0 deletions lib/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
///
/// This library may depend on the data and services library.
library models;

export "src/models/vitals.dart";
27 changes: 25 additions & 2 deletions lib/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,46 @@
/// not import any other library in this project, only 3rd party plugins.
library services;

import "src/services/message_receiver.dart";
import "src/services/message_sender.dart";
import "src/services/service.dart";

export "src/services/message_receiver.dart";
export "src/services/message_sender.dart";

/// A dependency injection service that manages the lifecycle of other services.
///
/// All services must only be used by accessing them from this class, and this class will take care
/// of calling lifecycle methods like [init] while handling possibly asynchrony.
///
/// When adding a new service, declare it as a field in this class **and** add it to the [_services]
/// list in the constructor. Otherwise, the service will fail to initialize and dispose properly.
///
/// To get an instance of this class, use [Services.instance].
class Services extends Service {
/// The singleton instance of this class.
///
/// This is the only instance of this class the app can guarantee is properly initialized.
static Services instance = Services._();

late final List<Service> _services;

/// This class has a private constructor since users should only use [Services.instance].
Services._();
Services._() { _services = [messageReceiver, messageSender]; }

/// A service that receives messages from the rover over the network.
final messageReceiver = MessageReceiver();

/// A service that sends messages to the rover over the network.
final messageSender = MessageSender();

@override
Future<void> init() async {
for (final service in _services) { await service.init(); }
}

@override
Future<void> init() async { }
Future<void> dispose() async {
for (final service in _services) { await service.dispose(); }
}
}
1 change: 1 addition & 0 deletions lib/src/data/Protobuf
Submodule Protobuf added at 6da4ac
12 changes: 6 additions & 6 deletions lib/src/data/metrics.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "package:protobuf/protobuf.dart";

import "package:rover_dashboard/data.dart";
/// A readout of metrics reported by one of the rover's subsystems.
///
/// To use this class, create a subclass that extends this class with [T] as the generated
Expand Down Expand Up @@ -29,7 +29,7 @@ abstract class Metrics<T extends GeneratedMessage> {
/// These metrics represent the vitals of the rover: basics like voltage, current, and temperature
/// of the various electrical components. These values aren't useful for the missions, but should
/// be monitored to catch problems before they cause damage to the rover.
class ElectricalMetrics extends Metrics {
class ElectricalMetrics extends Metrics<ElectricalData> {
/// A collection of metrics relevant for monitoring the rover's electrical status.
const ElectricalMetrics(super.data);

Expand All @@ -39,10 +39,10 @@ class ElectricalMetrics extends Metrics {
// TODO: implement this
@override
List<String> get allMetrics => [
// "Battery: ${data.batteryVoltage} V, ${data.batteryCurrent} A",
// "12V supply: ${data.v12SupplyVoltage} V, ${data.v12SupplyCurrent} A, ${data.v12SupplyTemperature} °F",
// "5V supply: ${data.v5SupplyVoltage} V, ${data.v5SupplyCurrent} A, ${data.v5SupplyTemperature} °F",
// "ODrives: ${data.odriveCurrent1} A, ${data.odriveCurrent2} A, ${data.odriveCurrent3} A",
"Battery: ${data.batteryVoltage} V, ${data.batteryCurrent} A",
"12V supply: ${data.v12SupplyVoltage} V, ${data.v12SupplyCurrent} A, ${data.v12SupplyTemperature} °F",
"5V supply: ${data.v5SupplyVoltage} V, ${data.v5SupplyCurrent} A, ${data.v5SupplyTemperature} °F",
"ODrives: ${data.odrive0Current} A, ${data.odrive1Current} A, ${data.odrive2Current} A",
];
}

Expand Down
16 changes: 16 additions & 0 deletions lib/src/data/wrapped_message.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import "package:protobuf/protobuf.dart" as proto;
import "package:rover_dashboard/data.dart";

/// A cleaner name for any message generated by Protobuf.
typedef Message = proto.GeneratedMessage;

/// A function that decodes a Protobuf messages serialized form.
///
/// The `.fromBuffer` constructor is a type of [MessageDecoder].
typedef MessageDecoder<T extends Message> = T Function(List<int> data);

/// Decodes a wrapped Protobuf message.
extension Unwrapper on WrappedMessage {
/// Decodes the wrapped message into a message of type [T].
T decode<T extends Message>(MessageDecoder<T> decoder) => decoder(data);
}
7 changes: 7 additions & 0 deletions lib/src/models/model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import "package:flutter/foundation.dart";

/// A data model that handles data from services.
abstract class Model with ChangeNotifier {
/// Initializes any data needed by this model.
Future<void> init();
}
19 changes: 19 additions & 0 deletions lib/src/models/vitals.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import "model.dart";

/// Handles incoming data about the vitals of the rover, like voltage, power and temperature.
class Vitals extends Model {
/// The temperature of the rover, in degrees F.
int temperature = 0;

@override
Future<void> init() async {
// Subscribe to ElectricalMessages using the MessageReceiver service.
// When a new message arrives, update the relevant fields and call [notifyListeners].
}

@override
Future<void> dispose() async {
// Cancel the subscription.
super.dispose();
}
}
84 changes: 84 additions & 0 deletions lib/src/services/message_receiver.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import "dart:async";
import "dart:io";

import "package:rover_dashboard/data.dart";
import "service.dart";

/// A callback to execute with raw Protobuf data.
typedef RawMessageHandler = void Function(List<int> data);

/// A callback to execute with a specific serialized Protobuf message.
// typedef MessageHandler<T extends Message> = void Function(T);
typedef Handler<T> = void Function(T);

/// The port to listen for messages on.
const port = 22201;

/// A function that handles incoming data.
extension on RawDatagramSocket {
StreamSubscription listenForData(Handler<List<int>> handler) => listen((event) {
final datagram = receive();
if (datagram == null) return;
handler(datagram.data);
});
}

/// A service that receives messages over a UDP connection.
///
/// To listen to certain messages, call [registerHandler] with the type of message you want
/// to receive, as well as a decoder and the handler callback itself.
class MessageReceiver extends Service {
/// Handlers for every possible type of Protobuf message in serialized form.
final Map<String, RawMessageHandler> _handlers = {};

/// The UDP socket to listen on.
///
/// Initialized in [init].
late final RawDatagramSocket _socket;

/// The subscription that listens to [_socket].
late final StreamSubscription _subscription;

@override
Future<void> init() async {
_socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port);
_subscription = _socket.listenForData(_listener);
}

@override
Future<void> dispose() async {
await _subscription.cancel();
_socket.close();
}

/// Runs every time data is received by the socket.
///
/// The datagram contains a [WrappedMessage]. These are Protobuf messages that wrap an
/// underlying message and record their name. We use the type of the underlying message
/// to get the appropriate handler from [_handlers] which decodes the message to the
/// correct type and processes it.
void _listener(List<int> data) {
final wrapped = WrappedMessage.fromBuffer(data);
final RawMessageHandler? rawHandler = _handlers[wrapped.name];
if (rawHandler == null) { /* Log in some meaningful way, through the UI */ }
else { rawHandler(wrapped.data); }
}

/// Adds a handler for a given type.
///
/// [decoder] is a function that decodes a byte buffer to a Protobuf message class. [handler]
/// then handles that message somehow.
void registerHandler<T extends Message>({
required String name,
required MessageDecoder<T> decoder,
required Handler<T> handler
}) {
if (T == Message) { // no T was actually passed, [Message] is the default
throw ArgumentError("No message type was passed");
} else if (_handlers.containsKey(name)) { // handler was already registered
throw ArgumentError("Message handler for type [$T] already registered");
} else {
_handlers[name] = (List<int> data) => handler(decoder(data));
}
}
}
41 changes: 41 additions & 0 deletions lib/src/services/message_sender.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import "dart:io";

import "package:rover_dashboard/data.dart";

import "service.dart";

/// A service to send [Message] objects to the rover.
class MessageSender extends Service {
/// The IP address of the rover.
static final address = InternetAddress("127.0.0.1");

/// The port on the rover to send to.
static const port = 8082;

/// The socket for the UDP connection.
late final RawDatagramSocket _socket;

@override
Future<void> init() async {
_socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port);
}

@override
Future<void> dispose() async {
_socket.close();
}

/// Resets the connection.
///
/// When in doubt, turn it off and back on again.
Future<void> reset() async {
await dispose();
await init();
}

/// Wraps the [message] in a [WrappedMessage] container and sends it to the rover.
Future<void> sendMessage(Message message) async {
final wrapper = WrappedMessage(name: message.info_.messageName, data: message.writeToBuffer());
_socket.send(wrapper.writeToBuffer(), address, port);
}
}
3 changes: 3 additions & 0 deletions lib/src/services/service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@
abstract class Service {
/// Initializes the service.
Future<void> init();

/// Cleans up any resources used by the service.
Future<void> dispose();
}
Loading