Refactor USB port handling to introduce display labels and improve state management

This commit is contained in:
just_stuff_tm
2026-03-02 15:54:39 -05:00
committed by just-stuff-tm
parent a0feb129e1
commit 5216e00807
5 changed files with 80 additions and 27 deletions
+15 -7
View File
@@ -115,7 +115,8 @@ class MeshCoreConnector extends ChangeNotifier {
final UsbSerialService _usbSerialService = UsbSerialService();
StreamSubscription<Uint8List>? _usbFrameSubscription;
MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
String? _activeUsbPort;
String? _activeUsbPortKey;
String? _activeUsbPortLabel;
final List<ScanResult> _scanResults = [];
final List<Contact> _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();
+18 -14
View File
@@ -23,6 +23,7 @@ class _UsbScreenState extends State<UsbScreen> {
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<UsbScreen> {
_connector = context.read<MeshCoreConnector>();
_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<UsbScreen> {
fit: BoxFit.scaleDown,
child: Chip(
label: Text(
_selectedPort == null
_connectedPortDisplayLabel != null &&
_connectedPortDisplayLabel!.isNotEmpty
? _friendlyPortName(
_connectedPortDisplayLabel!,
)
: _selectedPort == null
? l10n.usbScreenStatus
: _friendlyPortName(_selectedPort!),
overflow: TextOverflow.ellipsis,
+13 -5
View File
@@ -26,11 +26,14 @@ class UsbSerialService {
StreamSubscription<dynamic>? _androidDataSubscription;
StreamSubscription<FlSerialEventArgs>? _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<Uint8List> 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() {
+2 -1
View File
@@ -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<Uint8List> get frameStream => _frameController.stream;
bool get isConnected => _status == UsbSerialStatus.connected;
+32
View File
@@ -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: <String>['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 {