From dcad5c586de7ec91f977f69a434d8448d38fe940 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 04:11:52 -0500 Subject: [PATCH] Refactor USB connection handling to use scheduled closure and improve error management in USB services --- .../meshcore/meshcore_open/MainActivity.kt | 32 ++++++++++++------- lib/connector/meshcore_connector.dart | 11 ++++++- lib/services/usb_serial_service_native.dart | 32 +++++++++++++++---- lib/services/usb_serial_service_web.dart | 30 ++++++++++++++--- 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt index 7ce40d4..e0e0723 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt @@ -105,11 +105,8 @@ class MainActivity : FlutterActivity() { "connect" -> handleUsbConnect(call, result) "write" -> handleUsbWrite(call, result) "disconnect" -> { - usbIoExecutor.execute { - closeUsbConnection() - mainHandler.post { - result.success(null) - } + scheduleCloseUsbConnection { + result.success(null) } } else -> result.notImplemented() @@ -315,7 +312,7 @@ class MainActivity : FlutterActivity() { null, ) } - closeUsbConnection() + scheduleCloseUsbConnection() } }, ).also { manager -> @@ -338,6 +335,16 @@ class MainActivity : FlutterActivity() { return driver.ports.firstOrNull() } + private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) { + usbIoExecutor.execute { + closeUsbConnection() + if (onComplete != null) { + mainHandler.post(onComplete) + } + } + } + + @Synchronized private fun closeUsbConnection() { try { ioManager?.stop() @@ -381,12 +388,13 @@ class MainActivity : FlutterActivity() { } if (connectedDeviceName == detachedName) { - closeUsbConnection() - eventSink?.error( - "usb_device_detached", - "USB device was disconnected", - null, - ) + scheduleCloseUsbConnection { + eventSink?.error( + "usb_device_detached", + "USB device was disconnected", + null, + ) + } } } diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 39a34f2..e3712ed 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1266,14 +1266,23 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer?.cancel(); if (PlatformInfo.isWeb && _activeTransport == MeshCoreTransportType.bluetooth) { - _selfInfoRetryTimer = Timer(const Duration(seconds: 10), () { + var attempts = 0; + const maxAttempts = 3; + _selfInfoRetryTimer = Timer.periodic(const Duration(seconds: 10), ( + timer, + ) { if (!isConnected || !_awaitingSelfInfo) { + timer.cancel(); return; } if (_isLoadingContacts || _isSyncingChannels || _channelSyncInFlight) { return; } + attempts += 1; unawaited(sendFrame(buildAppStartFrame())); + if (attempts >= maxAttempts) { + timer.cancel(); + } }); return; } diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index f69c6fc..7f6eb11 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -186,8 +186,7 @@ class UsbSerialService { } void dispose() { - unawaited(disconnect()); - unawaited(_frameController.close()); + unawaited(disconnect().whenComplete(_closeFrameController)); } void _handleSerialData(FlSerialEventArgs event) { @@ -197,7 +196,7 @@ class UsbSerialService { _ingestRawBytes(Uint8List.fromList(bytes)); } } catch (error, stack) { - _frameController.addError(error, stack); + _addFrameError(error, stack); } } @@ -210,13 +209,13 @@ class UsbSerialService { _ingestRawBytes(data.buffer.asUint8List()); return; } - _frameController.addError( + _addFrameError( StateError('Unexpected Android USB event payload: ${data.runtimeType}'), ); } void _handleSerialError(Object error, [StackTrace? stackTrace]) { - _frameController.addError(error, stackTrace); + _addFrameError(error, stackTrace); } void _handleSerialDone() { @@ -231,10 +230,31 @@ class UsbSerialService { ); continue; } - _frameController.add(packet.payload); + _addFrame(packet.payload); } } + void _addFrame(Uint8List payload) { + if (_frameController.isClosed) { + return; + } + _frameController.add(payload); + } + + void _addFrameError(Object error, [StackTrace? stackTrace]) { + if (_frameController.isClosed) { + return; + } + _frameController.addError(error, stackTrace); + } + + Future _closeFrameController() async { + if (_frameController.isClosed) { + return; + } + await _frameController.close(); + } + void _logFrameSummary(String prefix, Uint8List bytes) { if (bytes.isEmpty) { debugPrint('$prefix len=0'); diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 1f0fcb9..71e5127 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -160,8 +160,7 @@ class UsbSerialService { } void dispose() { - unawaited(disconnect()); - unawaited(_frameController.close()); + unawaited(disconnect().whenComplete(_closeFrameController)); } Future> _getAuthorizedPorts() async { @@ -285,12 +284,12 @@ class UsbSerialService { } } catch (error, stackTrace) { if (_status == UsbSerialStatus.connected) { - _frameController.addError(error, stackTrace); + _addFrameError(error, stackTrace); } } finally { _releaseLock(reader); if (_status == UsbSerialStatus.connected && identical(reader, _reader)) { - _frameController.addError(StateError('USB serial connection closed')); + _addFrameError(StateError('USB serial connection closed')); } } } @@ -402,10 +401,31 @@ class UsbSerialService { ); continue; } - _frameController.add(packet.payload); + _addFrame(packet.payload); } } + void _addFrame(Uint8List payload) { + if (_frameController.isClosed) { + return; + } + _frameController.add(payload); + } + + void _addFrameError(Object error, [StackTrace? stackTrace]) { + if (_frameController.isClosed) { + return; + } + _frameController.addError(error, stackTrace); + } + + Future _closeFrameController() async { + if (_frameController.isClosed) { + return; + } + await _frameController.close(); + } + void _logFrameSummary(String prefix, Uint8List bytes) { if (bytes.isEmpty) { debugPrint('$prefix len=0');