From bd6811fb1a60525b4415c2627d7c673f0d67cb59 Mon Sep 17 00:00:00 2001 From: selester11 Date: Sun, 1 Feb 2026 12:06:10 -0500 Subject: [PATCH] Add JSON config support for device USB port mappings Fixes #38. Setup script can now load device configs from JSON instead of hardcoded values. Added fromJson/toJson to Device class, a config flag to the rover script, and updated writeUdevFile to load from JSON when provided. Included an example config file for reference. --- bin/rover.dart | 6 +++-- camera_config.example.json | 51 ++++++++++++++++++++++++++++++++++++++ lib/src/device.dart | 28 +++++++++++++++++++++ lib/src/utils.dart | 40 +++++++++++++++++++++++++++--- 4 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 camera_config.example.json diff --git a/bin/rover.dart b/bin/rover.dart index 4f37e476..b0879bc5 100644 --- a/bin/rover.dart +++ b/bin/rover.dart @@ -8,6 +8,7 @@ void main(List 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"); 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); @@ -21,11 +22,12 @@ void main(List 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 ] [--help]\n${parser.usage}"); + print("\nUsage: dart run :rover [--offline] [--verbose] [--only ] [--config ] [--help]\n${parser.usage}"); return; } @@ -35,7 +37,7 @@ void main(List cliArgs) async { if (udev) { logger.info("Generating udev rules..."); - await writeUdevFile(); + await writeUdevFile(configPath: configPath); } logger.info("Done!"); diff --git a/camera_config.example.json b/camera_config.example.json new file mode 100644 index 00000000..b4760e34 --- /dev/null +++ b/camera_config.example.json @@ -0,0 +1,51 @@ +{ + "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" + } + ] +} diff --git a/lib/src/device.dart b/lib/src/device.dart index 406aa821..f106aede 100644 --- a/lib/src/device.dart +++ b/lib/src/device.dart @@ -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. @@ -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, + }; } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 58e6bbd1..ed9ece29 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -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. @@ -65,10 +66,41 @@ Future writeSystemdFile(RoverProgram program) async { } /// Writes a udev rules file to `/etc/udev/rules.d` with all devices at once. -Future writeUdevFile() async { +Future 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; + + 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); @@ -84,5 +116,5 @@ Future 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"); }