meshcore-open/lib/utils/macos_usb_device_names.dart
Ben Allfree e6c9a3fea7 wip
2026-03-05 02:25:46 -05:00

92 lines
3.1 KiB
Dart

import 'dart:io';
/// Queries the macOS IOKit registry via [ioreg] to build a map of serial port
/// callout device paths to human-readable USB device names.
///
/// The [flserial] native library uses the deprecated [IOUSBDevice] IOKit class
/// to resolve device names, but macOS 10.15+ renamed it to [IOUSBHostDevice].
/// As a result flserial always returns "n/a" for USB product/vendor info on
/// modern macOS. This utility bypasses that limitation by invoking ioreg
/// directly and parsing its output.
///
/// Returns a Map of e.g. `"/dev/cu.usbmodem1101"` → `"Nordic NRF52 DK"`.
/// Devices without a USB product name are not included in the map.
Future<Map<String, String>> queryMacOsUsbDeviceNames() async {
assert(Platform.isMacOS);
try {
final result = await Process.run('ioreg', [
'-r',
'-c',
'IOUSBHostDevice',
'-l',
], stdoutEncoding: const SystemEncoding());
if (result.exitCode != 0) return const <String, String>{};
return _parseIoregOutput(result.stdout as String);
} catch (_) {
return const <String, String>{};
}
}
Map<String, String> _parseIoregOutput(String output) {
final lines = output.split('\n');
final result = <String, String>{};
// We accumulate the current device block's properties.
// A new block starts at a line beginning with "+-o " which indicates a
// top-level IOUSBHostDevice entry in the ioreg tree.
String? currentVendor;
String? currentProduct;
final List<String> currentPorts = <String>[];
void flushBlock() {
if (currentPorts.isNotEmpty &&
(currentVendor != null || currentProduct != null)) {
final parts = <String>[
if (currentVendor != null && currentVendor!.isNotEmpty) currentVendor!,
if (currentProduct != null && currentProduct!.isNotEmpty)
currentProduct!,
];
final name = parts.join(' ');
for (final port in currentPorts) {
result[port] = name;
}
}
currentVendor = null;
currentProduct = null;
currentPorts.clear();
}
for (final line in lines) {
// A new top-level device block begins here.
if (line.startsWith('+-o ')) {
flushBlock();
continue;
}
// USB Product Name (appears at multiple depths in the tree, first wins)
final productMatch = _kProductName.firstMatch(line);
if (productMatch != null && currentProduct == null) {
currentProduct = productMatch.group(1)?.trim();
continue;
}
// USB Vendor Name
final vendorMatch = _kVendorName.firstMatch(line);
if (vendorMatch != null && currentVendor == null) {
currentVendor = vendorMatch.group(1)?.trim();
continue;
}
// IOCalloutDevice — the /dev/cu.xxx path our app uses
final calloutMatch = _kCalloutDevice.firstMatch(line);
if (calloutMatch != null) {
final port = calloutMatch.group(1)?.trim();
if (port != null && port.isNotEmpty) {
currentPorts.add(port);
}
}
}
flushBlock();
return result;
}
final RegExp _kProductName = RegExp(r'"USB Product Name" = "([^"]*)"');
final RegExp _kVendorName = RegExp(r'"USB Vendor Name" = "([^"]*)"');
final RegExp _kCalloutDevice = RegExp(r'"IOCalloutDevice" = "([^"]*)"');