mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-25 03:42:55 +10:00
feat: Add TCP connection support and UI integration
- Implemented TCP transport service for native platforms. - Added TCP connection screen with input fields for host and port. - Integrated TCP connection options into the scanner and USB screens. - Updated localization files for new TCP-related strings. - Added tests for TCP connection flow and error handling. - Enhanced USB screen to include TCP connection option. - Improved layout to ensure no overflow in narrow widths for scanner and USB screens.
This commit is contained in:
@@ -22,6 +22,7 @@ import '../services/app_settings_service.dart';
|
||||
import '../services/background_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import 'meshcore_connector_usb.dart';
|
||||
import 'meshcore_connector_tcp.dart';
|
||||
import '../storage/channel_message_store.dart';
|
||||
import '../storage/channel_order_store.dart';
|
||||
import '../storage/channel_settings_store.dart';
|
||||
@@ -86,7 +87,7 @@ enum MeshCoreConnectionState {
|
||||
disconnecting,
|
||||
}
|
||||
|
||||
enum MeshCoreTransportType { bluetooth, usb }
|
||||
enum MeshCoreTransportType { bluetooth, usb, tcp }
|
||||
|
||||
class RepeaterBatterySnapshot {
|
||||
final int millivolts;
|
||||
@@ -116,6 +117,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
bool _manualDisconnect = false;
|
||||
final MeshCoreUsbManager _usbManager = MeshCoreUsbManager();
|
||||
StreamSubscription<Uint8List>? _usbFrameSubscription;
|
||||
final MeshCoreTcpManager _tcpManager = MeshCoreTcpManager();
|
||||
StreamSubscription<Uint8List>? _tcpFrameSubscription;
|
||||
MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
|
||||
|
||||
final List<ScanResult> _scanResults = [];
|
||||
@@ -255,6 +258,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
bool get isUsbTransportConnected =>
|
||||
_state == MeshCoreConnectionState.connected &&
|
||||
_activeTransport == MeshCoreTransportType.usb;
|
||||
String? get activeTcpEndpoint => _tcpManager.activeEndpoint;
|
||||
bool get isTcpTransportConnected =>
|
||||
_state == MeshCoreConnectionState.connected &&
|
||||
_activeTransport == MeshCoreTransportType.tcp;
|
||||
|
||||
String get deviceDisplayName {
|
||||
if (_selfName != null && _selfName!.isNotEmpty) {
|
||||
@@ -659,6 +666,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_appDebugLogService = appDebugLogService;
|
||||
_backgroundService = backgroundService;
|
||||
_usbManager.setDebugLogService(_appDebugLogService);
|
||||
_tcpManager.setDebugLogService(_appDebugLogService);
|
||||
|
||||
// Initialize notification service
|
||||
_notificationService.initialize();
|
||||
@@ -964,6 +972,70 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connectTcp({required String host, required int port}) async {
|
||||
if (_state == MeshCoreConnectionState.connecting ||
|
||||
_state == MeshCoreConnectionState.connected) {
|
||||
_appDebugLogService?.warn(
|
||||
'connectTcp ignored: already $_state',
|
||||
tag: 'TCP',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_appDebugLogService?.info('connectTcp: endpoint=$host:$port', tag: 'TCP');
|
||||
|
||||
await stopScan();
|
||||
_cancelReconnectTimer();
|
||||
_manualDisconnect = false;
|
||||
_resetConnectionHandshakeState();
|
||||
_activeTransport = MeshCoreTransportType.tcp;
|
||||
_setState(MeshCoreConnectionState.connecting);
|
||||
|
||||
try {
|
||||
await _tcpFrameSubscription?.cancel();
|
||||
_tcpFrameSubscription = null;
|
||||
await _tcpManager.connect(host: host, port: port);
|
||||
notifyListeners();
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
_tcpFrameSubscription = _tcpManager.frameStream.listen(
|
||||
_handleFrame,
|
||||
onError: (error, stackTrace) {
|
||||
_appDebugLogService?.error('TCP transport error: $error', tag: 'TCP');
|
||||
unawaited(disconnect(manual: false));
|
||||
},
|
||||
onDone: () {
|
||||
_appDebugLogService?.warn('TCP frame stream ended', tag: 'TCP');
|
||||
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 TCP connect');
|
||||
}
|
||||
|
||||
await syncTime();
|
||||
} catch (error) {
|
||||
_appDebugLogService?.error('TCP connection error: $error', tag: 'TCP');
|
||||
await disconnect(manual: false);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect(BluetoothDevice device, {String? displayName}) async {
|
||||
if (_state == MeshCoreConnectionState.connecting ||
|
||||
_state == MeshCoreConnectionState.connected) {
|
||||
@@ -1230,6 +1302,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
bool get _shouldGateInitialChannelSync =>
|
||||
_activeTransport == MeshCoreTransportType.usb ||
|
||||
_activeTransport == MeshCoreTransportType.tcp ||
|
||||
(_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
PlatformInfo.isWeb);
|
||||
|
||||
@@ -1276,9 +1349,11 @@ 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';
|
||||
final transportLabel = switch (transportAtDisconnect) {
|
||||
MeshCoreTransportType.bluetooth => 'BLE',
|
||||
MeshCoreTransportType.usb => 'USB',
|
||||
MeshCoreTransportType.tcp => 'TCP',
|
||||
};
|
||||
|
||||
_appDebugLogService?.info(
|
||||
'Starting disconnect transport=$transportLabel manual=$manual',
|
||||
@@ -1298,6 +1373,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
await _usbFrameSubscription?.cancel();
|
||||
_usbFrameSubscription = null;
|
||||
await _usbManager.disconnect();
|
||||
await _tcpFrameSubscription?.cancel();
|
||||
_tcpFrameSubscription = null;
|
||||
await _tcpManager.disconnect();
|
||||
|
||||
await _notifySubscription?.cancel();
|
||||
_notifySubscription = null;
|
||||
@@ -1379,6 +1457,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
if (_activeTransport == MeshCoreTransportType.usb) {
|
||||
await _usbManager.write(data);
|
||||
} else if (_activeTransport == MeshCoreTransportType.tcp) {
|
||||
await _tcpManager.write(data);
|
||||
} else {
|
||||
if (_rxCharacteristic == null) {
|
||||
throw Exception("MeshCore RX characteristic not available");
|
||||
@@ -2338,7 +2418,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
if (_pendingDeferredChannelSyncAfterContacts &&
|
||||
(_activeTransport == MeshCoreTransportType.bluetooth ||
|
||||
_activeTransport == MeshCoreTransportType.usb)) {
|
||||
_activeTransport == MeshCoreTransportType.usb ||
|
||||
_activeTransport == MeshCoreTransportType.tcp)) {
|
||||
_pendingDeferredChannelSyncAfterContacts = false;
|
||||
_pendingInitialChannelSync = false;
|
||||
unawaited(getChannels());
|
||||
@@ -2505,14 +2586,16 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth) {
|
||||
_pendingInitialContactsSync = true;
|
||||
} else if (_activeTransport == MeshCoreTransportType.usb) {
|
||||
} else if (_activeTransport == MeshCoreTransportType.usb ||
|
||||
_activeTransport == MeshCoreTransportType.tcp) {
|
||||
_pendingDeferredChannelSyncAfterContacts = true;
|
||||
getContacts();
|
||||
} else {
|
||||
getContacts();
|
||||
}
|
||||
if (_shouldGateInitialChannelSync &&
|
||||
_activeTransport != MeshCoreTransportType.usb) {
|
||||
_activeTransport != MeshCoreTransportType.usb &&
|
||||
_activeTransport != MeshCoreTransportType.tcp) {
|
||||
_maybeStartInitialChannelSync();
|
||||
}
|
||||
}
|
||||
@@ -4274,12 +4357,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_scanSubscription?.cancel();
|
||||
_connectionSubscription?.cancel();
|
||||
_usbFrameSubscription?.cancel();
|
||||
_tcpFrameSubscription?.cancel();
|
||||
_notifySubscription?.cancel();
|
||||
_notifyListenersTimer?.cancel();
|
||||
_reconnectTimer?.cancel();
|
||||
_batteryPollTimer?.cancel();
|
||||
_receivedFramesController.close();
|
||||
_usbManager.dispose();
|
||||
_tcpManager.dispose();
|
||||
|
||||
// Flush pending unread writes before disposal
|
||||
_unreadStore.flush();
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../services/tcp_transport_service.dart';
|
||||
|
||||
class MeshCoreTcpManager {
|
||||
final TcpTransportService _service = TcpTransportService();
|
||||
AppDebugLogService? _debugLog;
|
||||
|
||||
String? get activeEndpoint => _service.activeEndpoint;
|
||||
bool get isConnected => _service.isConnected;
|
||||
Stream<Uint8List> get frameStream => _service.frameStream;
|
||||
|
||||
void setDebugLogService(AppDebugLogService? service) {
|
||||
_debugLog = service;
|
||||
_service.setDebugLogService(service);
|
||||
}
|
||||
|
||||
Future<void> connect({required String host, required int port}) async {
|
||||
_debugLog?.info('TcpManager.connect endpoint=$host:$port', tag: 'TCP');
|
||||
await _service.connect(host: host, port: port);
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
if (!_service.isConnected && _service.activeEndpoint == null) {
|
||||
return;
|
||||
}
|
||||
_debugLog?.info('TcpManager.disconnect', tag: 'TCP');
|
||||
await _service.disconnect();
|
||||
}
|
||||
|
||||
Future<void> write(Uint8List data) => _service.write(data);
|
||||
|
||||
void dispose() {
|
||||
_service.dispose();
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,9 @@ class MeshCoreUsbManager {
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
if (!_service.isConnected && _activePortKey == null) {
|
||||
return;
|
||||
}
|
||||
_debugLog?.info('UsbManager.disconnect', tag: 'USB');
|
||||
await _service.disconnect();
|
||||
_activePortKey = null;
|
||||
|
||||
Reference in New Issue
Block a user