Skip to content

Commit

Permalink
Add shared serial code to the library (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
Levi-Lesches authored Apr 17, 2024
1 parent 8499c7d commit cb34671
Show file tree
Hide file tree
Showing 27 changed files with 568 additions and 67 deletions.
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Levi has to review everything

* @Levi-Lesches
2 changes: 2 additions & 0 deletions .github/workflows/analyze.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
# You can specify other versions if desired, see documentation here:
# https://github.com/dart-lang/setup-dart/blob/main/README.md
- uses: dart-lang/setup-dart@v1
with:
sdk: 3.2.2

- name: Install dependencies
run: dart pub get
Expand Down
2 changes: 1 addition & 1 deletion Protobuf
Submodule Protobuf updated 1 files
+0 −9 drive.proto
2 changes: 1 addition & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ linter:
cascade_invocations: false # sometimes, multi-line code is more readable

# Temporarily disabled until we are ready to document
public_member_api_docs: false
# public_member_api_docs: false
6 changes: 4 additions & 2 deletions bin/logs.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import "package:burt_network/burt_network.dart";
import "package:burt_network/logging.dart";

// 1. Define a new socket on port 8001 that doesn't do anything
class LogsServer extends RoverServer {
Expand All @@ -9,7 +8,10 @@ class LogsServer extends RoverServer {
void onMessage(_) { }

@override
void restart() { }
Future<void> restart() async { }

@override
Future<void> onShutdown() async { }
}

// 2. Create that socket and make a logger that uses it.
Expand Down
1 change: 0 additions & 1 deletion example/server.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import "package:burt_network/burt_network.dart";
import "package:burt_network/logging.dart";

final logger = BurtLogger();

Expand Down
32 changes: 20 additions & 12 deletions lib/burt_network.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,26 @@
library;

// For doc comments:
import "src/burt_protocol.dart";
import "src/proto_socket.dart";
import "src/rover_heartbeats.dart";
import "src/rover_logger.dart";
import "src/rover_server.dart";
import "src/udp_socket.dart";
import "src/udp/burt_protocol.dart";
import "src/udp/proto_socket.dart";
import "src/udp/rover_heartbeats.dart";
import "src/udp/rover_logger.dart";
import "src/udp/rover_server.dart";
import "src/udp/udp_socket.dart";

export "src/proto_socket.dart";
export "src/rover_server.dart";
export "src/burt_protocol.dart";
export "src/rover_heartbeats.dart";
export "src/socket_info.dart";
export "src/udp_socket.dart";
export "src/udp/proto_socket.dart";
export "src/udp/rover_server.dart";
export "src/udp/burt_protocol.dart";
export "src/udp/rover_heartbeats.dart";
export "src/udp/socket_info.dart";
export "src/udp/udp_socket.dart";

export "src/serial/device.dart";
export "src/serial/firmware.dart";
export "src/serial/port_delegate.dart";
export "src/serial/port_interface.dart";

export "src/service.dart";

export "generated.dart";
export "logging.dart";
12 changes: 10 additions & 2 deletions lib/logging.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ library;

import "dart:io";
import "package:burt_network/burt_network.dart";
import "package:logger/logger.dart";

export "package:logger/logger.dart";

/// An alias for [Level].
Expand All @@ -19,7 +19,15 @@ class BurtLogger {

/// The device that's sending these logs.
final Device? device;
final RoverServer? socket;

/// The socket to send messages over.
///
/// If this is not null, logs of type [Level.info] or more severe will be queued up. Once this
/// socket connects, the messages will send to the connected device (ie, the Dashboard).
///
/// If the device is already connected, all messages are sent to it immediately.
RoverServer? socket;

/// Creates a logger capable of sending network messages over the given socket.
BurtLogger({this.socket}) : device = socket?.device;

Expand Down
38 changes: 0 additions & 38 deletions lib/src/rover_server.dart

This file was deleted.

95 changes: 95 additions & 0 deletions lib/src/serial/device.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import "dart:async";
import "dart:typed_data";

import "package:burt_network/burt_network.dart";

/// A wrapper around the `package:libserialport` library.
///
/// - Check [DelegateSerialPort.allPorts] for a list of all available ports.
/// - Call [init] to open the port
/// - Use [write] to write bytes to the port. Strings are not supported
/// - Listen to [stream] to get incoming data
/// - Call [dispose] to close the port
class SerialDevice extends Service {
/// The port to connect to.
final String portName;
/// How often to read from the port.
final Duration readInterval;
/// The underlying connection to the serial port.
final SerialPortInterface _port;
/// The logger to use
final BurtLogger logger;

/// A timer to periodically read from the port (see [readBytes]).
Timer? _timer;

/// The controller for [stream].
final _controller = StreamController<Uint8List>.broadcast();

/// Manages a connection to a serial device.
SerialDevice({
required this.portName,
required this.readInterval,
required this.logger,
}) : _port = SerialPortInterface.factory(portName);

/// Whether the port is open (ie, the device is connected).
bool get isOpen => _port.isOpen;

@override
Future<bool> init() async {
try {
return _port.init();
} catch (error) {
return false;
}
}

/// Starts listening to data sent over the serial port via [stream].
void startListening() => _timer = Timer.periodic(readInterval, _listenForBytes);

/// Stops listening to the serial port.
void stopListening() => _timer?.cancel();

/// Reads bytes from the port. If [count] is provided, only reads that number of bytes.
Uint8List readBytes([int? count]) {
try {
return _port.read(count ?? _port.bytesAvailable);
} catch (error) {
logger.error("Could not read from serial port $portName:\n $error");
return Uint8List(0);
}
}

/// Reads any data from the port and adds it to the [stream].
void _listenForBytes(_) {
try {
final bytes = readBytes();
if (bytes.isEmpty) return;
_controller.add(bytes);
} catch (error) {
logger.critical("Could not read $portName", body: error.toString());
dispose();
}
}

@override
Future<void> dispose() async {
_timer?.cancel();
await _port.dispose();
await _controller.close();
}

/// Writes data to the port.
void write(Uint8List data) {
if (!_port.isOpen) return;
try {
_port.write(data);
} catch (error) {
logger.warning("Could not write data to port $portName");
}
}

/// All incoming bytes coming from the port.
Stream<Uint8List> get stream => _controller.stream;
}
101 changes: 101 additions & 0 deletions lib/src/serial/firmware.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import "dart:typed_data";

import "package:protobuf/protobuf.dart";

import "package:burt_network/burt_network.dart";

/// Represents a firmware device connected over Serial.
///
/// This device starts with an unknown [device]. Calling [init] starts a handshake with the device
/// that identifies it. If the handshake fails, [isReady] will be false. Calling [dispose] will
/// reset the device and close the connection.
class BurtFirmwareSerial extends Service {
/// The interval to read serial data at.
static const readInterval = Duration(milliseconds: 100);
/// How long it should take for a firmware device to respond to a handshake.
static const handshakeDelay = Duration(milliseconds: 200);
/// The reset code to send to a firmware device.
static final resetCode = Uint8List.fromList([0, 0, 0, 0]);

/// The name of this device.
Device device = Device.FIRMWARE;

SerialDevice? _serial;

/// The port this device is attached to.
final String port;
/// The logger to use.
final BurtLogger logger;
/// Creates a firmware device at the given serial port.
BurtFirmwareSerial({required this.port, required this.logger});

/// The stream of incoming data.
Stream<Uint8List>? get stream => _serial?.stream;

/// Whether this device has passed the handshake.
bool get isReady => device != Device.FIRMWARE;

@override
Future<bool> init() async {
// Open the port
_serial = SerialDevice(portName: port, readInterval: readInterval, logger: logger);
if (!await _serial!.init()) {
logger.warning("Could not open firmware device on port $port");
return false;
}

// Execute the handshake
if (!_reset()) logger.warning("The Teensy on port $port failed to reset");
if (!await _sendHandshake()) {
logger.warning("Could not connect to Teensy", body: "Device on port $port failed the handshake");
return false;
}

logger.info("Connected to the ${device.name} Teensy on port $port");
_serial!.startListening();
return true;
}

/// Sends the handshake to the device and returns whether it was successful.
Future<bool> _sendHandshake() async {
logger.debug("Sending handshake to port $port...");
final handshake = Connect(sender: Device.SUBSYSTEMS, receiver: Device.FIRMWARE);
_serial!.write(handshake.writeToBuffer());
await Future<void>.delayed(handshakeDelay);
final response = _serial!.readBytes(4);
if (response.isEmpty) {
logger.trace("Device did not respond");
return false;
}
try {
final message = Connect.fromBuffer(response);
logger.trace("Device responded with: ${message.toProto3Json()}");
if (message.receiver != Device.SUBSYSTEMS) return false;
device = message.sender;
return true;
} on InvalidProtocolBufferException {
logger.trace("Device responded with malformed data: $response");
return false;
}
}

/// Sends the reset code and returns whether the device confirmed its reset.
bool _reset() {
_serial?.write(resetCode);
final response = _serial?.readBytes( 4);
if (response == null) return false;
if (response.length != 4 || response.any((x) => x != 1)) return false;
logger.info("The ${device.name} Teensy has been reset");
return true;
}

/// Sends bytes to the device via Serial.
void sendBytes(List<int> bytes) => _serial?.write(Uint8List.fromList(bytes));

/// Resets the device and closes the port.
@override
Future<void> dispose() async {
if (!_reset()) logger.warning("The $device device on port $port did not reset");
await _serial?.dispose();
}
}
45 changes: 45 additions & 0 deletions lib/src/serial/port_delegate.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import "dart:typed_data";

import "package:libserialport/libserialport.dart";

import "port_interface.dart";

/// A serial port implementation that delegates to [`package:libserialport`](https://pub.dev/packages/libserialport)
class DelegateSerialPort extends SerialPortInterface {
/// A list of all available ports on the device.
static List<String> allPorts = SerialPort.availablePorts;

SerialPort? _delegate;

/// Creates a serial port that delegates to the `libserialport` package.
DelegateSerialPort(super.portName);

@override
bool get isOpen => _delegate?.isOpen ?? false;

@override
Future<bool> init() async {
try {
_delegate = SerialPort(portName);
return _delegate!.openReadWrite();
} catch (error) {
return false;
}
}

@override
int get bytesAvailable => _delegate?.bytesAvailable ?? 0;

@override
Uint8List read(int count) => _delegate?.read(count) ?? Uint8List.fromList([]);

@override
void write(Uint8List bytes) => _delegate?.write(bytes);

@override
Future<void> dispose() async {
if (!isOpen) return;
_delegate?.close();
_delegate?.dispose();
}
}
Loading

0 comments on commit cb34671

Please sign in to comment.