Skip to content
Open
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
43 changes: 38 additions & 5 deletions bin/rover.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import "dart:convert";
import "dart:io";

import "package:args/args.dart";
import "package:burt_network/logging.dart";
import "package:burt_network/burt_network.dart";
import "package:rover/rover.dart";

late bool offline;
Expand All @@ -8,6 +11,7 @@ void main(List<String> cliArgs) async {
final parser = ArgParser();
final programNames = { for (final program in programs) program.name };
parser.addOption("only", help: "Only compile the given program", allowed: programNames);
parser.addOption("config", help: "Path to JSON config file for device mappings and program settings");
parser.addFlag("compile", help: "Compile all rover programs", defaultsTo: true);
parser.addFlag("udev", help: "Generate udev rules", defaultsTo: true);
parser.addFlag("offline", help: "Skip any steps that require internet", negatable: false);
Expand All @@ -21,27 +25,47 @@ void main(List<String> cliArgs) async {
final udev = args.flag("udev");
final compile = args.flag("compile");
final only = args.option("only");
final configPath = args.option("config");
Logger.level = verbose ? LogLevel.all : LogLevel.info;

if (showHelp) {
// ignore: avoid_print
print("\nUsage: dart run :rover [--offline] [--verbose] [--only <program>] [--help]\n${parser.usage}");
print("\nUsage: dart run :rover [--offline] [--verbose] [--only <program>] [--config <path>] [--help]\n${parser.usage}");
return;
}

if (compile) {
await compileAllPrograms(only);
await compileAllPrograms(only, configPath: configPath);
}

if (udev) {
logger.info("Generating udev rules...");
await writeUdevFile();
await writeUdevFile(configPath: configPath);
}

logger.info("Done!");
}

Future<void> compileAllPrograms(String? only) async {
Future<void> compileAllPrograms(String? only, {String? configPath}) async {
// Load program settings from config if provided
Map<String, bool>? programSettings;

if (configPath != null) {
final configFile = File(configPath);
if (configFile.existsSync()) {
try {
final configContent = await configFile.readAsString();
final configJson = jsonDecode(configContent) as Json;
if (configJson.containsKey("programs")) {
programSettings = Map<String, bool>.from(configJson["programs"] as Map);
logger.info("Loaded program settings from config");
}
} catch (error) {
logger.warning("Failed to load program settings from config: $error");
}
}
}

if (await needsGitSubmodules()) {
if (offline) {
logger.warning("Not all git submodules have been initialized, but --offline was passed");
Expand All @@ -58,6 +82,15 @@ Future<void> compileAllPrograms(String? only) async {
for (final program in programs) {
final name = program.name;
if (only != null && name != only) continue;

// Check if program is disabled in config
if (programSettings != null && programSettings.containsKey(name)) {
if (programSettings[name] == false) {
logger.info("Skipping $name (disabled in config)");
continue;
}
}

logger.info("Processing the $name program");

// Stop the service if it was already running
Expand Down
28 changes: 28 additions & 0 deletions lib/src/device.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import "package:burt_network/burt_network.dart";

/// The type of USB-device, which determines what subsystem it is.
enum DeviceType {
/// A camera.
Expand Down Expand Up @@ -59,4 +61,30 @@ class Device {
this.interface,
this.serialNumber,
});

/// Creates a USB device from a JSON configuration.
factory Device.fromJson(Json json) => Device(
humanName: json["humanName"] as String,
alias: json["alias"] as String,
type: DeviceType.values.byName(json["type"] as String),
manufacturer: json["manufacturer"] as String?,
product: json["product"] as String?,
index: json["index"] as int?,
port: json["port"] as String?,
interface: json["interface"] as String?,
serialNumber: json["serialNumber"] as String?,
);

/// Converts this device to a JSON map.
Json toJson() => {
"humanName": humanName,
"alias": alias,
"type": type.name,
if (manufacturer != null) "manufacturer": manufacturer,
if (product != null) "product": product,
if (index != null) "index": index,
if (port != null) "port": port,
if (interface != null) "interface": interface,
if (serialNumber != null) "serialNumber": serialNumber,
};
}
40 changes: 36 additions & 4 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "dart:convert";
import "dart:io";

import "package:burt_network/logging.dart";
import "package:burt_network/burt_network.dart" hide Device;
import "package:rover/rover.dart";

/// The logger for all scripts.
Expand Down Expand Up @@ -65,10 +66,41 @@ Future<void> writeSystemdFile(RoverProgram program) async {
}

/// Writes a udev rules file to `/etc/udev/rules.d` with all devices at once.
Future<void> writeUdevFile() async {
Future<void> writeUdevFile({String? configPath}) async {
// Load devices from config or use defaults
var deviceList = devices;

if (configPath != null) {
logger.info("Loading device config from $configPath");
final configFile = File(configPath);
if (!configFile.existsSync()) {
logger.error("Config file not found: $configPath");
exit(1);
}

try {
final configContent = await configFile.readAsString();
final configJson = jsonDecode(configContent) as Json;
final devicesJson = configJson["devices"] as List<dynamic>;

deviceList = [
for (final json in devicesJson)
Device.fromJson(json as Json),
];

logger.info("Loaded ${deviceList.length} devices from config");
} on FormatException catch (error) {
logger.error("Invalid JSON in config file: $error");
exit(1);
} catch (error) {
logger.error("Failed to parse config file: $error");
exit(1);
}
}

final buffer = StringBuffer();
buffer.writeln(udevHeader);
for (final device in devices) {
for (final device in deviceList) {
logger.debug("Generating udev rules for ${device.humanName}");
final rule = device.udevRule;
logger.trace(rule);
Expand All @@ -84,5 +116,5 @@ Future<void> writeUdevFile() async {
await runCommand("sudo", ["udevadm", "control", "--reload-rules"]);
await runCommand("sudo", ["udevadm", "trigger"]);
await udevFile.delete();
logger.debug("Generated udev rules with ${devices.length} devices");
logger.debug("Generated udev rules with ${deviceList.length} devices");
}
58 changes: 58 additions & 0 deletions rover_config.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"devices": [
{
"humanName": "Front Camera",
"alias": "rover_cam_front",
"type": "camera",
"port": "3-2:1.0",
"index": 0
},
{
"humanName": "Rear Camera",
"alias": "rover_cam_rear",
"type": "camera",
"port": "1-2.4:1.0",
"index": 0
},
{
"humanName": "Bottom Left Camera",
"alias": "rover_cam_bottom_left",
"type": "camera",
"port": "1-2.3:1.0",
"index": 0
},
{
"humanName": "Bottom Right Camera",
"alias": "rover_cam_bottom_right",
"type": "camera",
"port": "1-2.2:1.0",
"index": 0
},
{
"humanName": "RealSense RGB",
"alias": "rover_realsense_rgb",
"type": "camera",
"interface": "Intel(R) RealSense(TM) Depth Camera 435i RGB",
"index": 0
},
{
"humanName": "GPS",
"alias": "rover_gps",
"type": "serial",
"manufacturer": "u-blox AG - www.u-blox.com"
},
{
"humanName": "IMU",
"alias": "rover_imu",
"type": "serial",
"product": "CDC + MSD Demo"
}
],
"programs": {
"video": true,
"subsystems": true,
"autonomy": true,
"vision": true,
"lidar": true
}
}
Loading