From 781090243cf0b2155024830b9d55a66c201341fd Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 05:28:31 -0500 Subject: [PATCH] Enhance USB functionality by adding request port label management and platform support checks --- lib/connector/meshcore_connector.dart | 4 ++ lib/screens/connection_choice_screen.dart | 24 +++++---- lib/screens/usb_screen.dart | 8 +++ lib/services/usb_serial_service_native.dart | 58 ++++++++++++++------- lib/services/usb_serial_service_web.dart | 18 +++++-- lib/utils/platform_info.dart | 10 ++++ lib/utils/usb_port_labels.dart | 5 +- 7 files changed, 93 insertions(+), 34 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index e3712ed..3447eee 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -767,6 +767,10 @@ class MeshCoreConnector extends ChangeNotifier { Future> listUsbPorts() => _usbSerialService.listPorts(); + void setUsbRequestPortLabel(String label) { + _usbSerialService.setRequestPortLabel(label); + } + Future connect(BluetoothDevice device, {String? displayName}) async { if (_state == MeshCoreConnectionState.connecting || _state == MeshCoreConnectionState.connected) { diff --git a/lib/screens/connection_choice_screen.dart b/lib/screens/connection_choice_screen.dart index e4abe7f..16634c6 100644 --- a/lib/screens/connection_choice_screen.dart +++ b/lib/screens/connection_choice_screen.dart @@ -3,6 +3,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import '../l10n/l10n.dart'; +import '../utils/platform_info.dart'; import 'scanner_screen.dart'; import 'usb_screen.dart'; @@ -14,6 +15,7 @@ class ConnectionChoiceScreen extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; final theme = Theme.of(context); + final usbSupported = PlatformInfo.supportsUsbSerial; return Scaffold( appBar: AppBar( title: FittedBox( @@ -82,14 +84,18 @@ class ConnectionChoiceScreen extends StatelessWidget { label: l10n.connectionChoiceUsbLabel, color: theme.colorScheme.primaryContainer, iconColor: theme.colorScheme.onPrimaryContainer, - onPressed: () { - debugPrint( - 'ConnectionChoiceScreen: USB selected, opening UsbScreen', - ); - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const UsbScreen()), - ); - }, + onPressed: usbSupported + ? () { + debugPrint( + 'ConnectionChoiceScreen: USB selected, opening UsbScreen', + ); + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const UsbScreen(), + ), + ); + } + : null, ), ), SizedBox(height: gap), @@ -133,7 +139,7 @@ class _ConnectionMethodButton extends StatelessWidget { final IconData icon; final String label; - final VoidCallback onPressed; + final VoidCallback? onPressed; final Color color; final Color iconColor; diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index fb62b95..160f463 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -61,6 +61,12 @@ class _UsbScreenState extends State { unawaited(_loadPorts()); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); + } + @override void dispose() { _connector.removeListener(_connectionListener); @@ -389,6 +395,7 @@ class _UsbScreenState extends State { Future _loadPorts() async { if (!mounted) return; + _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); setState(() { _isLoadingPorts = true; @@ -425,6 +432,7 @@ class _UsbScreenState extends State { if (selectedPort == null || selectedPort.isEmpty) { return; } + _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); if (_connector.state != MeshCoreConnectionState.disconnected) { setState(() { _isConnecting = false; diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index 7f6eb11..8467d3a 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -5,6 +5,7 @@ import 'package:flserial/flserial_exception.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; import 'usb_serial_frame_codec.dart'; @@ -21,28 +22,38 @@ class UsbSerialService { ); final StreamController _frameController = StreamController.broadcast(); - final FlSerial _serial = FlSerial(); final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder(); StreamSubscription? _androidDataSubscription; StreamSubscription? _dataSubscription; UsbSerialStatus _status = UsbSerialStatus.disconnected; String? _connectedPortName; + FlSerial? _serial; UsbSerialStatus get status => _status; String? get activePortName => _connectedPortName; Stream get frameStream => _frameController.stream; bool get _useAndroidUsbHost => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + bool get _useDesktopFlSerial => + PlatformInfo.isWindows || PlatformInfo.isLinux; + bool get _isSupportedPlatform => _useAndroidUsbHost || _useDesktopFlSerial; + FlSerial get _nativeSerial => _serial ??= FlSerial(); bool get isConnected { + if (!_isSupportedPlatform) { + return false; + } if (_useAndroidUsbHost) { return _status == UsbSerialStatus.connected; } return _status == UsbSerialStatus.connected && - _serial.isOpen() == FlOpenStatus.open; + _serial?.isOpen() == FlOpenStatus.open; } Future> listPorts() async { + if (!_isSupportedPlatform) { + return const []; + } if (_useAndroidUsbHost) { final ports = await _androidMethodChannel.invokeListMethod( 'listPorts', @@ -60,6 +71,9 @@ class UsbSerialService { _status == UsbSerialStatus.connecting) { throw StateError('USB serial transport is already active'); } + if (!_isSupportedPlatform) { + throw UnsupportedError('USB serial is not supported on this platform.'); + } _status = UsbSerialStatus.connecting; final normalizedPortName = normalizeUsbPortName(portName); @@ -78,32 +92,35 @@ class UsbSerialService { throw StateError(error.message ?? error.code); } } else { - _serial.init(); + final serial = _nativeSerial; + serial.init(); try { - final status = _serial.openPort(normalizedPortName, baudRate); + final status = serial.openPort(normalizedPortName, baudRate); if (status != FlOpenStatus.open) { throw StateError( 'Failed to open USB port $normalizedPortName ($status)', ); } - _serial.setByteSize8(); - _serial.setBitParityNone(); - _serial.setStopBits1(); - _serial.setFlowControlNone(); - _serial.setRTS(false); - _serial.setDTR(true); + serial.setByteSize8(); + serial.setBitParityNone(); + serial.setStopBits1(); + serial.setFlowControlNone(); + serial.setRTS(false); + serial.setDTR(true); debugPrint( - 'USB serial opened port=$normalizedPortName cts=${_serial.getCTS()} dsr=${_serial.getDSR()} dtr=true rts=false', + 'USB serial opened port=$normalizedPortName cts=${serial.getCTS()} dsr=${serial.getDSR()} dtr=true rts=false', ); } on FlSerialException catch (error) { - _serial.free(); + _serial?.free(); + _serial = null; _status = UsbSerialStatus.disconnected; throw StateError( 'Failed to open USB port $normalizedPortName: ${error.msg} (${error.error})', ); } catch (error) { - _serial.free(); + _serial?.free(); + _serial = null; _status = UsbSerialStatus.disconnected; rethrow; } @@ -119,7 +136,7 @@ class UsbSerialService { onDone: _handleSerialDone, ); } else { - _dataSubscription = _serial.onSerialData.stream.listen( + _dataSubscription = _nativeSerial.onSerialData.stream.listen( _handleSerialData, onError: _handleSerialError, onDone: _handleSerialDone, @@ -143,7 +160,7 @@ class UsbSerialService { throw StateError(error.message ?? error.code); } } else { - _serial.write(packet); + _nativeSerial.write(packet); } } @@ -165,18 +182,23 @@ class UsbSerialService { } } else { try { - if (_serial.isOpen() == FlOpenStatus.open) { - _serial.closePort(); + if (_serial?.isOpen() == FlOpenStatus.open) { + _serial?.closePort(); } } catch (_) { // Ignore errors while closing. } - _serial.free(); + _serial?.free(); + _serial = null; } _status = UsbSerialStatus.disconnected; } + void setRequestPortLabel(String label) { + // Native implementations do not use a synthetic chooser row. + } + void updateConnectedLabel(String label) { final trimmed = label.trim(); if (trimmed.isEmpty) { diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 71e5127..8c3900d 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -26,6 +26,7 @@ class UsbSerialService { JSObject? _writer; String? _connectedPortName; String? _connectedPortKey; + String _requestPortLabel = 'Choose USB Device'; UsbSerialStatus get status => _status; String? get activePortName => _connectedPortName; @@ -49,7 +50,7 @@ class UsbSerialService { final ports = await _getAuthorizedPorts(); if (ports.isEmpty) { - return const [usbRequestPortLabel]; + return [_requestPortLabel]; } return ports.map(_displayLabelForPort).toList(growable: false); } @@ -159,6 +160,14 @@ class UsbSerialService { _connectedPortName = _buildDisplayLabel(portKey); } + void setRequestPortLabel(String label) { + final trimmed = label.trim(); + if (trimmed.isEmpty) { + return; + } + _requestPortLabel = trimmed; + } + void dispose() { unawaited(disconnect().whenComplete(_closeFrameController)); } @@ -189,7 +198,7 @@ class UsbSerialService { if (ports.isEmpty) { return null; } - if (requestedPortName.isEmpty || requestedPortName == usbRequestPortLabel) { + if (requestedPortName.isEmpty || requestedPortName == _requestPortLabel) { return ports.first; } for (final port in ports) { @@ -350,7 +359,7 @@ class UsbSerialService { try { final info = port.callMethod('getInfo'.toJS); if (info == null) { - return usbRequestPortLabel; + return _requestPortLabel; } final infoObject = info as JSObject; @@ -366,10 +375,11 @@ class UsbSerialService { return describeWebUsbPort( vendorId: hasVendor ? vendorId.toInt() : null, productId: hasProduct ? productId.toInt() : null, + requestPortLabel: _requestPortLabel, knownUsbNames: _knownUsbNames, ); } catch (_) { - return usbRequestPortLabel; + return _requestPortLabel; } } diff --git a/lib/utils/platform_info.dart b/lib/utils/platform_info.dart index dc8e27e..e3fd428 100644 --- a/lib/utils/platform_info.dart +++ b/lib/utils/platform_info.dart @@ -33,4 +33,14 @@ class PlatformInfo { /// Whether the app is running on a desktop platform (macOS, Windows, or Linux). static bool get isDesktop => isMacOS || isWindows || isLinux; + + /// Whether the current platform supports a native USB serial backend. + static bool get supportsNativeUsbSerial => isAndroid || isWindows || isLinux; + + /// Whether the current browser supports the Web Serial backend. + static bool get supportsWebSerial => isWeb && isChrome; + + /// Whether USB serial is expected to be available on the current platform. + static bool get supportsUsbSerial => + supportsNativeUsbSerial || supportsWebSerial; } diff --git a/lib/utils/usb_port_labels.dart b/lib/utils/usb_port_labels.dart index 6430f95..ff32937 100644 --- a/lib/utils/usb_port_labels.dart +++ b/lib/utils/usb_port_labels.dart @@ -1,5 +1,3 @@ -const String usbRequestPortLabel = 'Choose USB Device'; - String normalizeUsbPortName(String portLabel) { final separatorIndex = portLabel.indexOf(' - '); final normalized = separatorIndex >= 0 @@ -23,10 +21,11 @@ String friendlyUsbPortName(String portLabel) { String describeWebUsbPort({ required int? vendorId, required int? productId, + String requestPortLabel = 'Choose USB Device', Map knownUsbNames = const {}, }) { if (vendorId == null && productId == null) { - return usbRequestPortLabel; + return requestPortLabel; } final vendorHex = vendorId?.toRadixString(16).padLeft(4, '0').toUpperCase();