From 4c7ee3b3b0300d8200019f8930df15a1ce3634d0 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:54:12 -0500 Subject: [PATCH] Enhance USB serial services with debug logging and reset functionality - Introduced debug logging in USB serial services for better traceability. - Added reset method to UsbSerialFrameDecoder to clear buffered data. - Updated tests to verify the reset functionality of the decoder. --- lib/connector/meshcore_connector.dart | 113 ++++++++++++++---- lib/services/usb_serial_frame_codec.dart | 5 + lib/services/usb_serial_service_native.dart | 24 +++- lib/services/usb_serial_service_web.dart | 23 +++- .../services/usb_serial_frame_codec_test.dart | 18 +++ 5 files changed, 153 insertions(+), 30 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b3aaeab..a589849 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -350,11 +350,38 @@ class MeshCoreConnector extends ChangeNotifier { ? allMessages.sublist(allMessages.length - _messageWindowSize) : allMessages; - _conversations[contactKeyHex] = windowedMessages; + final currentMessages = + _conversations[contactKeyHex] ?? const []; + final mergedMessages = [...windowedMessages]; + final existingKeys = { + for (final message in windowedMessages) _messageMergeKey(message), + }; + + for (final message in currentMessages) { + final key = _messageMergeKey(message); + if (existingKeys.add(key)) { + mergedMessages.add(message); + } + } + + mergedMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + final windowedMergedMessages = mergedMessages.length > _messageWindowSize + ? mergedMessages.sublist(mergedMessages.length - _messageWindowSize) + : mergedMessages; + + _conversations[contactKeyHex] = windowedMergedMessages; notifyListeners(); } } + String _messageMergeKey(Message message) { + final messageId = message.messageId; + if (messageId != null && messageId.isNotEmpty) { + return 'id:$messageId'; + } + return 'fallback:${message.isOutgoing}:${message.timestamp.millisecondsSinceEpoch}:${message.text}'; + } + /// Load older messages for a contact (pagination) Future> loadOlderMessages( String contactKeyHex, { @@ -590,6 +617,7 @@ class MeshCoreConnector extends ChangeNotifier { _bleDebugLogService = bleDebugLogService; _appDebugLogService = appDebugLogService; _backgroundService = backgroundService; + _usbSerialService.setDebugLogService(_appDebugLogService); // Initialize notification service _notificationService.initialize(); @@ -749,7 +777,7 @@ class MeshCoreConnector extends ChangeNotifier { androidScanMode: AndroidScanMode.lowLatency, ); } catch (error) { - debugPrint('[BLE Scan] Scan/picker failure: $error'); + _appDebugLogService?.warn('Scan/picker failure: $error', tag: 'BLE Scan'); _setState(MeshCoreConnectionState.disconnected); rethrow; } @@ -806,7 +834,10 @@ class MeshCoreConnector extends ChangeNotifier { try { final connectLabel = _deviceDisplayName ?? _deviceId; - debugPrint('[BLE Connect] Starting connect to $connectLabel'); + _appDebugLogService?.info( + 'Starting connect to $connectLabel', + tag: 'BLE Connect', + ); _connectionSubscription = device.connectionState.listen((state) { if (state == BluetoothConnectionState.disconnected && isConnected) { _handleDisconnection(); @@ -820,7 +851,10 @@ class MeshCoreConnector extends ChangeNotifier { license: License.free, ); } catch (error) { - debugPrint('[BLE Connect] device.connect() failure: $error'); + _appDebugLogService?.error( + 'device.connect() failure: $error', + tag: 'BLE Connect', + ); rethrow; } @@ -828,9 +862,12 @@ class MeshCoreConnector extends ChangeNotifier { if (!PlatformInfo.isWeb) { try { final mtu = await device.requestMtu(185); - debugPrint('MTU set to: $mtu'); + _appDebugLogService?.info('MTU set to: $mtu', tag: 'BLE Connect'); } catch (e) { - debugPrint('MTU request failed: $e, using default'); + _appDebugLogService?.warn( + 'MTU request failed: $e, using default', + tag: 'BLE Connect', + ); } } @@ -838,11 +875,15 @@ class MeshCoreConnector extends ChangeNotifier { try { services = await device.discoverServices(); } catch (error) { - debugPrint('[BLE Connect] service discovery failure: $error'); + _appDebugLogService?.error( + 'service discovery failure: $error', + tag: 'BLE Connect', + ); if (PlatformInfo.isWeb && error.toString().contains('GATT Server is disconnected')) { - debugPrint( - '[BLE Connect] retrying service discovery after transient web disconnect', + _appDebugLogService?.warn( + 'retrying service discovery after transient web disconnect', + tag: 'BLE Connect', ); await Future.delayed(const Duration(milliseconds: 300)); await device.connect( @@ -882,17 +923,32 @@ class MeshCoreConnector extends ChangeNotifier { } if (PlatformInfo.isWeb) { - debugPrint('Starting setNotifyValue(true)'); - debugPrint('Web: Calling setNotifyValue(true) without awaiting'); + _appDebugLogService?.info( + 'Starting setNotifyValue(true)', + tag: 'BLE Connect', + ); + _appDebugLogService?.info( + 'Web: Calling setNotifyValue(true) without awaiting', + tag: 'BLE Connect', + ); unawaited(() async { try { await _txCharacteristic!.setNotifyValue(true); } catch (error) { - debugPrint('[BLE Connect] notify failure (web, ignored): $error'); - debugPrint('Web setNotifyValue error (ignoring): $error'); + _appDebugLogService?.warn( + 'notify failure (web, ignored): $error', + tag: 'BLE Connect', + ); + _appDebugLogService?.warn( + 'Web setNotifyValue error (ignoring): $error', + tag: 'BLE Connect', + ); } }()); - debugPrint('setNotifyValue(true) configuration completed'); + _appDebugLogService?.info( + 'setNotifyValue(true) configuration completed', + tag: 'BLE Connect', + ); } else { bool notifySet = false; for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { @@ -903,8 +959,11 @@ class MeshCoreConnector extends ChangeNotifier { await _txCharacteristic!.setNotifyValue(true); notifySet = true; } catch (e) { - debugPrint('[BLE Connect] notify failure: $e'); - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + _appDebugLogService?.warn('notify failure: $e', tag: 'BLE Connect'); + _appDebugLogService?.warn( + 'setNotifyValue attempt ${attempt + 1}/3 failed: $e', + tag: 'BLE Connect', + ); if (attempt == 2) rethrow; } } @@ -925,7 +984,19 @@ class MeshCoreConnector extends ChangeNotifier { _activeTransport == MeshCoreTransportType.bluetooth) { // Chrome's Web Bluetooth stack commonly delays incoming notifications // until the non-blocking notify setup settles. Avoid stacking extra - // startup writes while that is happening. + // startup writes while that is happening. Defer the clock sync until + // the connection has had time to settle. + unawaited( + Future(() async { + await Future.delayed(const Duration(seconds: 5)); + if (!isConnected || + !PlatformInfo.isWeb || + _activeTransport != MeshCoreTransportType.bluetooth) { + return; + } + await syncTime(); + }), + ); } else { final gotSelfInfo = await _waitForSelfInfo( timeout: const Duration(seconds: 3), @@ -943,7 +1014,7 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(getChannels()); } } catch (e) { - debugPrint("Connection error: $e"); + _appDebugLogService?.error('Connection error: $e', tag: 'BLE Connect'); await disconnect(manual: false); rethrow; } @@ -986,7 +1057,7 @@ class MeshCoreConnector extends ChangeNotifier { _usbFrameSubscription = _usbSerialService.frameStream.listen( _handleFrame, onError: (error, stackTrace) { - debugPrint('USB transport error: $error'); + _appDebugLogService?.error('USB transport error: $error', tag: 'USB'); unawaited(disconnect(manual: false)); }, onDone: () { @@ -1013,7 +1084,7 @@ class MeshCoreConnector extends ChangeNotifier { await syncTime(); } catch (error) { - debugPrint('USB connection error: $error'); + _appDebugLogService?.error('USB connection error: $error', tag: 'USB'); await disconnect(manual: false); rethrow; } @@ -1149,7 +1220,7 @@ class MeshCoreConnector extends ChangeNotifier { // Skip queued BLE operations so disconnect doesn't get stuck behind them. await _device?.disconnect(queue: false); } catch (e) { - debugPrint("Disconnect error: $e"); + _appDebugLogService?.warn('Disconnect error: $e', tag: 'BLE Connect'); } _device = null; diff --git a/lib/services/usb_serial_frame_codec.dart b/lib/services/usb_serial_frame_codec.dart index ebe1733..59e4d4b 100644 --- a/lib/services/usb_serial_frame_codec.dart +++ b/lib/services/usb_serial_frame_codec.dart @@ -37,6 +37,11 @@ class UsbSerialFrameDecoder { final List _rxBuffer = []; int _startIndex = 0; + void reset() { + _rxBuffer.clear(); + _startIndex = 0; + } + List ingest(Uint8List bytes) { if (bytes.isEmpty) { return const []; diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index f6b879b..d79205b 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 'app_debug_log_service.dart'; import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; import 'usb_serial_frame_codec.dart'; @@ -29,6 +30,7 @@ class UsbSerialService { String? _connectedPortKey; String? _connectedPortLabel; FlSerial? _serial; + AppDebugLogService? _debugLogService; UsbSerialStatus get status => _status; String? get activePortKey => _connectedPortKey; @@ -66,6 +68,10 @@ class UsbSerialService { return Future.value(FlSerial.listPorts()); } + void setDebugLogService(AppDebugLogService? service) { + _debugLogService = service; + } + Future connect({ required String portName, int baudRate = 115200, @@ -80,6 +86,7 @@ class UsbSerialService { _status = UsbSerialStatus.connecting; final normalizedPortName = normalizeUsbPortName(portName); + _frameDecoder.reset(); if (_useAndroidUsbHost) { try { @@ -87,8 +94,9 @@ class UsbSerialService { 'portName': normalizedPortName, 'baudRate': baudRate, }); - debugPrint( + _debugLogService?.info( 'USB serial opened port=$normalizedPortName on Android via USB host bridge', + tag: 'USB Serial', ); } on PlatformException catch (error) { _status = UsbSerialStatus.disconnected; @@ -111,8 +119,9 @@ class UsbSerialService { serial.setFlowControlNone(); serial.setRTS(false); serial.setDTR(true); - debugPrint( + _debugLogService?.info( 'USB serial opened port=$normalizedPortName cts=${serial.getCTS()} dsr=${serial.getDSR()} dtr=true rts=false', + tag: 'USB Serial', ); } on FlSerialException catch (error) { _serial?.free(); @@ -174,6 +183,7 @@ class UsbSerialService { _status = UsbSerialStatus.disconnecting; _connectedPortKey = null; _connectedPortLabel = null; + _frameDecoder.reset(); await _androidDataSubscription?.cancel(); _androidDataSubscription = null; await _dataSubscription?.cancel(); @@ -255,8 +265,9 @@ class UsbSerialService { void _ingestRawBytes(Uint8List bytes) { for (final packet in _frameDecoder.ingest(bytes)) { if (!packet.isRxFrame) { - debugPrint( + _debugLogService?.info( 'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}', + tag: 'USB Serial', ); continue; } @@ -287,10 +298,13 @@ class UsbSerialService { void _logFrameSummary(String prefix, Uint8List bytes) { if (bytes.isEmpty) { - debugPrint('$prefix len=0'); + _debugLogService?.info('$prefix len=0', tag: 'USB Serial'); return; } - debugPrint('$prefix code=${bytes[0]} len=${bytes.length}'); + _debugLogService?.info( + '$prefix code=${bytes[0]} len=${bytes.length}', + tag: 'USB Serial', + ); } } diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 87fe8e9..974928e 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -5,6 +5,7 @@ import 'dart:js_interop_unsafe'; import 'package:flutter/foundation.dart'; import 'package:web/web.dart' as web; +import 'app_debug_log_service.dart'; import '../utils/usb_port_labels.dart'; import 'usb_serial_frame_codec.dart'; @@ -27,6 +28,7 @@ class UsbSerialService { String? _connectedPortName; String? _connectedPortKey; String _requestPortLabel = 'Choose USB Device'; + AppDebugLogService? _debugLogService; UsbSerialStatus get status => _status; String? get activePortKey => _connectedPortKey; @@ -69,6 +71,7 @@ class UsbSerialService { } _status = UsbSerialStatus.connecting; + _frameDecoder.reset(); try { final requestedPortName = normalizeUsbPortName(portName); @@ -88,7 +91,10 @@ class UsbSerialService { _status = UsbSerialStatus.connected; unawaited(_pumpReads()); - debugPrint('USB serial opened port=$_connectedPortName via Web Serial'); + _debugLogService?.info( + 'USB serial opened port=$_connectedPortName via Web Serial', + tag: 'USB Serial', + ); } catch (error) { await _cleanupFailedConnect(); _status = UsbSerialStatus.disconnected; @@ -126,6 +132,7 @@ class UsbSerialService { _port = null; _connectedPortName = null; _connectedPortKey = null; + _frameDecoder.reset(); if (reader != null) { try { @@ -169,6 +176,10 @@ class UsbSerialService { _requestPortLabel = trimmed; } + void setDebugLogService(AppDebugLogService? service) { + _debugLogService = service; + } + void dispose() { unawaited(disconnect().whenComplete(_closeFrameController)); } @@ -407,8 +418,9 @@ class UsbSerialService { void _ingestRawBytes(Uint8List bytes) { for (final packet in _frameDecoder.ingest(bytes)) { if (!packet.isRxFrame) { - debugPrint( + _debugLogService?.info( 'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}', + tag: 'USB Serial', ); continue; } @@ -439,10 +451,13 @@ class UsbSerialService { void _logFrameSummary(String prefix, Uint8List bytes) { if (bytes.isEmpty) { - debugPrint('$prefix len=0'); + _debugLogService?.info('$prefix len=0', tag: 'USB Serial'); return; } - debugPrint('$prefix code=${bytes[0]} len=${bytes.length}'); + _debugLogService?.info( + '$prefix code=${bytes[0]} len=${bytes.length}', + tag: 'USB Serial', + ); } } diff --git a/test/services/usb_serial_frame_codec_test.dart b/test/services/usb_serial_frame_codec_test.dart index be4497e..54165de 100644 --- a/test/services/usb_serial_frame_codec_test.dart +++ b/test/services/usb_serial_frame_codec_test.dart @@ -120,4 +120,22 @@ void main() { expect(packets.single.payload, orderedEquals([0x44])); }, ); + + test('UsbSerialFrameDecoder reset clears buffered partial data', () { + final decoder = UsbSerialFrameDecoder(); + + expect( + decoder.ingest(Uint8List.fromList([usbSerialRxFrameStart, 0x02])), + isEmpty, + ); + + decoder.reset(); + + final packets = decoder.ingest( + Uint8List.fromList([usbSerialRxFrameStart, 0x01, 0x00, 0x55]), + ); + + expect(packets, hasLength(1)); + expect(packets.single.payload, orderedEquals([0x55])); + }); }