Skip to content

Commit

Permalink
Networking (#25)
Browse files Browse the repository at this point in the history
> * Created Electrical Message and Metrics
> * Changed Wrapped Message
> * Message Receiver, Message Sender edited
> * Moved Protobuf into metrics.dart
> * Updated analyzer test to use new protobuf files

> Added Protobuf files to `build.yaml` and `.gitignore`, so now you can run
> 
> ```
> dart run build_runner build
> ```
> 
> from `dashboard` instead of running
> 
> ```
> protoc --dart_out=./generated Protobuf/*.proto
> ```
> 
> from `dashboard/lib/src/data`.

Co-authored-by: Levi Lesches <[email protected]>
  • Loading branch information
AndyZ54 and Levi-Lesches authored Oct 25, 2022
1 parent dbee83a commit 9f77896
Show file tree
Hide file tree
Showing 20 changed files with 572 additions and 35 deletions.
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
7 changes: 5 additions & 2 deletions .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 @@ -42,7 +43,7 @@ jobs:
git commit -m "Removed documentation" -m "Will generate new docs"
- name: Add Flutter to path
run: echo "../flutter/bin" >> $GITHUB_PATH
run: echo "$GITHUB_WORKSPACE/flutter/bin" >> $GITHUB_PATH

- name: Flutter 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);
}
}
Loading

0 comments on commit 9f77896

Please sign in to comment.