From 5216e00807b2499aac723e7665122d139df5a568 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:54:39 -0500 Subject: [PATCH] Refactor USB port handling to introduce display labels and improve state management --- lib/connector/meshcore_connector.dart | 22 +++++++++----- lib/screens/usb_screen.dart | 32 ++++++++++++--------- lib/services/usb_serial_service_native.dart | 18 ++++++++---- lib/services/usb_serial_service_web.dart | 3 +- test/screens/usb_flow_test.dart | 32 +++++++++++++++++++++ 5 files changed, 80 insertions(+), 27 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 3447eee0..b91dd49d 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -115,7 +115,8 @@ class MeshCoreConnector extends ChangeNotifier { final UsbSerialService _usbSerialService = UsbSerialService(); StreamSubscription? _usbFrameSubscription; MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth; - String? _activeUsbPort; + String? _activeUsbPortKey; + String? _activeUsbPortLabel; final List _scanResults = []; final List _contacts = []; @@ -229,7 +230,9 @@ class MeshCoreConnector extends ChangeNotifier { String get deviceIdLabel => _deviceId ?? 'Unknown'; MeshCoreTransportType get activeTransport => _activeTransport; - String? get activeUsbPort => _activeUsbPort; + String? get activeUsbPort => _activeUsbPortKey; + String? get activeUsbPortDisplayLabel => + _activeUsbPortLabel ?? _activeUsbPortKey; bool get isUsbTransportConnected => _state == MeshCoreConnectionState.connected && _activeTransport == MeshCoreTransportType.usb; @@ -778,7 +781,8 @@ class MeshCoreConnector extends ChangeNotifier { } _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPort = null; + _activeUsbPortKey = null; + _activeUsbPortLabel = null; await stopScan(); _setState(MeshCoreConnectionState.connecting); @@ -955,14 +959,16 @@ class MeshCoreConnector extends ChangeNotifier { } _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPort = null; + _activeUsbPortKey = null; + _activeUsbPortLabel = null; await stopScan(); _cancelReconnectTimer(); _manualDisconnect = false; _resetConnectionHandshakeState(); _activeTransport = MeshCoreTransportType.usb; - _activeUsbPort = portName; + _activeUsbPortKey = portName; + _activeUsbPortLabel = portName; unawaited(_backgroundService?.start()); _setState(MeshCoreConnectionState.connecting); @@ -1178,7 +1184,8 @@ class MeshCoreConnector extends ChangeNotifier { _reactionSendQueueSequence = 0; _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPort = null; + _activeUsbPortKey = null; + _activeUsbPortLabel = null; _setState(MeshCoreConnectionState.disconnected); if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) { @@ -2218,7 +2225,8 @@ class MeshCoreConnector extends ChangeNotifier { selfName != null && selfName.isNotEmpty) { _usbSerialService.updateConnectedLabel(selfName); - _activeUsbPort = _usbSerialService.activePortName ?? _activeUsbPort; + _activeUsbPortLabel = + _usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel; } _awaitingSelfInfo = false; _selfInfoRetryTimer?.cancel(); diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 38a7c672..69e95c48 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -23,6 +23,7 @@ class _UsbScreenState extends State { bool _navigatedToContacts = false; bool _didScheduleInitialLoad = false; String? _selectedPort; + String? _connectedPortDisplayLabel; String? _errorText; late final MeshCoreConnector _connector; late final VoidCallback _connectionListener; @@ -33,21 +34,19 @@ class _UsbScreenState extends State { _connector = context.read(); _connectionListener = () { if (!mounted) return; - final activeUsbPort = _connector.activeUsbPort; - if (activeUsbPort != null && - activeUsbPort.isNotEmpty && - activeUsbPort != _selectedPort) { - setState(() { - _selectedPort = activeUsbPort; - }); - } + final activeUsbPortDisplayLabel = _connector.activeUsbPortDisplayLabel; + final shouldUpdateDisplayLabel = + activeUsbPortDisplayLabel != _connectedPortDisplayLabel; if (_connector.state == MeshCoreConnectionState.disconnected) { _navigatedToContacts = false; - if (_isConnecting) { - setState(() { - _isConnecting = false; - }); - } + setState(() { + _isConnecting = false; + _connectedPortDisplayLabel = activeUsbPortDisplayLabel; + }); + } else if (shouldUpdateDisplayLabel) { + setState(() { + _connectedPortDisplayLabel = activeUsbPortDisplayLabel; + }); } if (_connector.state == MeshCoreConnectionState.connected && _connector.isUsbTransportConnected && @@ -167,7 +166,12 @@ class _UsbScreenState extends State { fit: BoxFit.scaleDown, child: Chip( label: Text( - _selectedPort == null + _connectedPortDisplayLabel != null && + _connectedPortDisplayLabel!.isNotEmpty + ? _friendlyPortName( + _connectedPortDisplayLabel!, + ) + : _selectedPort == null ? l10n.usbScreenStatus : _friendlyPortName(_selectedPort!), overflow: TextOverflow.ellipsis, diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index 8467d3af..f6b879bc 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -26,11 +26,14 @@ class UsbSerialService { StreamSubscription? _androidDataSubscription; StreamSubscription? _dataSubscription; UsbSerialStatus _status = UsbSerialStatus.disconnected; - String? _connectedPortName; + String? _connectedPortKey; + String? _connectedPortLabel; FlSerial? _serial; UsbSerialStatus get status => _status; - String? get activePortName => _connectedPortName; + String? get activePortKey => _connectedPortKey; + String? get activePortDisplayLabel => + _connectedPortLabel ?? _connectedPortKey; Stream get frameStream => _frameController.stream; bool get _useAndroidUsbHost => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; @@ -126,7 +129,8 @@ class UsbSerialService { } } - _connectedPortName = normalizedPortName; + _connectedPortKey = normalizedPortName; + _connectedPortLabel = normalizedPortName; if (_useAndroidUsbHost) { _androidDataSubscription = _androidEventChannel .receiveBroadcastStream() @@ -168,7 +172,8 @@ class UsbSerialService { if (_status == UsbSerialStatus.disconnected) return; _status = UsbSerialStatus.disconnecting; - _connectedPortName = null; + _connectedPortKey = null; + _connectedPortLabel = null; await _androidDataSubscription?.cancel(); _androidDataSubscription = null; await _dataSubscription?.cancel(); @@ -204,7 +209,10 @@ class UsbSerialService { if (trimmed.isEmpty) { return; } - _connectedPortName = trimmed; + _connectedPortLabel = buildUsbDisplayLabel( + basePortLabel: _connectedPortKey ?? trimmed, + deviceName: trimmed, + ); } void dispose() { diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 8c3900d6..87fe8e9b 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -29,7 +29,8 @@ class UsbSerialService { String _requestPortLabel = 'Choose USB Device'; UsbSerialStatus get status => _status; - String? get activePortName => _connectedPortName; + String? get activePortKey => _connectedPortKey; + String? get activePortDisplayLabel => _connectedPortName ?? _connectedPortKey; Stream get frameStream => _frameController.stream; bool get isConnected => _status == UsbSerialStatus.connected; diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 499f894b..317dc922 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -21,6 +21,7 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { int connectUsbCalls = 0; String? lastConnectPortName; String? fakeActiveUsbPort; + String? fakeActiveUsbPortDisplayLabel; bool fakeUsbTransportConnected = false; @override @@ -29,6 +30,10 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { @override String? get activeUsbPort => fakeActiveUsbPort; + @override + String? get activeUsbPortDisplayLabel => + fakeActiveUsbPortDisplayLabel ?? fakeActiveUsbPort; + @override bool get isUsbTransportConnected => fakeUsbTransportConnected; @@ -99,6 +104,33 @@ void main() { }, ); + testWidgets( + 'UsbScreen keeps raw selection while showing connector USB display label', + (tester) async { + final connector = _FakeMeshCoreConnector( + ports: ['COM6 - USB Serial Device (COM6)'], + ); + + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const UsbScreen()), + ); + await tester.pumpAndSettle(); + + connector.fakeActiveUsbPortDisplayLabel = + 'COM6 - KD3CGK mesh-utility.org'; + connector.notifyListeners(); + await tester.pump(); + + expect(find.text('KD3CGK mesh-utility.org'), findsOneWidget); + + await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.pump(); + + expect(connector.connectUsbCalls, 1); + expect(connector.lastConnectPortName, 'COM6'); + }, + ); + testWidgets('ConnectionChoiceScreen USB button reflects platform support', ( tester, ) async {