mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-30 15:23:02 +10:00
Enhance USB functionality by adding request port label management and platform support checks
This commit is contained in:
committed by
just-stuff-tm
parent
ca5784f3f8
commit
781090243c
@@ -767,6 +767,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<List<String>> listUsbPorts() => _usbSerialService.listPorts();
|
Future<List<String>> listUsbPorts() => _usbSerialService.listPorts();
|
||||||
|
|
||||||
|
void setUsbRequestPortLabel(String label) {
|
||||||
|
_usbSerialService.setRequestPortLabel(label);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> connect(BluetoothDevice device, {String? displayName}) async {
|
Future<void> connect(BluetoothDevice device, {String? displayName}) async {
|
||||||
if (_state == MeshCoreConnectionState.connecting ||
|
if (_state == MeshCoreConnectionState.connecting ||
|
||||||
_state == MeshCoreConnectionState.connected) {
|
_state == MeshCoreConnectionState.connected) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:math' as math;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
import 'scanner_screen.dart';
|
import 'scanner_screen.dart';
|
||||||
import 'usb_screen.dart';
|
import 'usb_screen.dart';
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ class ConnectionChoiceScreen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final usbSupported = PlatformInfo.supportsUsbSerial;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: FittedBox(
|
title: FittedBox(
|
||||||
@@ -82,14 +84,18 @@ class ConnectionChoiceScreen extends StatelessWidget {
|
|||||||
label: l10n.connectionChoiceUsbLabel,
|
label: l10n.connectionChoiceUsbLabel,
|
||||||
color: theme.colorScheme.primaryContainer,
|
color: theme.colorScheme.primaryContainer,
|
||||||
iconColor: theme.colorScheme.onPrimaryContainer,
|
iconColor: theme.colorScheme.onPrimaryContainer,
|
||||||
onPressed: () {
|
onPressed: usbSupported
|
||||||
debugPrint(
|
? () {
|
||||||
'ConnectionChoiceScreen: USB selected, opening UsbScreen',
|
debugPrint(
|
||||||
);
|
'ConnectionChoiceScreen: USB selected, opening UsbScreen',
|
||||||
Navigator.of(context).push(
|
);
|
||||||
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
Navigator.of(context).push(
|
||||||
);
|
MaterialPageRoute(
|
||||||
},
|
builder: (_) => const UsbScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: gap),
|
SizedBox(height: gap),
|
||||||
@@ -133,7 +139,7 @@ class _ConnectionMethodButton extends StatelessWidget {
|
|||||||
|
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback? onPressed;
|
||||||
final Color color;
|
final Color color;
|
||||||
final Color iconColor;
|
final Color iconColor;
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
unawaited(_loadPorts());
|
unawaited(_loadPorts());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_connector.removeListener(_connectionListener);
|
_connector.removeListener(_connectionListener);
|
||||||
@@ -389,6 +395,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
|
|
||||||
Future<void> _loadPorts() async {
|
Future<void> _loadPorts() async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoadingPorts = true;
|
_isLoadingPorts = true;
|
||||||
@@ -425,6 +432,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
if (selectedPort == null || selectedPort.isEmpty) {
|
if (selectedPort == null || selectedPort.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||||
if (_connector.state != MeshCoreConnectionState.disconnected) {
|
if (_connector.state != MeshCoreConnectionState.disconnected) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isConnecting = false;
|
_isConnecting = false;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flserial/flserial_exception.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
import '../utils/usb_port_labels.dart';
|
import '../utils/usb_port_labels.dart';
|
||||||
import 'usb_serial_frame_codec.dart';
|
import 'usb_serial_frame_codec.dart';
|
||||||
|
|
||||||
@@ -21,28 +22,38 @@ class UsbSerialService {
|
|||||||
);
|
);
|
||||||
final StreamController<Uint8List> _frameController =
|
final StreamController<Uint8List> _frameController =
|
||||||
StreamController<Uint8List>.broadcast();
|
StreamController<Uint8List>.broadcast();
|
||||||
final FlSerial _serial = FlSerial();
|
|
||||||
final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder();
|
final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder();
|
||||||
StreamSubscription<dynamic>? _androidDataSubscription;
|
StreamSubscription<dynamic>? _androidDataSubscription;
|
||||||
StreamSubscription<FlSerialEventArgs>? _dataSubscription;
|
StreamSubscription<FlSerialEventArgs>? _dataSubscription;
|
||||||
UsbSerialStatus _status = UsbSerialStatus.disconnected;
|
UsbSerialStatus _status = UsbSerialStatus.disconnected;
|
||||||
String? _connectedPortName;
|
String? _connectedPortName;
|
||||||
|
FlSerial? _serial;
|
||||||
|
|
||||||
UsbSerialStatus get status => _status;
|
UsbSerialStatus get status => _status;
|
||||||
String? get activePortName => _connectedPortName;
|
String? get activePortName => _connectedPortName;
|
||||||
Stream<Uint8List> get frameStream => _frameController.stream;
|
Stream<Uint8List> get frameStream => _frameController.stream;
|
||||||
bool get _useAndroidUsbHost =>
|
bool get _useAndroidUsbHost =>
|
||||||
!kIsWeb && defaultTargetPlatform == TargetPlatform.android;
|
!kIsWeb && defaultTargetPlatform == TargetPlatform.android;
|
||||||
|
bool get _useDesktopFlSerial =>
|
||||||
|
PlatformInfo.isWindows || PlatformInfo.isLinux;
|
||||||
|
bool get _isSupportedPlatform => _useAndroidUsbHost || _useDesktopFlSerial;
|
||||||
|
FlSerial get _nativeSerial => _serial ??= FlSerial();
|
||||||
|
|
||||||
bool get isConnected {
|
bool get isConnected {
|
||||||
|
if (!_isSupportedPlatform) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (_useAndroidUsbHost) {
|
if (_useAndroidUsbHost) {
|
||||||
return _status == UsbSerialStatus.connected;
|
return _status == UsbSerialStatus.connected;
|
||||||
}
|
}
|
||||||
return _status == UsbSerialStatus.connected &&
|
return _status == UsbSerialStatus.connected &&
|
||||||
_serial.isOpen() == FlOpenStatus.open;
|
_serial?.isOpen() == FlOpenStatus.open;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> listPorts() async {
|
Future<List<String>> listPorts() async {
|
||||||
|
if (!_isSupportedPlatform) {
|
||||||
|
return const <String>[];
|
||||||
|
}
|
||||||
if (_useAndroidUsbHost) {
|
if (_useAndroidUsbHost) {
|
||||||
final ports = await _androidMethodChannel.invokeListMethod<String>(
|
final ports = await _androidMethodChannel.invokeListMethod<String>(
|
||||||
'listPorts',
|
'listPorts',
|
||||||
@@ -60,6 +71,9 @@ class UsbSerialService {
|
|||||||
_status == UsbSerialStatus.connecting) {
|
_status == UsbSerialStatus.connecting) {
|
||||||
throw StateError('USB serial transport is already active');
|
throw StateError('USB serial transport is already active');
|
||||||
}
|
}
|
||||||
|
if (!_isSupportedPlatform) {
|
||||||
|
throw UnsupportedError('USB serial is not supported on this platform.');
|
||||||
|
}
|
||||||
|
|
||||||
_status = UsbSerialStatus.connecting;
|
_status = UsbSerialStatus.connecting;
|
||||||
final normalizedPortName = normalizeUsbPortName(portName);
|
final normalizedPortName = normalizeUsbPortName(portName);
|
||||||
@@ -78,32 +92,35 @@ class UsbSerialService {
|
|||||||
throw StateError(error.message ?? error.code);
|
throw StateError(error.message ?? error.code);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_serial.init();
|
final serial = _nativeSerial;
|
||||||
|
serial.init();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final status = _serial.openPort(normalizedPortName, baudRate);
|
final status = serial.openPort(normalizedPortName, baudRate);
|
||||||
if (status != FlOpenStatus.open) {
|
if (status != FlOpenStatus.open) {
|
||||||
throw StateError(
|
throw StateError(
|
||||||
'Failed to open USB port $normalizedPortName ($status)',
|
'Failed to open USB port $normalizedPortName ($status)',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_serial.setByteSize8();
|
serial.setByteSize8();
|
||||||
_serial.setBitParityNone();
|
serial.setBitParityNone();
|
||||||
_serial.setStopBits1();
|
serial.setStopBits1();
|
||||||
_serial.setFlowControlNone();
|
serial.setFlowControlNone();
|
||||||
_serial.setRTS(false);
|
serial.setRTS(false);
|
||||||
_serial.setDTR(true);
|
serial.setDTR(true);
|
||||||
debugPrint(
|
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) {
|
} on FlSerialException catch (error) {
|
||||||
_serial.free();
|
_serial?.free();
|
||||||
|
_serial = null;
|
||||||
_status = UsbSerialStatus.disconnected;
|
_status = UsbSerialStatus.disconnected;
|
||||||
throw StateError(
|
throw StateError(
|
||||||
'Failed to open USB port $normalizedPortName: ${error.msg} (${error.error})',
|
'Failed to open USB port $normalizedPortName: ${error.msg} (${error.error})',
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
_serial.free();
|
_serial?.free();
|
||||||
|
_serial = null;
|
||||||
_status = UsbSerialStatus.disconnected;
|
_status = UsbSerialStatus.disconnected;
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -119,7 +136,7 @@ class UsbSerialService {
|
|||||||
onDone: _handleSerialDone,
|
onDone: _handleSerialDone,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_dataSubscription = _serial.onSerialData.stream.listen(
|
_dataSubscription = _nativeSerial.onSerialData.stream.listen(
|
||||||
_handleSerialData,
|
_handleSerialData,
|
||||||
onError: _handleSerialError,
|
onError: _handleSerialError,
|
||||||
onDone: _handleSerialDone,
|
onDone: _handleSerialDone,
|
||||||
@@ -143,7 +160,7 @@ class UsbSerialService {
|
|||||||
throw StateError(error.message ?? error.code);
|
throw StateError(error.message ?? error.code);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_serial.write(packet);
|
_nativeSerial.write(packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,18 +182,23 @@ class UsbSerialService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
if (_serial.isOpen() == FlOpenStatus.open) {
|
if (_serial?.isOpen() == FlOpenStatus.open) {
|
||||||
_serial.closePort();
|
_serial?.closePort();
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Ignore errors while closing.
|
// Ignore errors while closing.
|
||||||
}
|
}
|
||||||
|
|
||||||
_serial.free();
|
_serial?.free();
|
||||||
|
_serial = null;
|
||||||
}
|
}
|
||||||
_status = UsbSerialStatus.disconnected;
|
_status = UsbSerialStatus.disconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setRequestPortLabel(String label) {
|
||||||
|
// Native implementations do not use a synthetic chooser row.
|
||||||
|
}
|
||||||
|
|
||||||
void updateConnectedLabel(String label) {
|
void updateConnectedLabel(String label) {
|
||||||
final trimmed = label.trim();
|
final trimmed = label.trim();
|
||||||
if (trimmed.isEmpty) {
|
if (trimmed.isEmpty) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class UsbSerialService {
|
|||||||
JSObject? _writer;
|
JSObject? _writer;
|
||||||
String? _connectedPortName;
|
String? _connectedPortName;
|
||||||
String? _connectedPortKey;
|
String? _connectedPortKey;
|
||||||
|
String _requestPortLabel = 'Choose USB Device';
|
||||||
|
|
||||||
UsbSerialStatus get status => _status;
|
UsbSerialStatus get status => _status;
|
||||||
String? get activePortName => _connectedPortName;
|
String? get activePortName => _connectedPortName;
|
||||||
@@ -49,7 +50,7 @@ class UsbSerialService {
|
|||||||
|
|
||||||
final ports = await _getAuthorizedPorts();
|
final ports = await _getAuthorizedPorts();
|
||||||
if (ports.isEmpty) {
|
if (ports.isEmpty) {
|
||||||
return const <String>[usbRequestPortLabel];
|
return <String>[_requestPortLabel];
|
||||||
}
|
}
|
||||||
return ports.map(_displayLabelForPort).toList(growable: false);
|
return ports.map(_displayLabelForPort).toList(growable: false);
|
||||||
}
|
}
|
||||||
@@ -159,6 +160,14 @@ class UsbSerialService {
|
|||||||
_connectedPortName = _buildDisplayLabel(portKey);
|
_connectedPortName = _buildDisplayLabel(portKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setRequestPortLabel(String label) {
|
||||||
|
final trimmed = label.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_requestPortLabel = trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
unawaited(disconnect().whenComplete(_closeFrameController));
|
unawaited(disconnect().whenComplete(_closeFrameController));
|
||||||
}
|
}
|
||||||
@@ -189,7 +198,7 @@ class UsbSerialService {
|
|||||||
if (ports.isEmpty) {
|
if (ports.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (requestedPortName.isEmpty || requestedPortName == usbRequestPortLabel) {
|
if (requestedPortName.isEmpty || requestedPortName == _requestPortLabel) {
|
||||||
return ports.first;
|
return ports.first;
|
||||||
}
|
}
|
||||||
for (final port in ports) {
|
for (final port in ports) {
|
||||||
@@ -350,7 +359,7 @@ class UsbSerialService {
|
|||||||
try {
|
try {
|
||||||
final info = port.callMethod<JSAny?>('getInfo'.toJS);
|
final info = port.callMethod<JSAny?>('getInfo'.toJS);
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
return usbRequestPortLabel;
|
return _requestPortLabel;
|
||||||
}
|
}
|
||||||
final infoObject = info as JSObject;
|
final infoObject = info as JSObject;
|
||||||
|
|
||||||
@@ -366,10 +375,11 @@ class UsbSerialService {
|
|||||||
return describeWebUsbPort(
|
return describeWebUsbPort(
|
||||||
vendorId: hasVendor ? vendorId.toInt() : null,
|
vendorId: hasVendor ? vendorId.toInt() : null,
|
||||||
productId: hasProduct ? productId.toInt() : null,
|
productId: hasProduct ? productId.toInt() : null,
|
||||||
|
requestPortLabel: _requestPortLabel,
|
||||||
knownUsbNames: _knownUsbNames,
|
knownUsbNames: _knownUsbNames,
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return usbRequestPortLabel;
|
return _requestPortLabel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,4 +33,14 @@ class PlatformInfo {
|
|||||||
|
|
||||||
/// Whether the app is running on a desktop platform (macOS, Windows, or Linux).
|
/// Whether the app is running on a desktop platform (macOS, Windows, or Linux).
|
||||||
static bool get isDesktop => isMacOS || isWindows || isLinux;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
const String usbRequestPortLabel = 'Choose USB Device';
|
|
||||||
|
|
||||||
String normalizeUsbPortName(String portLabel) {
|
String normalizeUsbPortName(String portLabel) {
|
||||||
final separatorIndex = portLabel.indexOf(' - ');
|
final separatorIndex = portLabel.indexOf(' - ');
|
||||||
final normalized = separatorIndex >= 0
|
final normalized = separatorIndex >= 0
|
||||||
@@ -23,10 +21,11 @@ String friendlyUsbPortName(String portLabel) {
|
|||||||
String describeWebUsbPort({
|
String describeWebUsbPort({
|
||||||
required int? vendorId,
|
required int? vendorId,
|
||||||
required int? productId,
|
required int? productId,
|
||||||
|
String requestPortLabel = 'Choose USB Device',
|
||||||
Map<String, String> knownUsbNames = const <String, String>{},
|
Map<String, String> knownUsbNames = const <String, String>{},
|
||||||
}) {
|
}) {
|
||||||
if (vendorId == null && productId == null) {
|
if (vendorId == null && productId == null) {
|
||||||
return usbRequestPortLabel;
|
return requestPortLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
final vendorHex = vendorId?.toRadixString(16).padLeft(4, '0').toUpperCase();
|
final vendorHex = vendorId?.toRadixString(16).padLeft(4, '0').toUpperCase();
|
||||||
|
|||||||
Reference in New Issue
Block a user