refactor(tcp): promote MeshCoreTcpConnector, fix translations, harden UI

- Replace thin MeshCoreTcpManager facade with a proper MeshCoreTcpConnector
  that owns TcpTransportService and the frame subscription, mirroring
  MeshCoreUsbManager. The connector no longer holds a raw TcpTransportService
  or a _tcpFrameSubscription field.
- Remove hardcoded default host IP from TcpScreen (keep port 5000 hint).
- Disable connect button during scanning state, not just connecting state.
- Fix tcpPortLabel mistranslated as nautical "port/harbor" in de, it, pt,
  nl, sv, sk, sl, zh; fix corrupted Slovak tcpPortHint ("5 000" → "5000").
- Remove unused tcpStatus_connecting string from all 15 locale arb files
  and all generated app_localizations_*.dart files.
- Add extendedPadding to TCP screen FABs to match USB screen.
- Add Key to connect button; update tests to use byKey and assert
  onPressed == null when button is disabled during scanning.
This commit is contained in:
Zach
2026-03-13 10:58:52 -07:00
parent 1ad5db27ca
commit db935a7454
35 changed files with 91 additions and 123 deletions
+17 -22
View File
@@ -116,8 +116,7 @@ class MeshCoreConnector extends ChangeNotifier {
bool _manualDisconnect = false;
final MeshCoreUsbManager _usbManager = MeshCoreUsbManager();
StreamSubscription<Uint8List>? _usbFrameSubscription;
final MeshCoreTcpManager _tcpManager = MeshCoreTcpManager();
StreamSubscription<Uint8List>? _tcpFrameSubscription;
final MeshCoreTcpConnector _tcpConnector = MeshCoreTcpConnector();
MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
final List<ScanResult> _scanResults = [];
@@ -257,7 +256,7 @@ class MeshCoreConnector extends ChangeNotifier {
bool get isUsbTransportConnected =>
_state == MeshCoreConnectionState.connected &&
_activeTransport == MeshCoreTransportType.usb;
String? get activeTcpEndpoint => _tcpManager.activeEndpoint;
String? get activeTcpEndpoint => _tcpConnector.activeEndpoint;
bool get isTcpTransportConnected =>
_state == MeshCoreConnectionState.connected &&
_activeTransport == MeshCoreTransportType.tcp;
@@ -666,7 +665,7 @@ class MeshCoreConnector extends ChangeNotifier {
_appDebugLogService = appDebugLogService;
_backgroundService = backgroundService;
_usbManager.setDebugLogService(_appDebugLogService);
_tcpManager.setDebugLogService(_appDebugLogService);
_tcpConnector.setDebugLogService(_appDebugLogService);
// Initialize notification service
_notificationService.initialize();
@@ -1002,22 +1001,21 @@ class MeshCoreConnector extends ChangeNotifier {
await disconnect(manual: false);
return;
}
if (_tcpManager.isConnected) {
await _tcpManager.disconnect();
if (_tcpConnector.isConnected) {
await _tcpConnector.disconnect();
}
}
await _tcpFrameSubscription?.cancel();
_tcpFrameSubscription = null;
await _tcpManager.connect(host: host, port: port);
await _tcpConnector.cancelFrameSubscription();
await _tcpConnector.connect(host: host, port: port);
final isTcpConnectCancelled =
_activeTransport != MeshCoreTransportType.tcp ||
_state != MeshCoreConnectionState.connecting ||
!_tcpManager.isConnected;
!_tcpConnector.isConnected;
if (isTcpConnectCancelled) {
await handleTcpConnectAbort(
message:
'connectTcp aborted before handshake: state=$_state transport=$_activeTransport connected=${_tcpManager.isConnected}',
'connectTcp aborted before handshake: state=$_state transport=$_activeTransport connected=${_tcpConnector.isConnected}',
);
return;
}
@@ -1027,16 +1025,16 @@ class MeshCoreConnector extends ChangeNotifier {
final isTcpConnectCancelledAfterDelay =
_activeTransport != MeshCoreTransportType.tcp ||
_state != MeshCoreConnectionState.connecting ||
!_tcpManager.isConnected;
!_tcpConnector.isConnected;
if (isTcpConnectCancelledAfterDelay) {
await handleTcpConnectAbort(
message:
'connectTcp aborted after connect delay: state=$_state transport=$_activeTransport connected=${_tcpManager.isConnected}',
'connectTcp aborted after connect delay: state=$_state transport=$_activeTransport connected=${_tcpConnector.isConnected}',
);
return;
}
_tcpFrameSubscription = _tcpManager.frameStream.listen(
_handleFrame,
_tcpConnector.listenFrames(
onFrame: _handleFrame,
onError: (error, stackTrace) {
_appDebugLogService?.error('TCP transport error: $error', tag: 'TCP');
unawaited(disconnect(manual: false));
@@ -1073,7 +1071,7 @@ class MeshCoreConnector extends ChangeNotifier {
manualDisconnect: _manualDisconnect,
state: _state,
activeTransport: _activeTransport,
tcpManagerConnected: _tcpManager.isConnected,
tcpManagerConnected: _tcpConnector.isConnected,
);
if (tcpConnectCancelledBeforeHandshake) {
_appDebugLogService?.info(
@@ -1445,9 +1443,7 @@ class MeshCoreConnector extends ChangeNotifier {
await _usbFrameSubscription?.cancel();
_usbFrameSubscription = null;
await _usbManager.disconnect();
await _tcpFrameSubscription?.cancel();
_tcpFrameSubscription = null;
await _tcpManager.disconnect();
await _tcpConnector.disconnect();
await _notifySubscription?.cancel();
_notifySubscription = null;
@@ -1530,7 +1526,7 @@ class MeshCoreConnector extends ChangeNotifier {
if (_activeTransport == MeshCoreTransportType.usb) {
await _usbManager.write(data);
} else if (_activeTransport == MeshCoreTransportType.tcp) {
await _tcpManager.write(data);
await _tcpConnector.write(data);
} else {
if (_rxCharacteristic == null) {
throw Exception("MeshCore RX characteristic not available");
@@ -4484,14 +4480,13 @@ 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();
_tcpConnector.dispose();
// Flush pending unread writes before disposal
_unreadStore.flush();
+40 -4
View File
@@ -1,34 +1,70 @@
import 'dart:async';
import 'dart:typed_data';
import '../services/app_debug_log_service.dart';
import '../services/tcp_transport_service.dart';
class MeshCoreTcpManager {
/// Manages TCP transport for MeshCore devices.
///
/// Owns the [TcpTransportService] and TCP-specific connection state.
/// The main [MeshCoreConnector] delegates all TCP operations here.
class MeshCoreTcpConnector {
final TcpTransportService _service = TcpTransportService();
AppDebugLogService? _debugLog;
StreamSubscription<Uint8List>? _frameSubscription;
// --- Getters ---
String? get activeEndpoint => _service.activeEndpoint;
bool get isConnected => _service.isConnected;
Stream<Uint8List> get frameStream => _service.frameStream;
// --- Configuration ---
void setDebugLogService(AppDebugLogService? service) {
_debugLog = service;
_service.setDebugLogService(service);
}
// --- Connection lifecycle ---
Future<void> connect({required String host, required int port}) async {
_debugLog?.info('TcpManager.connect endpoint=$host:$port', tag: 'TCP');
_debugLog?.info('TcpConnector.connect endpoint=$host:$port', tag: 'TCP');
await _frameSubscription?.cancel();
_frameSubscription = null;
await _service.connect(host: host, port: port);
_debugLog?.info(
'TcpConnector.connect done, endpoint=${_service.activeEndpoint}',
tag: 'TCP',
);
}
StreamSubscription<Uint8List> listenFrames({
required void Function(Uint8List) onFrame,
required void Function(Object, StackTrace?) onError,
required void Function() onDone,
}) {
_frameSubscription = _service.frameStream.listen(
onFrame,
onError: onError,
onDone: onDone,
);
return _frameSubscription!;
}
Future<void> cancelFrameSubscription() async {
await _frameSubscription?.cancel();
_frameSubscription = null;
}
Future<void> disconnect() async {
_debugLog?.info('TcpManager.disconnect', tag: 'TCP');
if (!_service.isConnected && _frameSubscription == null) return;
_debugLog?.info('TcpConnector.disconnect', tag: 'TCP');
await _frameSubscription?.cancel();
_frameSubscription = null;
await _service.disconnect();
}
Future<void> write(Uint8List data) => _service.write(data);
void dispose() {
_frameSubscription?.cancel();
_service.dispose();
}
}