mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-25 03:42:55 +10:00
Refine USB transport flow
- replace Android USB dependency with app-owned USB host implementation\n- restore BLE-first scanner flow with USB secondary action\n- tighten Web Serial key handling and disconnect logging\n\nTODO (follow-up):\n- review non-English localization copy for tone and consistency\n- trim remaining unused/awkward localization strings introduced during USB UI changes
This commit is contained in:
committed by
just-stuff-tm
parent
74da9e82b5
commit
44c0670dae
@@ -166,6 +166,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
bool _hasReceivedDeviceInfo = false;
|
||||
bool _pendingInitialChannelSync = false;
|
||||
bool _pendingInitialContactsSync = false;
|
||||
bool _bleInitialSyncStarted = false;
|
||||
bool _pendingDeferredChannelSyncAfterContacts = false;
|
||||
bool _webInitialHandshakeRequestSent = false;
|
||||
bool _preserveContactsOnRefresh = false;
|
||||
static const int _defaultMaxContacts = 32;
|
||||
static const int _defaultMaxChannels = 8;
|
||||
@@ -364,6 +367,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Re-sort after merging persisted and in-memory messages so the
|
||||
// conversation window remains stable after optimistic inserts.
|
||||
mergedMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
final windowedMergedMessages = mergedMessages.length > _messageWindowSize
|
||||
? mergedMessages.sublist(mergedMessages.length - _messageWindowSize)
|
||||
@@ -820,6 +825,76 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_usbSerialService.setRequestPortLabel(label);
|
||||
}
|
||||
|
||||
Future<void> connectUsb({
|
||||
required String portName,
|
||||
int baudRate = 115200,
|
||||
}) async {
|
||||
if (_state == MeshCoreConnectionState.connecting ||
|
||||
_state == MeshCoreConnectionState.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
_activeTransport = MeshCoreTransportType.bluetooth;
|
||||
_activeUsbPortKey = null;
|
||||
_activeUsbPortLabel = null;
|
||||
|
||||
await stopScan();
|
||||
_cancelReconnectTimer();
|
||||
_manualDisconnect = false;
|
||||
_resetConnectionHandshakeState();
|
||||
_activeTransport = MeshCoreTransportType.usb;
|
||||
_activeUsbPortKey = portName;
|
||||
_activeUsbPortLabel = portName;
|
||||
_setState(MeshCoreConnectionState.connecting);
|
||||
|
||||
try {
|
||||
await _usbFrameSubscription?.cancel();
|
||||
_usbFrameSubscription = null;
|
||||
await _usbSerialService.connect(portName: portName, baudRate: baudRate);
|
||||
_activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey;
|
||||
_activeUsbPortLabel =
|
||||
_usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel;
|
||||
notifyListeners();
|
||||
if (PlatformInfo.isWeb) {
|
||||
await stopScan();
|
||||
}
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
_usbFrameSubscription = _usbSerialService.frameStream.listen(
|
||||
_handleFrame,
|
||||
onError: (error, stackTrace) {
|
||||
_appDebugLogService?.error('USB transport error: $error', tag: 'USB');
|
||||
unawaited(disconnect(manual: false));
|
||||
},
|
||||
onDone: () {
|
||||
unawaited(disconnect(manual: false));
|
||||
},
|
||||
);
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
_pendingInitialChannelSync = true;
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
var gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
if (!gotSelfInfo) {
|
||||
await refreshDeviceInfo();
|
||||
gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
if (!gotSelfInfo) {
|
||||
throw StateError('Timed out waiting for SELF_INFO during connect');
|
||||
}
|
||||
|
||||
await syncTime();
|
||||
} catch (error) {
|
||||
_appDebugLogService?.error('USB connection error: $error', tag: 'USB');
|
||||
await disconnect(manual: false);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect(BluetoothDevice device, {String? displayName}) async {
|
||||
if (_state == MeshCoreConnectionState.connecting ||
|
||||
_state == MeshCoreConnectionState.connected) {
|
||||
@@ -844,6 +919,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_lastDeviceDisplayName = _deviceDisplayName;
|
||||
_manualDisconnect = false;
|
||||
_cancelReconnectTimer();
|
||||
_bleInitialSyncStarted = false;
|
||||
if (PlatformInfo.isWeb) {
|
||||
_resetConnectionHandshakeState();
|
||||
}
|
||||
@@ -856,6 +932,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
'Starting connect to $connectLabel',
|
||||
tag: 'BLE Connect',
|
||||
);
|
||||
await _connectionSubscription?.cancel();
|
||||
_connectionSubscription = null;
|
||||
await _notifySubscription?.cancel();
|
||||
_notifySubscription = null;
|
||||
_connectionSubscription = device.connectionState.listen((state) {
|
||||
if (state == BluetoothConnectionState.disconnected && isConnected) {
|
||||
_handleDisconnection();
|
||||
@@ -899,6 +979,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
);
|
||||
if (PlatformInfo.isWeb &&
|
||||
error.toString().contains('GATT Server is disconnected')) {
|
||||
// Chrome Web Bluetooth intermittently disconnects between connect()
|
||||
// and service discovery; retry once to recover that transient state.
|
||||
_appDebugLogService?.warn(
|
||||
'retrying service discovery after transient web disconnect',
|
||||
tag: 'BLE Connect',
|
||||
@@ -995,42 +1077,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = true;
|
||||
}
|
||||
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
if (PlatformInfo.isWeb &&
|
||||
_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. Defer the clock sync until
|
||||
// the connection has had time to settle.
|
||||
unawaited(
|
||||
Future<void>(() async {
|
||||
await Future<void>.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),
|
||||
);
|
||||
if (!gotSelfInfo) {
|
||||
await refreshDeviceInfo();
|
||||
await _waitForSelfInfo(timeout: const Duration(seconds: 3));
|
||||
}
|
||||
|
||||
unawaited(syncTime());
|
||||
}
|
||||
|
||||
// Fetch channels so we can track unread counts for incoming messages
|
||||
if (!_shouldGateInitialChannelSync) {
|
||||
unawaited(getChannels());
|
||||
}
|
||||
unawaited(Future<void>.microtask(() => _startBleInitialSync()));
|
||||
} catch (e) {
|
||||
_appDebugLogService?.error('Connection error: $e', tag: 'BLE Connect');
|
||||
await disconnect(manual: false);
|
||||
@@ -1038,76 +1085,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connectUsb({
|
||||
required String portName,
|
||||
int baudRate = 115200,
|
||||
}) async {
|
||||
if (_state == MeshCoreConnectionState.connecting ||
|
||||
_state == MeshCoreConnectionState.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
_activeTransport = MeshCoreTransportType.bluetooth;
|
||||
_activeUsbPortKey = null;
|
||||
_activeUsbPortLabel = null;
|
||||
|
||||
await stopScan();
|
||||
_cancelReconnectTimer();
|
||||
_manualDisconnect = false;
|
||||
_resetConnectionHandshakeState();
|
||||
_activeTransport = MeshCoreTransportType.usb;
|
||||
_activeUsbPortKey = portName;
|
||||
_activeUsbPortLabel = portName;
|
||||
_setState(MeshCoreConnectionState.connecting);
|
||||
|
||||
try {
|
||||
await _usbFrameSubscription?.cancel();
|
||||
_usbFrameSubscription = null;
|
||||
await _usbSerialService.connect(portName: portName, baudRate: baudRate);
|
||||
_activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey;
|
||||
_activeUsbPortLabel =
|
||||
_usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel;
|
||||
notifyListeners();
|
||||
if (PlatformInfo.isWeb) {
|
||||
await stopScan();
|
||||
}
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
_usbFrameSubscription = _usbSerialService.frameStream.listen(
|
||||
_handleFrame,
|
||||
onError: (error, stackTrace) {
|
||||
_appDebugLogService?.error('USB transport error: $error', tag: 'USB');
|
||||
unawaited(disconnect(manual: false));
|
||||
},
|
||||
onDone: () {
|
||||
unawaited(disconnect(manual: false));
|
||||
},
|
||||
);
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
_pendingInitialChannelSync = true;
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
var gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
if (!gotSelfInfo) {
|
||||
await refreshDeviceInfo();
|
||||
gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
if (!gotSelfInfo) {
|
||||
throw StateError('Timed out waiting for SELF_INFO during connect');
|
||||
}
|
||||
|
||||
await syncTime();
|
||||
} catch (error) {
|
||||
_appDebugLogService?.error('USB connection error: $error', tag: 'USB');
|
||||
await disconnect(manual: false);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _waitForSelfInfo({required Duration timeout}) async {
|
||||
if (_selfPublicKey != null) return true;
|
||||
if (!isConnected) return false;
|
||||
@@ -1139,17 +1116,60 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _startBleInitialSync() async {
|
||||
if (_bleInitialSyncStarted ||
|
||||
!isConnected ||
|
||||
_activeTransport != MeshCoreTransportType.bluetooth) {
|
||||
return;
|
||||
}
|
||||
_bleInitialSyncStarted = true;
|
||||
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
|
||||
if (PlatformInfo.isWeb) {
|
||||
// Keep Web BLE startup writes light while notifications settle.
|
||||
unawaited(
|
||||
Future<void>(() async {
|
||||
await Future<void>.delayed(const Duration(seconds: 5));
|
||||
if (!isConnected ||
|
||||
!PlatformInfo.isWeb ||
|
||||
_activeTransport != MeshCoreTransportType.bluetooth) {
|
||||
return;
|
||||
}
|
||||
await syncTime();
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
if (!gotSelfInfo) {
|
||||
await refreshDeviceInfo();
|
||||
await _waitForSelfInfo(timeout: const Duration(seconds: 3));
|
||||
}
|
||||
|
||||
unawaited(syncTime());
|
||||
_pendingDeferredChannelSyncAfterContacts = true;
|
||||
}
|
||||
|
||||
void _resetConnectionHandshakeState() {
|
||||
_selfPublicKey = null;
|
||||
_selfName = null;
|
||||
_selfLatitude = null;
|
||||
_selfLongitude = null;
|
||||
_awaitingSelfInfo = false;
|
||||
_webInitialHandshakeRequestSent = false;
|
||||
_selfInfoRetryTimer?.cancel();
|
||||
_selfInfoRetryTimer = null;
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = false;
|
||||
_pendingInitialContactsSync = false;
|
||||
_bleInitialSyncStarted = false;
|
||||
_pendingDeferredChannelSyncAfterContacts = false;
|
||||
_webInitialHandshakeRequestSent = false;
|
||||
}
|
||||
|
||||
bool get _shouldAutoReconnect =>
|
||||
@@ -1205,6 +1225,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
Future<void> disconnect({bool manual = true}) async {
|
||||
if (_state == MeshCoreConnectionState.disconnecting) return;
|
||||
final transportAtDisconnect = _activeTransport;
|
||||
final transportLabel = transportAtDisconnect == MeshCoreTransportType.usb
|
||||
? 'USB'
|
||||
: 'BLE';
|
||||
|
||||
_appDebugLogService?.info(
|
||||
'Starting disconnect transport=$transportLabel manual=$manual',
|
||||
tag: 'Connection',
|
||||
);
|
||||
|
||||
if (manual) {
|
||||
_manualDisconnect = true;
|
||||
@@ -1280,6 +1308,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_activeUsbPortLabel = null;
|
||||
|
||||
_setState(MeshCoreConnectionState.disconnected);
|
||||
_appDebugLogService?.info(
|
||||
'Disconnect complete transport=$transportLabel manual=$manual',
|
||||
tag: 'Connection',
|
||||
);
|
||||
if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) {
|
||||
_scheduleReconnect();
|
||||
}
|
||||
@@ -1345,7 +1377,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
Future<void> refreshDeviceInfo() async {
|
||||
if (!isConnected) return;
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
_webInitialHandshakeRequestSent &&
|
||||
_selfPublicKey == null) {
|
||||
return;
|
||||
}
|
||||
_awaitingSelfInfo = true;
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
_selfPublicKey == null) {
|
||||
_webInitialHandshakeRequestSent = true;
|
||||
}
|
||||
await sendFrame(buildDeviceQueryFrame());
|
||||
await sendFrame(buildAppStartFrame());
|
||||
await requestBatteryStatus(force: true);
|
||||
@@ -1356,7 +1399,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
Future<void> _requestDeviceInfo() async {
|
||||
if (!isConnected || _awaitingSelfInfo) return;
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
_webInitialHandshakeRequestSent &&
|
||||
_selfPublicKey == null) {
|
||||
return;
|
||||
}
|
||||
_awaitingSelfInfo = true;
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
_selfPublicKey == null) {
|
||||
_webInitialHandshakeRequestSent = true;
|
||||
}
|
||||
await sendFrame(buildDeviceQueryFrame());
|
||||
await sendFrame(buildAppStartFrame());
|
||||
await sendFrame(buildGetCustomVarsFrame());
|
||||
@@ -2183,6 +2237,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_pendingQueueSync = false;
|
||||
unawaited(syncQueuedMessages(force: true));
|
||||
}
|
||||
if (_pendingDeferredChannelSyncAfterContacts &&
|
||||
(_activeTransport == MeshCoreTransportType.bluetooth ||
|
||||
_activeTransport == MeshCoreTransportType.usb)) {
|
||||
_pendingDeferredChannelSyncAfterContacts = false;
|
||||
unawaited(getChannels());
|
||||
}
|
||||
break;
|
||||
case respCodeContactMsgRecv:
|
||||
case respCodeContactMsgRecvV3:
|
||||
@@ -2294,6 +2354,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// [58+] = node_name
|
||||
if (frame.length < 4 + pubKeySize) return;
|
||||
|
||||
final wasAwaitingSelfInfo = _awaitingSelfInfo;
|
||||
|
||||
_currentTxPower = frame[2];
|
||||
_maxTxPower = frame[3];
|
||||
_selfPublicKey = Uint8List.fromList(frame.sublist(4, 4 + pubKeySize));
|
||||
@@ -2325,15 +2387,25 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_selfInfoRetryTimer = null;
|
||||
notifyListeners();
|
||||
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
!wasAwaitingSelfInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-fetch contacts after getting self info. On web BLE, defer this
|
||||
// until after channel 0 so startup writes stay serialized.
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth) {
|
||||
_pendingInitialContactsSync = true;
|
||||
} else if (_activeTransport == MeshCoreTransportType.usb) {
|
||||
_pendingDeferredChannelSyncAfterContacts = true;
|
||||
getContacts();
|
||||
} else {
|
||||
getContacts();
|
||||
}
|
||||
if (_shouldGateInitialChannelSync) {
|
||||
if (_shouldGateInitialChannelSync &&
|
||||
_activeTransport != MeshCoreTransportType.usb) {
|
||||
_maybeStartInitialChannelSync();
|
||||
}
|
||||
}
|
||||
@@ -2367,6 +2439,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
unawaited(loadChannelSettings(maxChannels: nextMaxChannels));
|
||||
unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels));
|
||||
if (isConnected &&
|
||||
_selfPublicKey != null &&
|
||||
(!_shouldGateInitialChannelSync || !_pendingInitialChannelSync)) {
|
||||
unawaited(getChannels(maxChannels: nextMaxChannels));
|
||||
}
|
||||
@@ -3524,17 +3597,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// For 1:1 chats, sender is implicit (null)
|
||||
String? senderName;
|
||||
if (isRoomServer && !msg.isOutgoing) {
|
||||
// Treat a missing room-contact key as unknown instead of matching every
|
||||
// contact via an empty prefix.
|
||||
if (msg.fourByteRoomContactKey.length == 4) {
|
||||
final senderContact = _contacts.cast<Contact?>().firstWhere(
|
||||
(c) =>
|
||||
c != null &&
|
||||
_matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
|
||||
orElse: () => null,
|
||||
);
|
||||
senderName = senderContact?.name;
|
||||
}
|
||||
final senderContact = _contacts.cast<Contact?>().firstWhere(
|
||||
(c) =>
|
||||
c != null &&
|
||||
_matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
|
||||
orElse: () => null,
|
||||
);
|
||||
senderName = senderContact?.name;
|
||||
} else if (isRoomServer && msg.isOutgoing) {
|
||||
senderName = selfName;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'meshcore_connector.dart';
|
||||
|
||||
class MeshCoreConnectorUsb {
|
||||
const MeshCoreConnectorUsb(this.connector);
|
||||
|
||||
final MeshCoreConnector connector;
|
||||
|
||||
MeshCoreConnectionState get state => connector.state;
|
||||
MeshCoreTransportType get activeTransport => connector.activeTransport;
|
||||
String? get activeUsbPortDisplayLabel => connector.activeUsbPortDisplayLabel;
|
||||
bool get isUsbTransportConnected => connector.isUsbTransportConnected;
|
||||
|
||||
void addListener(VoidCallback listener) => connector.addListener(listener);
|
||||
void removeListener(VoidCallback listener) =>
|
||||
connector.removeListener(listener);
|
||||
|
||||
Future<List<String>> listPorts() => connector.listUsbPorts();
|
||||
|
||||
void setRequestPortLabel(String label) {
|
||||
connector.setUsbRequestPortLabel(label);
|
||||
}
|
||||
|
||||
Future<void> connect({required String portName, int baudRate = 115200}) {
|
||||
return connector.connectUsb(portName: portName, baudRate: baudRate);
|
||||
}
|
||||
|
||||
Future<void> disconnect({bool manual = true}) {
|
||||
return connector.disconnect(manual: manual);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user