Compare commits

..

3 Commits

Author SHA1 Message Date
Winston Lowe f870e77e98 Merge branch 'main' into dev-dbDevicePrefix 2026-03-12 09:37:44 -07:00
Winston Lowe e6658a6026 Migrate storage keys to scoped keys across multiple store classes 2026-03-12 09:29:46 -07:00
Winston Lowe 6da54e13c3 Migrate legacy storage keys to scoped keys in various store classes 2026-03-12 08:29:56 -07:00
70 changed files with 446 additions and 3429 deletions
-1
View File
@@ -1 +0,0 @@
6.2.4
+42 -271
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:crypto/crypto.dart' as crypto; import 'package:crypto/crypto.dart' as crypto;
import 'package:meshcore_open/models/discovery_contact.dart';
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart';
@@ -21,7 +22,6 @@ import '../services/app_settings_service.dart';
import '../services/background_service.dart'; import '../services/background_service.dart';
import '../services/notification_service.dart'; import '../services/notification_service.dart';
import 'meshcore_connector_usb.dart'; import 'meshcore_connector_usb.dart';
import 'meshcore_connector_tcp.dart';
import '../storage/channel_message_store.dart'; import '../storage/channel_message_store.dart';
import '../storage/channel_order_store.dart'; import '../storage/channel_order_store.dart';
import '../storage/channel_settings_store.dart'; import '../storage/channel_settings_store.dart';
@@ -86,7 +86,7 @@ enum MeshCoreConnectionState {
disconnecting, disconnecting,
} }
enum MeshCoreTransportType { bluetooth, usb, tcp } enum MeshCoreTransportType { bluetooth, usb }
class RepeaterBatterySnapshot { class RepeaterBatterySnapshot {
final int millivolts; final int millivolts;
@@ -116,12 +116,11 @@ class MeshCoreConnector extends ChangeNotifier {
bool _manualDisconnect = false; bool _manualDisconnect = false;
final MeshCoreUsbManager _usbManager = MeshCoreUsbManager(); final MeshCoreUsbManager _usbManager = MeshCoreUsbManager();
StreamSubscription<Uint8List>? _usbFrameSubscription; StreamSubscription<Uint8List>? _usbFrameSubscription;
final MeshCoreTcpConnector _tcpConnector = MeshCoreTcpConnector();
MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth; MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
final List<ScanResult> _scanResults = []; final List<ScanResult> _scanResults = [];
final List<Contact> _contacts = []; final List<Contact> _contacts = [];
final List<Contact> _discoveredContacts = []; final List<DiscoveryContact> _discoveredContacts = [];
final List<Channel> _channels = []; final List<Channel> _channels = [];
final Map<String, List<Message>> _conversations = {}; final Map<String, List<Message>> _conversations = {};
final Map<int, List<ChannelMessage>> _channelMessages = {}; final Map<int, List<ChannelMessage>> _channelMessages = {};
@@ -199,9 +198,6 @@ class MeshCoreConnector extends ChangeNotifier {
int _queueSyncRetries = 0; int _queueSyncRetries = 0;
static const int _maxQueueSyncRetries = 3; static const int _maxQueueSyncRetries = 3;
static const int _queueSyncTimeoutMs = 5000; // 5 second timeout static const int _queueSyncTimeoutMs = 5000; // 5 second timeout
// Serializes path operations (setContactPath/clearContactPath) to prevent
// interleaved async calls from leaving in-memory state inconsistent with device.
Future<void> _pathOpLock = Future.value();
Map<String, String>? _currentCustomVars; Map<String, String>? _currentCustomVars;
// Channel syncing state (sequential pattern) // Channel syncing state (sequential pattern)
@@ -259,10 +255,6 @@ class MeshCoreConnector extends ChangeNotifier {
bool get isUsbTransportConnected => bool get isUsbTransportConnected =>
_state == MeshCoreConnectionState.connected && _state == MeshCoreConnectionState.connected &&
_activeTransport == MeshCoreTransportType.usb; _activeTransport == MeshCoreTransportType.usb;
String? get activeTcpEndpoint => _tcpConnector.activeEndpoint;
bool get isTcpTransportConnected =>
_state == MeshCoreConnectionState.connected &&
_activeTransport == MeshCoreTransportType.tcp;
String get deviceDisplayName { String get deviceDisplayName {
if (_selfName != null && _selfName!.isNotEmpty) { if (_selfName != null && _selfName!.isNotEmpty) {
@@ -289,7 +281,7 @@ class MeshCoreConnector extends ChangeNotifier {
); );
} }
List<Contact> get discoveredContacts { List<DiscoveryContact> get discoveredContacts {
return List.unmodifiable(_discoveredContacts); return List.unmodifiable(_discoveredContacts);
} }
@@ -561,10 +553,6 @@ class MeshCoreConnector extends ChangeNotifier {
_unreadStore.saveContactUnreadCount( _unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount), Map<String, int>.from(_contactUnreadCount),
); );
_notificationService.clearContactNotification(
contactKeyHex,
getTotalUnreadCount(),
);
notifyListeners(); notifyListeners();
} }
} }
@@ -583,10 +571,6 @@ class MeshCoreConnector extends ChangeNotifier {
_channels.isNotEmpty ? _channels : _cachedChannels, _channels.isNotEmpty ? _channels : _cachedChannels,
), ),
); );
_notificationService.clearChannelNotification(
channelIndex,
getTotalUnreadCount(),
);
notifyListeners(); notifyListeners();
} }
} }
@@ -676,11 +660,11 @@ class MeshCoreConnector extends ChangeNotifier {
_appDebugLogService = appDebugLogService; _appDebugLogService = appDebugLogService;
_backgroundService = backgroundService; _backgroundService = backgroundService;
_usbManager.setDebugLogService(_appDebugLogService); _usbManager.setDebugLogService(_appDebugLogService);
_tcpConnector.setDebugLogService(_appDebugLogService);
// Initialize notification service // Initialize notification service
_notificationService.initialize(); _notificationService.initialize();
_loadChannelOrder(); _loadChannelOrder();
_loadDiscoveredContactCache();
// Initialize retry service callbacks // Initialize retry service callbacks
_retryService?.initialize( _retryService?.initialize(
@@ -982,142 +966,6 @@ 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 {
Future<void> handleTcpConnectAbort({required String message}) async {
_appDebugLogService?.warn(message, tag: 'TCP');
final shouldResetState = shouldResetStateAfterTcpConnectAbort(
state: _state,
activeTransport: _activeTransport,
);
if (shouldResetState) {
await disconnect(manual: false);
return;
}
if (_tcpConnector.isConnected) {
await _tcpConnector.disconnect();
}
}
await _tcpConnector.cancelFrameSubscription();
await _tcpConnector.connect(host: host, port: port);
final isTcpConnectCancelled =
_activeTransport != MeshCoreTransportType.tcp ||
_state != MeshCoreConnectionState.connecting ||
!_tcpConnector.isConnected;
if (isTcpConnectCancelled) {
await handleTcpConnectAbort(
message:
'connectTcp aborted before handshake: state=$_state transport=$_activeTransport connected=${_tcpConnector.isConnected}',
);
return;
}
notifyListeners();
await Future<void>.delayed(const Duration(milliseconds: 200));
final isTcpConnectCancelledAfterDelay =
_activeTransport != MeshCoreTransportType.tcp ||
_state != MeshCoreConnectionState.connecting ||
!_tcpConnector.isConnected;
if (isTcpConnectCancelledAfterDelay) {
await handleTcpConnectAbort(
message:
'connectTcp aborted after connect delay: state=$_state transport=$_activeTransport connected=${_tcpConnector.isConnected}',
);
return;
}
_tcpConnector.listenFrames(
onFrame: _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');
final tcpConnectCancelledBeforeHandshake =
shouldIgnoreLateTcpConnectError(
manualDisconnect: _manualDisconnect,
state: _state,
activeTransport: _activeTransport,
tcpManagerConnected: _tcpConnector.isConnected,
);
if (tcpConnectCancelledBeforeHandshake) {
_appDebugLogService?.info(
'Ignoring late TCP connect error after cancellation/switch: state=$_state transport=$_activeTransport',
tag: 'TCP',
);
return;
}
await disconnect(manual: false);
rethrow;
}
}
@visibleForTesting
static bool shouldIgnoreLateTcpConnectError({
required bool manualDisconnect,
required MeshCoreConnectionState state,
required MeshCoreTransportType activeTransport,
required bool tcpManagerConnected,
}) {
return manualDisconnect &&
(state == MeshCoreConnectionState.disconnected ||
state == MeshCoreConnectionState.disconnecting) &&
(activeTransport != MeshCoreTransportType.tcp || !tcpManagerConnected);
}
@visibleForTesting
static bool shouldResetStateAfterTcpConnectAbort({
required MeshCoreConnectionState state,
required MeshCoreTransportType activeTransport,
}) {
return state == MeshCoreConnectionState.connecting &&
activeTransport == MeshCoreTransportType.tcp;
}
Future<void> connect(BluetoothDevice device, {String? displayName}) async { Future<void> connect(BluetoothDevice device, {String? displayName}) async {
if (_state == MeshCoreConnectionState.connecting || if (_state == MeshCoreConnectionState.connecting ||
_state == MeshCoreConnectionState.connected) { _state == MeshCoreConnectionState.connected) {
@@ -1383,7 +1231,6 @@ class MeshCoreConnector extends ChangeNotifier {
bool get _shouldGateInitialChannelSync => bool get _shouldGateInitialChannelSync =>
_activeTransport == MeshCoreTransportType.usb || _activeTransport == MeshCoreTransportType.usb ||
_activeTransport == MeshCoreTransportType.tcp ||
(_activeTransport == MeshCoreTransportType.bluetooth && (_activeTransport == MeshCoreTransportType.bluetooth &&
PlatformInfo.isWeb); PlatformInfo.isWeb);
@@ -1430,11 +1277,9 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> disconnect({bool manual = true}) async { Future<void> disconnect({bool manual = true}) async {
if (_state == MeshCoreConnectionState.disconnecting) return; if (_state == MeshCoreConnectionState.disconnecting) return;
final transportAtDisconnect = _activeTransport; final transportAtDisconnect = _activeTransport;
final transportLabel = switch (transportAtDisconnect) { final transportLabel = transportAtDisconnect == MeshCoreTransportType.usb
MeshCoreTransportType.bluetooth => 'BLE', ? 'USB'
MeshCoreTransportType.usb => 'USB', : 'BLE';
MeshCoreTransportType.tcp => 'TCP',
};
_appDebugLogService?.info( _appDebugLogService?.info(
'Starting disconnect transport=$transportLabel manual=$manual', 'Starting disconnect transport=$transportLabel manual=$manual',
@@ -1454,7 +1299,6 @@ class MeshCoreConnector extends ChangeNotifier {
await _usbFrameSubscription?.cancel(); await _usbFrameSubscription?.cancel();
_usbFrameSubscription = null; _usbFrameSubscription = null;
await _usbManager.disconnect(); await _usbManager.disconnect();
await _tcpConnector.disconnect();
await _notifySubscription?.cancel(); await _notifySubscription?.cancel();
_notifySubscription = null; _notifySubscription = null;
@@ -1536,8 +1380,6 @@ class MeshCoreConnector extends ChangeNotifier {
if (_activeTransport == MeshCoreTransportType.usb) { if (_activeTransport == MeshCoreTransportType.usb) {
await _usbManager.write(data); await _usbManager.write(data);
} else if (_activeTransport == MeshCoreTransportType.tcp) {
await _tcpConnector.write(data);
} else { } else {
if (_rxCharacteristic == null) { if (_rxCharacteristic == null) {
throw Exception("MeshCore RX characteristic not available"); throw Exception("MeshCore RX characteristic not available");
@@ -1751,33 +1593,18 @@ class MeshCoreConnector extends ChangeNotifier {
Uint8List customPath, Uint8List customPath,
int pathLen, int pathLen,
) async { ) async {
// Serialize path operations to prevent interleaved async calls from if (!isConnected) return;
// leaving in-memory state inconsistent with the device.
final prev = _pathOpLock;
final completer = Completer<void>();
_pathOpLock = completer.future;
await prev;
try {
if (!isConnected) return;
await sendFrame( await sendFrame(
buildUpdateContactPathFrame( buildUpdateContactPathFrame(
contact.publicKey, contact.publicKey,
customPath, customPath,
pathLen, pathLen,
type: contact.type, type: contact.type,
flags: contact.flags, flags: contact.flags,
name: contact.name, name: contact.name,
), ),
); );
// USB writes return instantly (no BLE flow control), so give the firmware
// time to persist the path change before subsequent commands.
if (_activeTransport == MeshCoreTransportType.usb) {
await Future<void>.delayed(const Duration(milliseconds: 100));
}
} finally {
completer.complete();
}
} }
Future<void> setContactFavorite(Contact contact, bool isFavorite) async { Future<void> setContactFavorite(Contact contact, bool isFavorite) async {
@@ -2077,11 +1904,7 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> removeContact(Contact contact) async { Future<void> removeContact(Contact contact) async {
if (!isConnected) return; if (!isConnected) return;
_handleDiscovery( _handleDiscovery(contact, Uint8List(0), noNotify: true);
contact,
contact.rawPacket ?? Uint8List(0),
noNotify: true,
);
await sendFrame(buildRemoveContactFrame(contact.publicKey)); await sendFrame(buildRemoveContactFrame(contact.publicKey));
_contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex); _contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex);
@@ -2097,20 +1920,7 @@ class MeshCoreConnector extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> updateKnownDiscovered() async { Future<void> removeDiscoveredContact(DiscoveryContact contact) async {
if (!isConnected) return;
for (int i = 0; i < _discoveredContacts.length; i++) {
_discoveredContacts[i] = _discoveredContacts[i].copyWith(
isActive: _knownContactKeys.contains(
_discoveredContacts[i].publicKeyHex,
),
);
}
unawaited(_persistDiscoveredContacts());
notifyListeners();
}
Future<void> removeDiscoveredContact(Contact contact) async {
if (!isConnected) return; if (!isConnected) return;
_discoveredContacts.removeWhere( _discoveredContacts.removeWhere(
(c) => c.publicKeyHex == contact.publicKeyHex, (c) => c.publicKeyHex == contact.publicKeyHex,
@@ -2119,7 +1929,7 @@ class MeshCoreConnector extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> importDiscoveredContact(Contact contact) async { Future<void> importDiscoveredContact(DiscoveryContact contact) async {
if (!isConnected) return; if (!isConnected) return;
await sendFrame( await sendFrame(
@@ -2128,23 +1938,11 @@ class MeshCoreConnector extends ChangeNotifier {
contact.path, contact.path,
contact.pathLength, contact.pathLength,
type: contact.type, type: contact.type,
flags: contact.flags, flags: 0,
name: contact.name, name: contact.name,
lat: contact.latitude,
lon: contact.longitude,
lastModified: contact.lastSeen,
), ),
); );
// Update the discovered contact to mark it as active (imported)
final discoveredIndex = _discoveredContacts.indexWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
);
if (discoveredIndex >= 0) {
_discoveredContacts[discoveredIndex] =
_discoveredContacts[discoveredIndex].copyWith(isActive: true);
}
_handleContactAdvert( _handleContactAdvert(
Contact( Contact(
publicKey: contact.publicKey, publicKey: contact.publicKey,
@@ -2155,41 +1953,29 @@ class MeshCoreConnector extends ChangeNotifier {
latitude: contact.latitude, latitude: contact.latitude,
longitude: contact.longitude, longitude: contact.longitude,
lastSeen: DateTime.now(), lastSeen: DateTime.now(),
flags: contact.flags,
), ),
); );
notifyListeners(); notifyListeners();
} }
Future<void> clearContactPath(Contact contact) async { Future<void> clearContactPath(Contact contact) async {
// Serialize path operations to prevent interleaved async calls. if (!isConnected) return;
final prev = _pathOpLock;
final completer = Completer<void>();
_pathOpLock = completer.future;
await prev;
try {
if (!isConnected) return;
await sendFrame(buildResetPathFrame(contact.publicKey)); await sendFrame(buildResetPathFrame(contact.publicKey));
if (_activeTransport == MeshCoreTransportType.usb) { final existingIndex = _contacts.indexWhere(
await Future<void>.delayed(const Duration(milliseconds: 100)); (c) => c.publicKeyHex == contact.publicKeyHex,
} );
final existingIndex = _contacts.indexWhere( if (existingIndex >= 0) {
(c) => c.publicKeyHex == contact.publicKeyHex, final existing = _contacts[existingIndex];
// Use copyWith to preserve pathOverride and pathOverrideBytes
_contacts[existingIndex] = existing.copyWith(
pathLength: -1,
path: Uint8List(0),
); );
if (existingIndex >= 0) { notifyListeners();
final existing = _contacts[existingIndex]; unawaited(_persistContacts());
// Preserve pathOverride and pathOverrideBytes — only reset device path
_contacts[existingIndex] = existing.copyWith(
pathLength: -1,
path: Uint8List(0),
);
notifyListeners();
unawaited(_persistContacts());
}
} finally {
completer.complete();
} }
// The device will send updated contact info with path_len = -1
} }
void updateContactInMemory( void updateContactInMemory(
@@ -2525,9 +2311,6 @@ class MeshCoreConnector extends ChangeNotifier {
_isLoadingContacts = true; _isLoadingContacts = true;
notifyListeners(); notifyListeners();
break; break;
case pushCodeAdvert:
// Known contact was seen again - just a pub key, no action needed
break;
case pushCodeNewAdvert: case pushCodeNewAdvert:
debugPrint('Got New CONTACT'); debugPrint('Got New CONTACT');
// It's the same format as respCodeContact, so we can reuse the handler // It's the same format as respCodeContact, so we can reuse the handler
@@ -2541,7 +2324,6 @@ class MeshCoreConnector extends ChangeNotifier {
debugPrint('Got END_OF_CONTACTS'); debugPrint('Got END_OF_CONTACTS');
_isLoadingContacts = false; _isLoadingContacts = false;
_preserveContactsOnRefresh = false; _preserveContactsOnRefresh = false;
unawaited(updateKnownDiscovered());
notifyListeners(); notifyListeners();
unawaited(_persistContacts()); unawaited(_persistContacts());
if (PlatformInfo.isWeb && if (PlatformInfo.isWeb &&
@@ -2557,8 +2339,7 @@ class MeshCoreConnector extends ChangeNotifier {
} }
if (_pendingDeferredChannelSyncAfterContacts && if (_pendingDeferredChannelSyncAfterContacts &&
(_activeTransport == MeshCoreTransportType.bluetooth || (_activeTransport == MeshCoreTransportType.bluetooth ||
_activeTransport == MeshCoreTransportType.usb || _activeTransport == MeshCoreTransportType.usb)) {
_activeTransport == MeshCoreTransportType.tcp)) {
_pendingDeferredChannelSyncAfterContacts = false; _pendingDeferredChannelSyncAfterContacts = false;
_pendingInitialChannelSync = false; _pendingInitialChannelSync = false;
unawaited(getChannels()); unawaited(getChannels());
@@ -2729,7 +2510,6 @@ class MeshCoreConnector extends ChangeNotifier {
// Load persisted channel messages // Load persisted channel messages
loadAllChannelMessages(); loadAllChannelMessages();
loadUnreadState(); loadUnreadState();
_loadDiscoveredContactCache();
_awaitingSelfInfo = false; _awaitingSelfInfo = false;
_selfInfoRetryTimer?.cancel(); _selfInfoRetryTimer?.cancel();
@@ -2747,16 +2527,14 @@ class MeshCoreConnector extends ChangeNotifier {
if (PlatformInfo.isWeb && if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth) { _activeTransport == MeshCoreTransportType.bluetooth) {
_pendingInitialContactsSync = true; _pendingInitialContactsSync = true;
} else if (_activeTransport == MeshCoreTransportType.usb || } else if (_activeTransport == MeshCoreTransportType.usb) {
_activeTransport == MeshCoreTransportType.tcp) {
_pendingDeferredChannelSyncAfterContacts = true; _pendingDeferredChannelSyncAfterContacts = true;
getContacts(); getContacts();
} else { } else {
getContacts(); getContacts();
} }
if (_shouldGateInitialChannelSync && if (_shouldGateInitialChannelSync &&
_activeTransport != MeshCoreTransportType.usb && _activeTransport != MeshCoreTransportType.usb) {
_activeTransport != MeshCoreTransportType.tcp) {
_maybeStartInitialChannelSync(); _maybeStartInitialChannelSync();
} }
} }
@@ -4524,7 +4302,6 @@ class MeshCoreConnector extends ChangeNotifier {
_batteryPollTimer?.cancel(); _batteryPollTimer?.cancel();
_receivedFramesController.close(); _receivedFramesController.close();
_usbManager.dispose(); _usbManager.dispose();
_tcpConnector.dispose();
// Flush pending unread writes before disposal // Flush pending unread writes before disposal
_unreadStore.flush(); _unreadStore.flush();
@@ -4629,7 +4406,7 @@ class MeshCoreConnector extends ChangeNotifier {
} }
importDiscoveredContact( importDiscoveredContact(
Contact( DiscoveryContact(
rawPacket: frame, rawPacket: frame,
publicKey: publicKey, publicKey: publicKey,
name: name, name: name,
@@ -4700,7 +4477,6 @@ class MeshCoreConnector extends ChangeNotifier {
if (isNewContact) { if (isNewContact) {
final newContact = Contact( final newContact = Contact(
rawPacket: rawPacket,
publicKey: publicKey, publicKey: publicKey,
name: name, name: name,
type: type, type: type,
@@ -4846,15 +4622,13 @@ class MeshCoreConnector extends ChangeNotifier {
latitude: contact.latitude, latitude: contact.latitude,
longitude: contact.longitude, longitude: contact.longitude,
lastSeen: contact.lastSeen, lastSeen: contact.lastSeen,
flags: 0,
isActive: false,
); );
notifyListeners(); notifyListeners();
unawaited(_persistDiscoveredContacts()); unawaited(_persistDiscoveredContacts());
return; return;
} }
final disContact = Contact( final disContact = DiscoveryContact(
rawPacket: rawPacket, rawPacket: rawPacket,
publicKey: contact.publicKey, publicKey: contact.publicKey,
name: contact.name, name: contact.name,
@@ -4864,9 +4638,6 @@ class MeshCoreConnector extends ChangeNotifier {
latitude: contact.latitude, latitude: contact.latitude,
longitude: contact.longitude, longitude: contact.longitude,
lastSeen: contact.lastSeen, lastSeen: contact.lastSeen,
lastMessageAt: contact.lastMessageAt,
isActive: false,
flags: 0,
); );
_discoveredContacts.add(disContact); _discoveredContacts.add(disContact);
-70
View File
@@ -1,70 +0,0 @@
import 'dart:async';
import 'dart:typed_data';
import '../services/app_debug_log_service.dart';
import '../services/tcp_transport_service.dart';
/// 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;
// --- Configuration ---
void setDebugLogService(AppDebugLogService? service) {
_debugLog = service;
_service.setDebugLogService(service);
}
// --- Connection lifecycle ---
Future<void> connect({required String host, required int port}) async {
_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 {
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();
}
}
@@ -53,9 +53,6 @@ class MeshCoreUsbManager {
} }
Future<void> disconnect() async { Future<void> disconnect() async {
if (!_service.isConnected && _activePortKey == null) {
return;
}
_debugLog?.info('UsbManager.disconnect', tag: 'USB'); _debugLog?.info('UsbManager.disconnect', tag: 'USB');
await _service.disconnect(); await _service.disconnect();
_activePortKey = null; _activePortKey = null;
+13 -40
View File
@@ -148,19 +148,6 @@ class BufferWriter {
void writeHex(String hex) { void writeHex(String hex) {
writeBytes(hex2Uint8List(hex)); writeBytes(hex2Uint8List(hex));
} }
void writeBytesPadded(Uint8List bytes, int totalLength) {
// Path data (64 bytes, zero-padded)
final bytesPadded = Uint8List(totalLength);
final len = bytes.length < totalLength ? bytes.length : totalLength;
if (bytes.isNotEmpty && len > 0) {
final copyLen = bytes.length < totalLength ? bytes.length : totalLength;
for (int i = 0; i < copyLen; i++) {
bytesPadded[i] = bytes[i];
}
}
writeBytes(bytesPadded);
}
} }
Uint8List hex2Uint8List(String hex) { Uint8List hex2Uint8List(String hex) {
@@ -689,17 +676,14 @@ Uint8List buildResetPathFrame(Uint8List pubKey) {
} }
// Build CMD_ADD_UPDATE_CONTACT frame to set custom path // Build CMD_ADD_UPDATE_CONTACT frame to set custom path
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][Lat? x4, Lon? x4][timestamp? x4] // Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4]
Uint8List buildUpdateContactPathFrame( Uint8List buildUpdateContactPathFrame(
Uint8List pubKey, Uint8List pubKey,
Uint8List path, Uint8List customPath,
int pathLen, { int pathLen, {
int type = 1, // ADV_TYPE_CHAT int type = 1, // ADV_TYPE_CHAT
int flags = 0, int flags = 0,
String name = '', String name = '',
double? lat,
double? lon,
DateTime? lastModified,
}) { }) {
final writer = BufferWriter(); final writer = BufferWriter();
writer.writeByte(cmdAddUpdateContact); writer.writeByte(cmdAddUpdateContact);
@@ -708,7 +692,17 @@ Uint8List buildUpdateContactPathFrame(
writer.writeByte(flags); writer.writeByte(flags);
writer.writeByte(pathLen); writer.writeByte(pathLen);
writer.writeBytesPadded(path, maxPathSize); // Path data (64 bytes, zero-padded)
final pathPadded = Uint8List(maxPathSize);
if (customPath.isNotEmpty && pathLen > 0) {
final copyLen = customPath.length < maxPathSize
? customPath.length
: maxPathSize;
for (int i = 0; i < copyLen; i++) {
pathPadded[i] = customPath[i];
}
}
writer.writeBytes(pathPadded);
// Name (32 bytes, null-padded) // Name (32 bytes, null-padded)
writer.writeCString(name, maxNameSize); writer.writeCString(name, maxNameSize);
@@ -717,27 +711,6 @@ Uint8List buildUpdateContactPathFrame(
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
writer.writeUInt32LE(timestamp); writer.writeUInt32LE(timestamp);
if ((lat == null || lon == null) && lastModified != null) {
// If lat/lon not provided, write zeros
writer.writeInt32LE(0);
writer.writeInt32LE(0);
} else {
// Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6
// Latitude
final latitude = lat ?? 0.0;
writer.writeInt32LE((latitude * 1e6).round());
// Longitude
final longitude = lon ?? 0.0;
writer.writeInt32LE((longitude * 1e6).round());
}
if (lastModified != null) {
// Last modified
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
writer.writeUInt32LE(lastModifiedTimestamp);
}
return writer.toBytes(); return writer.toBytes();
} }
+1 -30
View File
@@ -1859,34 +1859,5 @@
"usbConnectionFailed": "Неуспешно свързване през USB: {error}", "usbConnectionFailed": "Неуспешно свързване през USB: {error}",
"usbStatus_notConnected": "Изберете USB устройство", "usbStatus_notConnected": "Изберете USB устройство",
"usbStatus_searching": "Търсене на USB устройства...", "usbStatus_searching": "Търсене на USB устройства...",
"usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.", "usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "Свържете се чрез TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpHostLabel": "IP адрес",
"tcpPortLabel": "Пристанище",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Въведете крайната точка и свържете се.",
"tcpStatus_connectingTo": "Свързване към {endpoint}...",
"tcpErrorHostRequired": "Необходим е IP адрес.",
"tcpErrorPortInvalid": "Портът трябва да бъде между 1 и 65535.",
"tcpErrorUnsupported": "Транспортът чрез TCP не се поддържа на тази платформа.",
"tcpErrorTimedOut": "Връзката TCP изтекла.",
"tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}",
"map_showDiscoveryContacts": "Покажи контакти за откриване",
"map_setAsMyLocation": "Задайте като моя местоположение"
} }
+1 -30
View File
@@ -1887,34 +1887,5 @@
"usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus", "usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus",
"usbStatus_connecting": "Verbindung zum USB-Gerät...", "usbStatus_connecting": "Verbindung zum USB-Gerät...",
"usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}", "usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}",
"usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.", "usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "IP-Adresse",
"connectionChoiceTcpLabel": "TCP",
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "Verbinden über TCP",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Geben Sie den Endpunkt ein und verbinden Sie sich.",
"tcpStatus_connectingTo": "Verbindung zu {endpoint}...",
"tcpErrorHostRequired": "Eine IP-Adresse ist erforderlich.",
"tcpErrorPortInvalid": "Die Portnummer muss zwischen 1 und 65535 liegen.",
"tcpErrorUnsupported": "Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.",
"tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.",
"tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}",
"map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen",
"map_setAsMyLocation": "Als meine aktuelle Position festlegen"
} }
-29
View File
@@ -49,33 +49,6 @@
"scanner_title": "MeshCore Open", "scanner_title": "MeshCore Open",
"connectionChoiceUsbLabel": "USB", "connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Connect over TCP",
"tcpHostLabel": "IP Address",
"tcpHostHint": "192.168.40.10",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Enter endpoint and connect",
"tcpStatus_connectingTo": "Connecting to {endpoint}...",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"tcpErrorHostRequired": "IP address is required.",
"tcpErrorPortInvalid": "Port must be between 1 and 65535.",
"tcpErrorUnsupported": "TCP transport is not supported on this platform.",
"tcpErrorTimedOut": "TCP connection timed out.",
"tcpConnectionFailed": "TCP connection failed: {error}",
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbScreenTitle": "Connect over USB", "usbScreenTitle": "Connect over USB",
"usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.", "usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.",
"usbScreenStatus": "Select a USB device", "usbScreenStatus": "Select a USB device",
@@ -807,7 +780,6 @@
"map_source": "Source", "map_source": "Source",
"map_flags": "Flags", "map_flags": "Flags",
"map_shareMarkerHere": "Share marker here", "map_shareMarkerHere": "Share marker here",
"map_setAsMyLocation": "Set as my location",
"map_pinLabel": "Pin label", "map_pinLabel": "Pin label",
"map_label": "Label", "map_label": "Label",
"map_pointOfInterest": "Point of interest", "map_pointOfInterest": "Point of interest",
@@ -835,7 +807,6 @@
"map_markers": "Markers", "map_markers": "Markers",
"map_showSharedMarkers": "Show shared markers", "map_showSharedMarkers": "Show shared markers",
"map_showGuessedLocations": "Show guessed node locations", "map_showGuessedLocations": "Show guessed node locations",
"map_showDiscoveryContacts": "Show Discovery Contacts",
"map_guessedLocation": "Guessed location", "map_guessedLocation": "Guessed location",
"map_lastSeenTime": "Last Seen Time", "map_lastSeenTime": "Last Seen Time",
"map_sharedPin": "Shared pin", "map_sharedPin": "Shared pin",
+1 -30
View File
@@ -1887,34 +1887,5 @@
"usbStatus_searching": "Buscando dispositivos USB...", "usbStatus_searching": "Buscando dispositivos USB...",
"usbStatus_notConnected": "Seleccione un dispositivo USB", "usbStatus_notConnected": "Seleccione un dispositivo USB",
"usbConnectionFailed": "Error al conectar mediante USB: {error}", "usbConnectionFailed": "Error al conectar mediante USB: {error}",
"usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.", "usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpScreenTitle": "Establecer conexión a través de TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpHostHint": "192.168.40.10",
"tcpHostLabel": "Dirección IP",
"tcpPortLabel": "Puerto",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Ingrese la dirección final y conecte.",
"tcpStatus_connectingTo": "Conectándose a {endpoint}...",
"tcpErrorHostRequired": "Se requiere la dirección IP.",
"tcpErrorPortInvalid": "El puerto debe estar entre 1 y 65535.",
"tcpErrorUnsupported": "El protocolo de transporte TCP no está soportado en esta plataforma.",
"tcpErrorTimedOut": "La conexión TCP ha caducado.",
"tcpConnectionFailed": "Error en la conexión TCP: {error}",
"map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento",
"map_setAsMyLocation": "Establecer mi ubicación"
} }
+1 -30
View File
@@ -1859,34 +1859,5 @@
"usbConnectionFailed": "Échec de la connexion USB : {error}", "usbConnectionFailed": "Échec de la connexion USB : {error}",
"usbStatus_connecting": "Connexion au périphérique USB...", "usbStatus_connecting": "Connexion au périphérique USB...",
"usbStatus_searching": "Recherche de périphériques USB...", "usbStatus_searching": "Recherche de périphériques USB...",
"usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion.", "usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "Adresse IP",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Établir une connexion via TCP",
"tcpHostHint": "192.168.40.10",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Entrez l'adresse de destination et connectez-vous.",
"tcpStatus_connectingTo": "Connexion à {endpoint}...",
"tcpErrorHostRequired": "Une adresse IP est obligatoire.",
"tcpErrorPortInvalid": "La taille du port doit être comprise entre 1 et 65535.",
"tcpErrorUnsupported": "Le protocole TCP n'est pas pris en charge sur cette plateforme.",
"tcpErrorTimedOut": "La connexion TCP a expiré.",
"tcpConnectionFailed": "Échec de la connexion TCP : {error}",
"map_showDiscoveryContacts": "Afficher les contacts de découverte",
"map_setAsMyLocation": "Définir comme ma localisation"
} }
+1 -30
View File
@@ -1859,34 +1859,5 @@
"usbConnectionFailed": "Errore nella connessione USB: {error}", "usbConnectionFailed": "Errore nella connessione USB: {error}",
"usbStatus_notConnected": "Seleziona un dispositivo USB", "usbStatus_notConnected": "Seleziona un dispositivo USB",
"usbStatus_connecting": "Connessione al dispositivo USB...", "usbStatus_connecting": "Connessione al dispositivo USB...",
"usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.", "usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "Indirizzo IP",
"tcpHostHint": "192.168.40.10",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Stabilire una connessione tramite TCP",
"tcpPortLabel": "Porta",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Inserisci l'endpoint e connettiti.",
"tcpStatus_connectingTo": "Connessione a {endpoint}...",
"tcpErrorHostRequired": "È necessario fornire un indirizzo IP.",
"tcpErrorPortInvalid": "La dimensione della porta deve essere compresa tra 1 e 65535.",
"tcpErrorUnsupported": "Il protocollo TCP non è supportato su questa piattaforma.",
"tcpErrorTimedOut": "La connessione TCP è scaduta.",
"tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}",
"map_showDiscoveryContacts": "Mostra Contatti di Discovery",
"map_setAsMyLocation": "Imposta come la mia posizione"
} }
-90
View File
@@ -334,84 +334,6 @@ abstract class AppLocalizations {
/// **'Bluetooth'** /// **'Bluetooth'**
String get connectionChoiceBluetoothLabel; String get connectionChoiceBluetoothLabel;
/// No description provided for @connectionChoiceTcpLabel.
///
/// In en, this message translates to:
/// **'TCP'**
String get connectionChoiceTcpLabel;
/// No description provided for @tcpScreenTitle.
///
/// In en, this message translates to:
/// **'Connect over TCP'**
String get tcpScreenTitle;
/// No description provided for @tcpHostLabel.
///
/// In en, this message translates to:
/// **'IP Address'**
String get tcpHostLabel;
/// No description provided for @tcpHostHint.
///
/// In en, this message translates to:
/// **'192.168.40.10'**
String get tcpHostHint;
/// No description provided for @tcpPortLabel.
///
/// In en, this message translates to:
/// **'Port'**
String get tcpPortLabel;
/// No description provided for @tcpPortHint.
///
/// In en, this message translates to:
/// **'5000'**
String get tcpPortHint;
/// No description provided for @tcpStatus_notConnected.
///
/// In en, this message translates to:
/// **'Enter endpoint and connect'**
String get tcpStatus_notConnected;
/// No description provided for @tcpStatus_connectingTo.
///
/// In en, this message translates to:
/// **'Connecting to {endpoint}...'**
String tcpStatus_connectingTo(String endpoint);
/// No description provided for @tcpErrorHostRequired.
///
/// In en, this message translates to:
/// **'IP address is required.'**
String get tcpErrorHostRequired;
/// No description provided for @tcpErrorPortInvalid.
///
/// In en, this message translates to:
/// **'Port must be between 1 and 65535.'**
String get tcpErrorPortInvalid;
/// No description provided for @tcpErrorUnsupported.
///
/// In en, this message translates to:
/// **'TCP transport is not supported on this platform.'**
String get tcpErrorUnsupported;
/// No description provided for @tcpErrorTimedOut.
///
/// In en, this message translates to:
/// **'TCP connection timed out.'**
String get tcpErrorTimedOut;
/// No description provided for @tcpConnectionFailed.
///
/// In en, this message translates to:
/// **'TCP connection failed: {error}'**
String tcpConnectionFailed(String error);
/// No description provided for @usbScreenTitle. /// No description provided for @usbScreenTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -2746,12 +2668,6 @@ abstract class AppLocalizations {
/// **'Share marker here'** /// **'Share marker here'**
String get map_shareMarkerHere; String get map_shareMarkerHere;
/// No description provided for @map_setAsMyLocation.
///
/// In en, this message translates to:
/// **'Set as my location'**
String get map_setAsMyLocation;
/// No description provided for @map_pinLabel. /// No description provided for @map_pinLabel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -2872,12 +2788,6 @@ abstract class AppLocalizations {
/// **'Show guessed node locations'** /// **'Show guessed node locations'**
String get map_showGuessedLocations; String get map_showGuessedLocations;
/// No description provided for @map_showDiscoveryContacts.
///
/// In en, this message translates to:
/// **'Show Discovery Contacts'**
String get map_showDiscoveryContacts;
/// No description provided for @map_guessedLocation. /// No description provided for @map_guessedLocation.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
-50
View File
@@ -117,50 +117,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Свържете се чрез TCP';
@override
String get tcpHostLabel => 'IP адрес';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Пристанище';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Въведете крайната точка и свържете се.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Свързване към $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Необходим е IP адрес.';
@override
String get tcpErrorPortInvalid => 'Портът трябва да бъде между 1 и 65535.';
@override
String get tcpErrorUnsupported =>
'Транспортът чрез TCP не се поддържа на тази платформа.';
@override
String get tcpErrorTimedOut => 'Връзката TCP изтекла.';
@override
String tcpConnectionFailed(String error) {
return 'Неуспешно е установено TCP връзката: $error';
}
@override @override
String get usbScreenTitle => 'Свържете се чрез USB'; String get usbScreenTitle => 'Свържете се чрез USB';
@@ -1511,9 +1467,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Споделете маркер тук'; String get map_shareMarkerHere => 'Споделете маркер тук';
@override
String get map_setAsMyLocation => 'Задайте като моя местоположение';
@override @override
String get map_pinLabel => 'Етикетиране на пин'; String get map_pinLabel => 'Етикетиране на пин';
@@ -1578,9 +1531,6 @@ class AppLocalizationsBg extends AppLocalizations {
String get map_showGuessedLocations => String get map_showGuessedLocations =>
'Покажете местоположенията на предположените възли.'; 'Покажете местоположенията на предположените възли.';
@override
String get map_showDiscoveryContacts => 'Покажи контакти за откриване';
@override @override
String get map_guessedLocation => 'Предполагано местоположение'; String get map_guessedLocation => 'Предполагано местоположение';
-52
View File
@@ -117,52 +117,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Verbinden über TCP';
@override
String get tcpHostLabel => 'IP-Adresse';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected =>
'Geben Sie den Endpunkt ein und verbinden Sie sich.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Verbindung zu $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Eine IP-Adresse ist erforderlich.';
@override
String get tcpErrorPortInvalid =>
'Die Portnummer muss zwischen 1 und 65535 liegen.';
@override
String get tcpErrorUnsupported =>
'Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.';
@override
String get tcpErrorTimedOut => 'Die TCP-Verbindung ist abgelaufen.';
@override
String tcpConnectionFailed(String error) {
return 'Fehler beim TCP-Verbindungsaufbau: $error';
}
@override @override
String get usbScreenTitle => 'Verbinden über USB'; String get usbScreenTitle => 'Verbinden über USB';
@@ -1513,9 +1467,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Teilen Sie den Marker hier.'; String get map_shareMarkerHere => 'Teilen Sie den Marker hier.';
@override
String get map_setAsMyLocation => 'Als meine aktuelle Position festlegen';
@override @override
String get map_pinLabel => 'Pin Name'; String get map_pinLabel => 'Pin Name';
@@ -1580,9 +1531,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get map_showGuessedLocations => String get map_showGuessedLocations =>
'Zeige die vermuteten Knotenpositionen'; 'Zeige die vermuteten Knotenpositionen';
@override
String get map_showDiscoveryContacts => 'Entdeckungs-Kontakte anzeigen';
@override @override
String get map_guessedLocation => 'Geschätzter Ort'; String get map_guessedLocation => 'Geschätzter Ort';
-50
View File
@@ -117,50 +117,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Connect over TCP';
@override
String get tcpHostLabel => 'IP Address';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Enter endpoint and connect';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Connecting to $endpoint...';
}
@override
String get tcpErrorHostRequired => 'IP address is required.';
@override
String get tcpErrorPortInvalid => 'Port must be between 1 and 65535.';
@override
String get tcpErrorUnsupported =>
'TCP transport is not supported on this platform.';
@override
String get tcpErrorTimedOut => 'TCP connection timed out.';
@override
String tcpConnectionFailed(String error) {
return 'TCP connection failed: $error';
}
@override @override
String get usbScreenTitle => 'Connect over USB'; String get usbScreenTitle => 'Connect over USB';
@@ -1487,9 +1443,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Share marker here'; String get map_shareMarkerHere => 'Share marker here';
@override
String get map_setAsMyLocation => 'Set as my location';
@override @override
String get map_pinLabel => 'Pin label'; String get map_pinLabel => 'Pin label';
@@ -1553,9 +1506,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get map_showGuessedLocations => 'Show guessed node locations'; String get map_showGuessedLocations => 'Show guessed node locations';
@override
String get map_showDiscoveryContacts => 'Show Discovery Contacts';
@override @override
String get map_guessedLocation => 'Guessed location'; String get map_guessedLocation => 'Guessed location';
-50
View File
@@ -117,50 +117,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Establecer conexión a través de TCP';
@override
String get tcpHostLabel => 'Dirección IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Puerto';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Ingrese la dirección final y conecte.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Conectándose a $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Se requiere la dirección IP.';
@override
String get tcpErrorPortInvalid => 'El puerto debe estar entre 1 y 65535.';
@override
String get tcpErrorUnsupported =>
'El protocolo de transporte TCP no está soportado en esta plataforma.';
@override
String get tcpErrorTimedOut => 'La conexión TCP ha caducado.';
@override
String tcpConnectionFailed(String error) {
return 'Error en la conexión TCP: $error';
}
@override @override
String get usbScreenTitle => 'Conecte mediante USB'; String get usbScreenTitle => 'Conecte mediante USB';
@@ -1509,9 +1465,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Compartir marcador aquí'; String get map_shareMarkerHere => 'Compartir marcador aquí';
@override
String get map_setAsMyLocation => 'Establecer mi ubicación';
@override @override
String get map_pinLabel => 'Etiqueta de marcador'; String get map_pinLabel => 'Etiqueta de marcador';
@@ -1576,9 +1529,6 @@ class AppLocalizationsEs extends AppLocalizations {
String get map_showGuessedLocations => String get map_showGuessedLocations =>
'Mostrar las ubicaciones estimadas de los nodos.'; 'Mostrar las ubicaciones estimadas de los nodos.';
@override
String get map_showDiscoveryContacts => 'Mostrar Contactos de Descubrimiento';
@override @override
String get map_guessedLocation => 'Ubicación estimada'; String get map_guessedLocation => 'Ubicación estimada';
-52
View File
@@ -117,52 +117,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Établir une connexion via TCP';
@override
String get tcpHostLabel => 'Adresse IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected =>
'Entrez l\'adresse de destination et connectez-vous.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Connexion à $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Une adresse IP est obligatoire.';
@override
String get tcpErrorPortInvalid =>
'La taille du port doit être comprise entre 1 et 65535.';
@override
String get tcpErrorUnsupported =>
'Le protocole TCP n\'est pas pris en charge sur cette plateforme.';
@override
String get tcpErrorTimedOut => 'La connexion TCP a expiré.';
@override
String tcpConnectionFailed(String error) {
return 'Échec de la connexion TCP : $error';
}
@override @override
String get usbScreenTitle => 'Connectez via USB'; String get usbScreenTitle => 'Connectez via USB';
@@ -1518,9 +1472,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Partager le marqueur ici'; String get map_shareMarkerHere => 'Partager le marqueur ici';
@override
String get map_setAsMyLocation => 'Définir comme ma localisation';
@override @override
String get map_pinLabel => 'Étiquete de repin'; String get map_pinLabel => 'Étiquete de repin';
@@ -1585,9 +1536,6 @@ class AppLocalizationsFr extends AppLocalizations {
String get map_showGuessedLocations => String get map_showGuessedLocations =>
'Afficher les emplacements des nœuds estimés'; 'Afficher les emplacements des nœuds estimés';
@override
String get map_showDiscoveryContacts => 'Afficher les contacts de découverte';
@override @override
String get map_guessedLocation => 'Lieu deviné'; String get map_guessedLocation => 'Lieu deviné';
-51
View File
@@ -117,51 +117,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Stabilire una connessione tramite TCP';
@override
String get tcpHostLabel => 'Indirizzo IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Porta';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Inserisci l\'endpoint e connettiti.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Connessione a $endpoint...';
}
@override
String get tcpErrorHostRequired => 'È necessario fornire un indirizzo IP.';
@override
String get tcpErrorPortInvalid =>
'La dimensione della porta deve essere compresa tra 1 e 65535.';
@override
String get tcpErrorUnsupported =>
'Il protocollo TCP non è supportato su questa piattaforma.';
@override
String get tcpErrorTimedOut => 'La connessione TCP è scaduta.';
@override
String tcpConnectionFailed(String error) {
return 'Impossibile stabilire la connessione TCP: $error';
}
@override @override
String get usbScreenTitle => 'Connessione tramite USB'; String get usbScreenTitle => 'Connessione tramite USB';
@@ -1510,9 +1465,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Condividi marcatore qui'; String get map_shareMarkerHere => 'Condividi marcatore qui';
@override
String get map_setAsMyLocation => 'Imposta come la mia posizione';
@override @override
String get map_pinLabel => 'Etichetta PIN'; String get map_pinLabel => 'Etichetta PIN';
@@ -1576,9 +1528,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi'; String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi';
@override
String get map_showDiscoveryContacts => 'Mostra Contatti di Discovery';
@override @override
String get map_guessedLocation => 'Località indovinata'; String get map_guessedLocation => 'Località indovinata';
-51
View File
@@ -117,51 +117,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Verbind via TCP';
@override
String get tcpHostLabel => 'IP-adres';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Poort';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Voer het eindpunt in en verbind';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Verbinding maken met $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Een IP-adres is vereist.';
@override
String get tcpErrorPortInvalid =>
'De poortwaarde moet tussen 1 en 65535 liggen.';
@override
String get tcpErrorUnsupported =>
'TCP-transport wordt niet ondersteund op deze platform.';
@override
String get tcpErrorTimedOut => 'De TCP-verbinding is verlopen.';
@override
String tcpConnectionFailed(String error) {
return 'Verbinding met TCP mislukt: $error';
}
@override @override
String get usbScreenTitle => 'Verbind via USB'; String get usbScreenTitle => 'Verbind via USB';
@@ -1502,9 +1457,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Deel marker hier'; String get map_shareMarkerHere => 'Deel marker hier';
@override
String get map_setAsMyLocation => 'Stel dit in als mijn locatie';
@override @override
String get map_pinLabel => 'Label vastzetten'; String get map_pinLabel => 'Label vastzetten';
@@ -1569,9 +1521,6 @@ class AppLocalizationsNl extends AppLocalizations {
String get map_showGuessedLocations => String get map_showGuessedLocations =>
'Toon de voorspelde locaties van de knopen'; 'Toon de voorspelde locaties van de knopen';
@override
String get map_showDiscoveryContacts => 'Ontdek contacten weergeven';
@override @override
String get map_guessedLocation => 'Geroerde locatie'; String get map_guessedLocation => 'Geroerde locatie';
-52
View File
@@ -117,52 +117,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Połącz się za pomocą protokołu TCP';
@override
String get tcpHostLabel => 'Adres IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Wprowadź adres URL i połącz';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Połączenie z $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Wymagana jest adresa IP.';
@override
String get tcpErrorPortInvalid =>
'Numer portu musi mieścić się w zakresie od 1 do 65535.';
@override
String get tcpErrorUnsupported =>
'Transport protokoł TCP nie jest obsługiwany na tym urządzeniu.';
@override
String get tcpErrorTimedOut =>
'Połączenie TCP zakończyło się bez powodzenia.';
@override
String tcpConnectionFailed(String error) {
return 'Błąd połączenia TCP: $error';
}
@override @override
String get usbScreenTitle => 'Połącz przez USB'; String get usbScreenTitle => 'Połącz przez USB';
@@ -1512,9 +1466,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Udostępnij znacznik tutaj'; String get map_shareMarkerHere => 'Udostępnij znacznik tutaj';
@override
String get map_setAsMyLocation => 'Ustaw jako moje lokalizację';
@override @override
String get map_pinLabel => 'Oznacz etykietę'; String get map_pinLabel => 'Oznacz etykietę';
@@ -1579,9 +1530,6 @@ class AppLocalizationsPl extends AppLocalizations {
String get map_showGuessedLocations => String get map_showGuessedLocations =>
'Wyświetl lokalizacje zgadanych węzłów'; 'Wyświetl lokalizacje zgadanych węzłów';
@override
String get map_showDiscoveryContacts => 'Pokaż kontakty odkrywania';
@override @override
String get map_guessedLocation => 'Wydana lokalizacja'; String get map_guessedLocation => 'Wydana lokalizacja';
-51
View File
@@ -117,51 +117,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Estabelecer conexão via TCP';
@override
String get tcpHostLabel => 'Endereço IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Porta';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Insira o endereço final e conecte-se.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Conectando a $endpoint...';
}
@override
String get tcpErrorHostRequired => 'É necessário fornecer um endereço IP.';
@override
String get tcpErrorPortInvalid =>
'O valor do porto deve estar entre 1 e 65535.';
@override
String get tcpErrorUnsupported =>
'O protocolo TCP não é suportado nesta plataforma.';
@override
String get tcpErrorTimedOut => 'A conexão TCP expirou.';
@override
String tcpConnectionFailed(String error) {
return 'Falha na conexão TCP: $error';
}
@override @override
String get usbScreenTitle => 'Conecte via USB'; String get usbScreenTitle => 'Conecte via USB';
@@ -1511,9 +1466,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Compartilhar marcador aqui'; String get map_shareMarkerHere => 'Compartilhar marcador aqui';
@override
String get map_setAsMyLocation => 'Defina minha localização';
@override @override
String get map_pinLabel => 'Rótulo de marcador'; String get map_pinLabel => 'Rótulo de marcador';
@@ -1578,9 +1530,6 @@ class AppLocalizationsPt extends AppLocalizations {
String get map_showGuessedLocations => String get map_showGuessedLocations =>
'Mostrar as localizações dos nós estimados'; 'Mostrar as localizações dos nós estimados';
@override
String get map_showDiscoveryContacts => 'Mostrar Contatos de Descoberta';
@override @override
String get map_guessedLocation => 'Localização estimada'; String get map_guessedLocation => 'Localização estimada';
-51
View File
@@ -117,51 +117,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Установить соединение по протоколу TCP';
@override
String get tcpHostLabel => 'IP-адрес';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Порт';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Введите адрес и подключитесь.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Подключение к $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Необходимо указать IP-адрес.';
@override
String get tcpErrorPortInvalid =>
'Порт должен находиться в диапазоне от 1 до 65535.';
@override
String get tcpErrorUnsupported =>
'Протокол TCP не поддерживается на этой платформе.';
@override
String get tcpErrorTimedOut => 'Соединение TCP не удалось установить.';
@override
String tcpConnectionFailed(String error) {
return 'Не удалось установить соединение TCP: $error';
}
@override @override
String get usbScreenTitle => 'Подключение через USB'; String get usbScreenTitle => 'Подключение через USB';
@@ -1513,9 +1468,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Поделиться меткой здесь'; String get map_shareMarkerHere => 'Поделиться меткой здесь';
@override
String get map_setAsMyLocation => 'Установить мое местоположение';
@override @override
String get map_pinLabel => 'Метка'; String get map_pinLabel => 'Метка';
@@ -1580,9 +1532,6 @@ class AppLocalizationsRu extends AppLocalizations {
String get map_showGuessedLocations => String get map_showGuessedLocations =>
'Отобразить предполагаемые места расположения узлов'; 'Отобразить предполагаемые места расположения узлов';
@override
String get map_showDiscoveryContacts => 'Показать контакты Discovery';
@override @override
String get map_guessedLocation => 'Угаданное место'; String get map_guessedLocation => 'Угаданное место';
-50
View File
@@ -117,50 +117,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Spojte sa pomocou protokolu TCP';
@override
String get tcpHostLabel => 'IP adresa';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Zadajte cieľovú adresu a pripojte sa.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Pripojenie k $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Je potrebné zadať IP adresu.';
@override
String get tcpErrorPortInvalid => 'Číslo portu musí byť medzi 1 a 65535.';
@override
String get tcpErrorUnsupported =>
'Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.';
@override
String get tcpErrorTimedOut => 'Pripojenie TCP vypršalo.';
@override
String tcpConnectionFailed(String error) {
return 'Neúspešné vytvorenie TCP spojenia: $error';
}
@override @override
String get usbScreenTitle => 'Pripojte cez USB'; String get usbScreenTitle => 'Pripojte cez USB';
@@ -1504,9 +1460,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Zdieľte značku tu'; String get map_shareMarkerHere => 'Zdieľte značku tu';
@override
String get map_setAsMyLocation => 'Nastavte ako moju polohu';
@override @override
String get map_pinLabel => 'Označka upozornenia'; String get map_pinLabel => 'Označka upozornenia';
@@ -1571,9 +1524,6 @@ class AppLocalizationsSk extends AppLocalizations {
String get map_showGuessedLocations => String get map_showGuessedLocations =>
'Zobraziť umiestnenia odhadnutých uzlov'; 'Zobraziť umiestnenia odhadnutých uzlov';
@override
String get map_showDiscoveryContacts => 'Zobraziť kontakty objavov';
@override @override
String get map_guessedLocation => 'Odhadnutá lokalita'; String get map_guessedLocation => 'Odhadnutá lokalita';
-50
View File
@@ -117,50 +117,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Komunicirajte preko protokola TCP';
@override
String get tcpHostLabel => 'IP naslov';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Vrata';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Vnesite končni naslov in se povežite';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Povezava z $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Potrebna je IP-naslov.';
@override
String get tcpErrorPortInvalid => 'Port mora biti med 1 in 65535.';
@override
String get tcpErrorUnsupported =>
'Transport preko protokola TCP ni podprt na tej platformi.';
@override
String get tcpErrorTimedOut => 'Povezava TCP je presegla časovno obdobje.';
@override
String tcpConnectionFailed(String error) {
return 'Napaka pri povezavi TCP: $error';
}
@override @override
String get usbScreenTitle => 'Povežite preko USB'; String get usbScreenTitle => 'Povežite preko USB';
@@ -1498,9 +1454,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Delite točke tukaj.'; String get map_shareMarkerHere => 'Delite točke tukaj.';
@override
String get map_setAsMyLocation => 'Nastavite to kot mojo lokacijo';
@override @override
String get map_pinLabel => 'Oznaka za pritrditev'; String get map_pinLabel => 'Oznaka za pritrditev';
@@ -1564,9 +1517,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.'; String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.';
@override
String get map_showDiscoveryContacts => 'Prikaži odkritja kontaktov';
@override @override
String get map_guessedLocation => 'Predpostavljena lokacija'; String get map_guessedLocation => 'Predpostavljena lokacija';
-50
View File
@@ -117,50 +117,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Anslut via TCP';
@override
String get tcpHostLabel => 'IP-adress';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Ange slutpunkt och anslut';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Anslutning till $endpoint...';
}
@override
String get tcpErrorHostRequired => 'IP-adress krävs.';
@override
String get tcpErrorPortInvalid => 'Porten måste vara mellan 1 och 65535.';
@override
String get tcpErrorUnsupported =>
'TCP-transport fungerar inte på denna plattform.';
@override
String get tcpErrorTimedOut => 'TCP-anslutningen har tidsut gått.';
@override
String tcpConnectionFailed(String error) {
return 'Fel vid TCP-anslutning: $error';
}
@override @override
String get usbScreenTitle => 'Anslut via USB'; String get usbScreenTitle => 'Anslut via USB';
@@ -1494,9 +1450,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Dela markeringen här'; String get map_shareMarkerHere => 'Dela markeringen här';
@override
String get map_setAsMyLocation => 'Ange som min plats';
@override @override
String get map_pinLabel => 'Fästetikett'; String get map_pinLabel => 'Fästetikett';
@@ -1561,9 +1514,6 @@ class AppLocalizationsSv extends AppLocalizations {
String get map_showGuessedLocations => String get map_showGuessedLocations =>
'Visa upp de antagna nodernas placeringar'; 'Visa upp de antagna nodernas placeringar';
@override
String get map_showDiscoveryContacts => 'Visa Discovery-kontakter';
@override @override
String get map_guessedLocation => 'Gissad plats'; String get map_guessedLocation => 'Gissad plats';
-51
View File
@@ -117,51 +117,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => 'Bluetooth'; String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'З\'єднатися через протокол TCP';
@override
String get tcpHostLabel => 'IP-адреса';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Порт';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Введіть кінцеву точку та підключіться';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Підключення до $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Необхідно вказати IP-адресу.';
@override
String get tcpErrorPortInvalid => 'Порт повинен бути в межах від 1 до 65535.';
@override
String get tcpErrorUnsupported =>
'Транспорт TCP не підтримується на цій платформі.';
@override
String get tcpErrorTimedOut =>
'З\'єднання TCP завершилося через закінчення часу очікування.';
@override
String tcpConnectionFailed(String error) {
return 'Не вдалося встановити з\'єднання TCP: $error';
}
@override @override
String get usbScreenTitle => 'Підключити через USB'; String get usbScreenTitle => 'Підключити через USB';
@@ -1510,9 +1465,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get map_shareMarkerHere => 'Поділитися маркером тут'; String get map_shareMarkerHere => 'Поділитися маркером тут';
@override
String get map_setAsMyLocation => 'Встановити моє місцезнаходження';
@override @override
String get map_pinLabel => 'Мітка піна'; String get map_pinLabel => 'Мітка піна';
@@ -1577,9 +1529,6 @@ class AppLocalizationsUk extends AppLocalizations {
String get map_showGuessedLocations => String get map_showGuessedLocations =>
'Показати місцезнаходження передбачених вузлів'; 'Показати місцезнаходження передбачених вузлів';
@override
String get map_showDiscoveryContacts => 'Показати контакти Відкриття';
@override @override
String get map_guessedLocation => 'Визначено місцезнаходження'; String get map_guessedLocation => 'Визначено місцезнаходження';
-49
View File
@@ -117,49 +117,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get connectionChoiceBluetoothLabel => '蓝牙'; String get connectionChoiceBluetoothLabel => '蓝牙';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => '通过 TCP 连接';
@override
String get tcpHostLabel => 'IP地址';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => '端口';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => '输入目标地址,然后连接';
@override
String tcpStatus_connectingTo(String endpoint) {
return '连接到 $endpoint...';
}
@override
String get tcpErrorHostRequired => '需要提供IP地址。';
@override
String get tcpErrorPortInvalid => '端口号必须在 1 到 65535 之间。';
@override
String get tcpErrorUnsupported => '此平台不支持 TCP 传输。';
@override
String get tcpErrorTimedOut => 'TCP 连接超时。';
@override
String tcpConnectionFailed(String error) {
return 'TCP 连接失败:$error';
}
@override @override
String get usbScreenTitle => '通过USB连接'; String get usbScreenTitle => '通过USB连接';
@@ -1421,9 +1378,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get map_shareMarkerHere => '在此分享标记'; String get map_shareMarkerHere => '在此分享标记';
@override
String get map_setAsMyLocation => '设置为我的位置';
@override @override
String get map_pinLabel => '标签'; String get map_pinLabel => '标签';
@@ -1486,9 +1440,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get map_showGuessedLocations => '显示猜测的节点位置'; String get map_showGuessedLocations => '显示猜测的节点位置';
@override
String get map_showDiscoveryContacts => '显示发现联系人';
@override @override
String get map_guessedLocation => '猜测的位置'; String get map_guessedLocation => '猜测的位置';
+1 -30
View File
@@ -1859,34 +1859,5 @@
"usbStatus_notConnected": "Selecteer een USB-apparaat", "usbStatus_notConnected": "Selecteer een USB-apparaat",
"usbStatus_connecting": "Verbinding maken met USB-apparaat...", "usbStatus_connecting": "Verbinding maken met USB-apparaat...",
"usbStatus_searching": "Zoeken naar USB-apparaten...", "usbStatus_searching": "Zoeken naar USB-apparaten...",
"usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.", "usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpScreenTitle": "Verbind via TCP",
"tcpHostLabel": "IP-adres",
"tcpHostHint": "192.168.40.10",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "Poort",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Voer het eindpunt in en verbind",
"tcpStatus_connectingTo": "Verbinding maken met {endpoint}...",
"tcpErrorHostRequired": "Een IP-adres is vereist.",
"tcpErrorPortInvalid": "De poortwaarde moet tussen 1 en 65535 liggen.",
"tcpErrorUnsupported": "TCP-transport wordt niet ondersteund op deze platform.",
"tcpErrorTimedOut": "De TCP-verbinding is verlopen.",
"tcpConnectionFailed": "Verbinding met TCP mislukt: {error}",
"map_showDiscoveryContacts": "Ontdek contacten weergeven",
"map_setAsMyLocation": "Stel dit in als mijn locatie"
} }
+1 -30
View File
@@ -1859,34 +1859,5 @@
"usbStatus_connecting": "Połączenie z urządzeniem USB...", "usbStatus_connecting": "Połączenie z urządzeniem USB...",
"usbStatus_notConnected": "Wybierz urządzenie USB", "usbStatus_notConnected": "Wybierz urządzenie USB",
"usbConnectionFailed": "Błąd połączenia USB: {error}", "usbConnectionFailed": "Błąd połączenia USB: {error}",
"usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".", "usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\"."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"connectionChoiceTcpLabel": "TCP",
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "Połącz się za pomocą protokołu TCP",
"tcpHostLabel": "Adres IP",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Wprowadź adres URL i połącz",
"tcpStatus_connectingTo": "Połączenie z {endpoint}...",
"tcpErrorHostRequired": "Wymagana jest adresa IP.",
"tcpErrorPortInvalid": "Numer portu musi mieścić się w zakresie od 1 do 65535.",
"tcpErrorUnsupported": "Transport protokoł TCP nie jest obsługiwany na tym urządzeniu.",
"tcpErrorTimedOut": "Połączenie TCP zakończyło się bez powodzenia.",
"tcpConnectionFailed": "Błąd połączenia TCP: {error}",
"map_showDiscoveryContacts": "Pokaż kontakty odkrywania",
"map_setAsMyLocation": "Ustaw jako moje lokalizację"
} }
+1 -30
View File
@@ -1859,34 +1859,5 @@
"usbStatus_notConnected": "Selecione um dispositivo USB", "usbStatus_notConnected": "Selecione um dispositivo USB",
"usbConnectionFailed": "Falha na conexão USB: {error}", "usbConnectionFailed": "Falha na conexão USB: {error}",
"usbStatus_connecting": "Conectando ao dispositivo USB...", "usbStatus_connecting": "Conectando ao dispositivo USB...",
"usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.", "usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "Endereço IP",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Estabelecer conexão via TCP",
"tcpHostHint": "192.168.40.10",
"tcpPortLabel": "Porta",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Insira o endereço final e conecte-se.",
"tcpStatus_connectingTo": "Conectando a {endpoint}...",
"tcpErrorHostRequired": "É necessário fornecer um endereço IP.",
"tcpErrorPortInvalid": "O valor do porto deve estar entre 1 e 65535.",
"tcpErrorUnsupported": "O protocolo TCP não é suportado nesta plataforma.",
"tcpErrorTimedOut": "A conexão TCP expirou.",
"tcpConnectionFailed": "Falha na conexão TCP: {error}",
"map_showDiscoveryContacts": "Mostrar Contatos de Descoberta",
"map_setAsMyLocation": "Defina minha localização"
} }
+1 -30
View File
@@ -1099,34 +1099,5 @@
"usbStatus_connecting": "Подключение к USB-устройству...", "usbStatus_connecting": "Подключение к USB-устройству...",
"usbConnectionFailed": "Не удалось установить соединение через USB: {error}", "usbConnectionFailed": "Не удалось установить соединение через USB: {error}",
"usbStatus_notConnected": "Выберите USB-устройство", "usbStatus_notConnected": "Выберите USB-устройство",
"usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.", "usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"connectionChoiceTcpLabel": "TCP",
"tcpHostLabel": "IP-адрес",
"tcpScreenTitle": "Установить соединение по протоколу TCP",
"tcpPortLabel": "Порт",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Введите адрес и подключитесь.",
"tcpStatus_connectingTo": "Подключение к {endpoint}...",
"tcpErrorHostRequired": "Необходимо указать IP-адрес.",
"tcpErrorPortInvalid": "Порт должен находиться в диапазоне от 1 до 65535.",
"tcpErrorUnsupported": "Протокол TCP не поддерживается на этой платформе.",
"tcpErrorTimedOut": "Соединение TCP не удалось установить.",
"tcpConnectionFailed": "Не удалось установить соединение TCP: {error}",
"map_showDiscoveryContacts": "Показать контакты Discovery",
"map_setAsMyLocation": "Установить мое местоположение"
} }
+1 -30
View File
@@ -1859,34 +1859,5 @@
"usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}", "usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}",
"usbStatus_notConnected": "Vyberte USB zariadenie", "usbStatus_notConnected": "Vyberte USB zariadenie",
"usbStatus_connecting": "Pripojenie k USB zariadeniu...", "usbStatus_connecting": "Pripojenie k USB zariadeniu...",
"usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.", "usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"tcpHostLabel": "IP adresa",
"tcpScreenTitle": "Spojte sa pomocou protokolu TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Zadajte cieľovú adresu a pripojte sa.",
"tcpStatus_connectingTo": "Pripojenie k {endpoint}...",
"tcpErrorHostRequired": "Je potrebné zadať IP adresu.",
"tcpErrorPortInvalid": "Číslo portu musí byť medzi 1 a 65535.",
"tcpErrorUnsupported": "Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.",
"tcpErrorTimedOut": "Pripojenie TCP vypršalo.",
"tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}",
"map_showDiscoveryContacts": "Zobraziť kontakty objavov",
"map_setAsMyLocation": "Nastavte ako moju polohu"
} }
+1 -30
View File
@@ -1859,34 +1859,5 @@
"usbStatus_connecting": "Povezava z USB napravo...", "usbStatus_connecting": "Povezava z USB napravo...",
"usbStatus_searching": "Iskanje USB naprav...", "usbStatus_searching": "Iskanje USB naprav...",
"usbConnectionFailed": "Napaka pri povezavi preko USB: {error}", "usbConnectionFailed": "Napaka pri povezavi preko USB: {error}",
"usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.", "usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"connectionChoiceTcpLabel": "TCP",
"tcpHostLabel": "IP naslov",
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "Komunicirajte preko protokola TCP",
"tcpPortLabel": "Vrata",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Vnesite končni naslov in se povežite",
"tcpStatus_connectingTo": "Povezava z {endpoint}...",
"tcpErrorHostRequired": "Potrebna je IP-naslov.",
"tcpErrorPortInvalid": "Port mora biti med 1 in 65535.",
"tcpErrorUnsupported": "Transport preko protokola TCP ni podprt na tej platformi.",
"tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.",
"tcpConnectionFailed": "Napaka pri povezavi TCP: {error}",
"map_showDiscoveryContacts": "Prikaži odkritja kontaktov",
"map_setAsMyLocation": "Nastavite to kot mojo lokacijo"
} }
+1 -30
View File
@@ -1859,34 +1859,5 @@
"usbStatus_notConnected": "Välj en USB-enhet", "usbStatus_notConnected": "Välj en USB-enhet",
"usbConnectionFailed": "Fel vid USB-anslutning: {error}", "usbConnectionFailed": "Fel vid USB-anslutning: {error}",
"usbStatus_searching": "Söker efter USB-enheter...", "usbStatus_searching": "Söker efter USB-enheter...",
"usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.", "usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"tcpHostLabel": "IP-adress",
"tcpScreenTitle": "Anslut via TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Ange slutpunkt och anslut",
"tcpStatus_connectingTo": "Anslutning till {endpoint}...",
"tcpErrorHostRequired": "IP-adress krävs.",
"tcpErrorPortInvalid": "Porten måste vara mellan 1 och 65535.",
"tcpErrorUnsupported": "TCP-transport fungerar inte på denna plattform.",
"tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.",
"tcpConnectionFailed": "Fel vid TCP-anslutning: {error}",
"map_showDiscoveryContacts": "Visa Discovery-kontakter",
"map_setAsMyLocation": "Ange som min plats"
} }
+1 -30
View File
@@ -1859,34 +1859,5 @@
"usbStatus_notConnected": "Виберіть пристрій USB", "usbStatus_notConnected": "Виберіть пристрій USB",
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}", "usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}",
"usbStatus_connecting": "Підключення до USB-пристрою...", "usbStatus_connecting": "Підключення до USB-пристрою...",
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.", "usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion."
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"connectionChoiceTcpLabel": "TCP",
"tcpHostHint": "192.168.40.10",
"tcpHostLabel": "IP-адреса",
"tcpScreenTitle": "З'єднатися через протокол TCP",
"tcpPortLabel": "Порт",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Введіть кінцеву точку та підключіться",
"tcpStatus_connectingTo": "Підключення до {endpoint}...",
"tcpErrorHostRequired": "Необхідно вказати IP-адресу.",
"tcpErrorPortInvalid": "Порт повинен бути в межах від 1 до 65535.",
"tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.",
"tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.",
"tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}",
"map_showDiscoveryContacts": "Показати контакти Відкриття",
"map_setAsMyLocation": "Встановити моє місцезнаходження"
} }
+1 -30
View File
@@ -1864,34 +1864,5 @@
"usbStatus_connecting": "连接USB设备...", "usbStatus_connecting": "连接USB设备...",
"usbStatus_notConnected": "选择一个 USB 设备", "usbStatus_notConnected": "选择一个 USB 设备",
"usbConnectionFailed": "USB 连接失败:{error}", "usbConnectionFailed": "USB 连接失败:{error}",
"usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。", "usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。"
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "IP地址",
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "通过 TCP 连接",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "端口",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "输入目标地址,然后连接",
"tcpStatus_connectingTo": "连接到 {endpoint}...",
"tcpErrorHostRequired": "需要提供IP地址。",
"tcpErrorPortInvalid": "端口号必须在 1 到 65535 之间。",
"tcpErrorUnsupported": "此平台不支持 TCP 传输。",
"tcpErrorTimedOut": "TCP 连接超时。",
"tcpConnectionFailed": "TCP 连接失败:{error}",
"map_showDiscoveryContacts": "显示发现联系人",
"map_setAsMyLocation": "设置为我的位置"
} }
-8
View File
@@ -39,7 +39,6 @@ class AppSettings {
final Map<String, String> batteryChemistryByRepeaterId; final Map<String, String> batteryChemistryByRepeaterId;
final UnitSystem unitSystem; final UnitSystem unitSystem;
final Set<String> mutedChannels; final Set<String> mutedChannels;
final bool mapShowDiscoveryContacts;
AppSettings({ AppSettings({
this.clearPathOnMaxRetry = false, this.clearPathOnMaxRetry = false,
@@ -67,7 +66,6 @@ class AppSettings {
Map<String, String>? batteryChemistryByRepeaterId, Map<String, String>? batteryChemistryByRepeaterId,
this.unitSystem = UnitSystem.metric, this.unitSystem = UnitSystem.metric,
Set<String>? mutedChannels, Set<String>? mutedChannels,
this.mapShowDiscoveryContacts = true,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
mutedChannels = mutedChannels ?? {}; mutedChannels = mutedChannels ?? {};
@@ -99,7 +97,6 @@ class AppSettings {
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId, 'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
'unit_system': unitSystem.value, 'unit_system': unitSystem.value,
'muted_channels': mutedChannels.toList(), 'muted_channels': mutedChannels.toList(),
'map_show_discovery_contacts': mapShowDiscoveryContacts,
}; };
} }
@@ -155,8 +152,6 @@ class AppSettings {
?.map((e) => e.toString()) ?.map((e) => e.toString())
.toSet()) ?? .toSet()) ??
{}, {},
mapShowDiscoveryContacts:
json['map_show_discovery_contacts'] as bool? ?? true,
); );
} }
@@ -186,7 +181,6 @@ class AppSettings {
Map<String, String>? batteryChemistryByRepeaterId, Map<String, String>? batteryChemistryByRepeaterId,
UnitSystem? unitSystem, UnitSystem? unitSystem,
Set<String>? mutedChannels, Set<String>? mutedChannels,
bool? mapShowDiscoveryContacts,
}) { }) {
return AppSettings( return AppSettings(
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
@@ -223,8 +217,6 @@ class AppSettings {
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId, batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
unitSystem: unitSystem ?? this.unitSystem, unitSystem: unitSystem ?? this.unitSystem,
mutedChannels: mutedChannels ?? this.mutedChannels, mutedChannels: mutedChannels ?? this.mutedChannels,
mapShowDiscoveryContacts:
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
); );
} }
} }
-10
View File
@@ -17,8 +17,6 @@ class Contact {
final double? longitude; final double? longitude;
final DateTime lastSeen; final DateTime lastSeen;
final DateTime lastMessageAt; final DateTime lastMessageAt;
final bool isActive;
final Uint8List? rawPacket;
Contact({ Contact({
required this.publicKey, required this.publicKey,
@@ -33,8 +31,6 @@ class Contact {
this.longitude, this.longitude,
required this.lastSeen, required this.lastSeen,
DateTime? lastMessageAt, DateTime? lastMessageAt,
this.isActive = true,
this.rawPacket,
}) : lastMessageAt = lastMessageAt ?? lastSeen; }) : lastMessageAt = lastMessageAt ?? lastSeen;
String get publicKeyHex => pubKeyToHex(publicKey); String get publicKeyHex => pubKeyToHex(publicKey);
@@ -82,8 +78,6 @@ class Contact {
double? longitude, double? longitude,
DateTime? lastSeen, DateTime? lastSeen,
DateTime? lastMessageAt, DateTime? lastMessageAt,
bool? isActive,
Uint8List? rawPacket,
}) { }) {
return Contact( return Contact(
publicKey: publicKey ?? this.publicKey, publicKey: publicKey ?? this.publicKey,
@@ -102,8 +96,6 @@ class Contact {
longitude: longitude ?? this.longitude, longitude: longitude ?? this.longitude,
lastSeen: lastSeen ?? this.lastSeen, lastSeen: lastSeen ?? this.lastSeen,
lastMessageAt: lastMessageAt ?? this.lastMessageAt, lastMessageAt: lastMessageAt ?? this.lastMessageAt,
isActive: isActive ?? this.isActive,
rawPacket: rawPacket ?? this.rawPacket,
); );
} }
@@ -212,8 +204,6 @@ class Contact {
latitude: lat, latitude: lat,
longitude: lon, longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000), lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
isActive: true,
rawPacket: null,
); );
} catch (e) { } catch (e) {
appLogger.error('Failed to parse contact frame: $e'); appLogger.error('Failed to parse contact frame: $e');
+105
View File
@@ -0,0 +1,105 @@
import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
class DiscoveryContact {
final Uint8List rawPacket;
final Uint8List publicKey;
final String name;
final int type;
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
final Uint8List path; // Path bytes from device
final double? latitude;
final double? longitude;
final DateTime lastSeen;
DiscoveryContact({
required this.rawPacket,
required this.publicKey,
required this.name,
required this.type,
required this.pathLength,
required this.path,
this.latitude,
this.longitude,
required this.lastSeen,
});
String get publicKeyHex => pubKeyToHex(publicKey);
String get typeLabel {
switch (type) {
case advTypeChat:
return 'Chat';
case advTypeRepeater:
return 'Repeater';
case advTypeRoom:
return 'Room';
case advTypeSensor:
return 'Sensor';
default:
return 'Unknown';
}
}
String get pathLabel {
if (pathLength < 0) return 'Flood';
if (pathLength == 0) return 'Direct';
return '$pathLength hops';
}
bool get hasLocation => latitude != null && longitude != null;
DiscoveryContact copyWith({
Uint8List? rawPacket,
Uint8List? publicKey,
String? name,
int? type,
int? pathLength,
Uint8List? path,
double? latitude,
double? longitude,
DateTime? lastSeen,
}) {
return DiscoveryContact(
rawPacket: rawPacket ?? this.rawPacket,
publicKey: publicKey ?? this.publicKey,
name: name ?? this.name,
type: type ?? this.type,
pathLength: pathLength ?? this.pathLength,
path: path ?? this.path,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
lastSeen: lastSeen ?? this.lastSeen,
);
}
String get pathIdList {
final pathBytes = path;
if (pathBytes.isEmpty) return '';
final parts = <String>[];
final groupSize = pathHashSize;
for (int i = 0; i < pathBytes.length; i += groupSize) {
final end = (i + groupSize) <= pathBytes.length
? (i + groupSize)
: pathBytes.length;
final chunk = pathBytes.sublist(i, end);
parts.add(
chunk
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(),
);
}
return parts.join(',');
}
String get shortPubKeyHex {
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
}
@override
bool operator ==(Object other) =>
other is DiscoveryContact && publicKeyHex == other.publicKeyHex;
@override
int get hashCode => publicKeyHex.hashCode;
}
-13
View File
@@ -118,19 +118,6 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
: Icons.download, : Icons.download,
size: 18, size: 18,
), ),
onLongPress: () async {
await Clipboard.setData(
ClipboardData(
text: entry.payload
.map(
(b) => b
.toRadixString(16)
.padLeft(2, '0'),
)
.join(''),
),
);
},
); );
} }
+7 -10
View File
@@ -40,11 +40,8 @@ class ChannelMessagePathScreen extends StatelessWidget {
final primaryPath = !channelMessage && !message.isOutgoing final primaryPath = !channelMessage && !message.isOutgoing
? Uint8List.fromList(primaryPathTmp.reversed.toList()) ? Uint8List.fromList(primaryPathTmp.reversed.toList())
: primaryPathTmp; : primaryPathTmp;
final contacts = <Contact>[
...connector.contacts, final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
...connector.discoveredContacts,
];
final hops = _buildPathHops(primaryPath, contacts, l10n);
final hasHopDetails = primaryPath.isNotEmpty; final hasHopDetails = primaryPath.isNotEmpty;
final observedLabel = _formatObservedHops( final observedLabel = _formatObservedHops(
primaryPath.length, primaryPath.length,
@@ -367,11 +364,11 @@ class _ChannelMessagePathMapScreenState
: selectedPathTmp; : selectedPathTmp;
final selectedIndex = _indexForPath(selectedPath, observedPaths); final selectedIndex = _indexForPath(selectedPath, observedPaths);
final contacts = <Contact>[ final hops = _buildPathHops(
...connector.contacts, selectedPath,
...connector.discoveredContacts, connector.contacts,
]; context.l10n,
final hops = _buildPathHops(selectedPath, contacts, context.l10n); );
final points = <LatLng>[]; final points = <LatLng>[];
+12 -32
View File
@@ -51,11 +51,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
// Cache of PSK hex -> Community for quick lookup // Cache of PSK hex -> Community for quick lookup
final Map<String, Community> _pskToCommunity = {}; final Map<String, Community> _pskToCommunity = {};
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MeshCoreConnector>().getChannels(); context.read<MeshCoreConnector>().getChannels();
_loadCommunities(); _loadCommunities();
@@ -63,8 +63,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
} }
Future<void> _loadCommunities() async { Future<void> _loadCommunities() async {
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
final communities = await _communityStore.loadCommunities(); final communities = await _communityStore.loadCommunities();
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -718,8 +716,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
bool isRegularHashtag = true; bool isRegularHashtag = true;
Community? selectedCommunity; Community? selectedCommunity;
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
showDialog( showDialog(
context: context, context: context,
builder: (dialogContext) => StatefulBuilder( builder: (dialogContext) => StatefulBuilder(
@@ -771,9 +767,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
); );
} }
Widget? buildExpandedContent( Widget? buildExpandedContent() {
ChannelMessageStore channelMessageStore,
) {
switch (selectedOption) { switch (selectedOption) {
case 0: // Create Private Channel case 0: // Create Private Channel
return Column( return Column(
@@ -798,7 +792,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
children: [ children: [
Expanded( Expanded(
child: FilledButton( child: FilledButton(
onPressed: () async { onPressed: () {
final name = nameController.text.trim(); final name = nameController.text.trim();
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of( ScaffoldMessenger.of(
@@ -820,14 +814,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
psk[i] = random.nextInt(256); psk[i] = random.nextInt(256);
} }
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
await connector.setChannel( connector.setChannel(nextIndex, name, psk);
nextIndex,
name,
psk,
);
await channelMessageStore.clearChannelMessages(
nextIndex,
);
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@@ -1346,8 +1333,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle: subtitle:
dialogContext.l10n.channels_createPrivateChannelDesc, dialogContext.l10n.channels_createPrivateChannelDesc,
), ),
if (selectedOption == 0) if (selectedOption == 0) buildExpandedContent()!,
buildExpandedContent(_channelMessageStore)!,
const Divider(height: 1), const Divider(height: 1),
buildOptionTile( buildOptionTile(
optionIndex: 1, optionIndex: 1,
@@ -1356,8 +1342,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle: subtitle:
dialogContext.l10n.channels_joinPrivateChannelDesc, dialogContext.l10n.channels_joinPrivateChannelDesc,
), ),
if (selectedOption == 1) if (selectedOption == 1) buildExpandedContent()!,
buildExpandedContent(_channelMessageStore)!,
if (!hasPublicChannel) ...[ if (!hasPublicChannel) ...[
const Divider(height: 1), const Divider(height: 1),
buildOptionTile( buildOptionTile(
@@ -1367,8 +1352,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle: subtitle:
dialogContext.l10n.channels_joinPublicChannelDesc, dialogContext.l10n.channels_joinPublicChannelDesc,
), ),
if (selectedOption == 2) if (selectedOption == 2) buildExpandedContent()!,
buildExpandedContent(_channelMessageStore)!,
], ],
const Divider(height: 1), const Divider(height: 1),
buildOptionTile( buildOptionTile(
@@ -1378,8 +1362,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle: subtitle:
dialogContext.l10n.channels_joinHashtagChannelDesc, dialogContext.l10n.channels_joinHashtagChannelDesc,
), ),
if (selectedOption == 3) if (selectedOption == 3) buildExpandedContent()!,
buildExpandedContent(_channelMessageStore)!,
const Divider(height: 1), const Divider(height: 1),
buildOptionTile( buildOptionTile(
optionIndex: 4, optionIndex: 4,
@@ -1387,8 +1370,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
title: dialogContext.l10n.community_scanQr, title: dialogContext.l10n.community_scanQr,
subtitle: dialogContext.l10n.community_join, subtitle: dialogContext.l10n.community_join,
), ),
if (selectedOption == 4) if (selectedOption == 4) buildExpandedContent()!,
buildExpandedContent(_channelMessageStore)!,
const Divider(height: 1), const Divider(height: 1),
buildOptionTile( buildOptionTile(
optionIndex: 5, optionIndex: 5,
@@ -1396,8 +1378,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
title: dialogContext.l10n.community_create, title: dialogContext.l10n.community_create,
subtitle: dialogContext.l10n.community_createDesc, subtitle: dialogContext.l10n.community_createDesc,
), ),
if (selectedOption == 5) if (selectedOption == 5) buildExpandedContent()!,
buildExpandedContent(_channelMessageStore)!,
], ],
), ),
), ),
@@ -1547,7 +1528,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
try { try {
await connector.deleteChannel(channel.index); await connector.deleteChannel(channel.index);
await channelMessageStore.clearChannelMessages(channel.index); channelMessageStore.clearChannelMessages(channel.index);
if (!context.mounted) return; if (!context.mounted) return;
@@ -1772,7 +1753,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
} }
final channelCount = communityChannels.length; final channelCount = communityChannels.length;
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
showDialog( showDialog(
context: context, context: context,
+30 -74
View File
@@ -106,9 +106,10 @@ class _ChatScreenState extends State<ChatScreen> {
final unreadLabel = context.l10n.chat_unread(unreadCount); final unreadLabel = context.l10n.chat_unread(unreadCount);
final pathLabel = _currentPathLabel(contact); final pathLabel = _currentPathLabel(contact);
// Show path details if we have non-empty path data (from device or override) // Show path details if we have path data (from device or override)
final hasPathData =
contact.path.isNotEmpty || contact.pathOverrideBytes != null;
final effectivePath = contact.pathOverrideBytes ?? contact.path; final effectivePath = contact.pathOverrideBytes ?? contact.path;
final hasPathData = effectivePath.isNotEmpty;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -142,25 +143,12 @@ class _ChatScreenState extends State<ChatScreen> {
final contact = _resolveContact(connector); final contact = _resolveContact(connector);
final isFloodMode = contact.pathOverride == -1; final isFloodMode = contact.pathOverride == -1;
final isDirectMode = contact.pathOverride == 0;
final activeMode = isFloodMode
? 'flood'
: isDirectMode
? 'direct'
: 'auto';
return PopupMenuButton<String>( return PopupMenuButton<String>(
icon: Icon(isFloodMode ? Icons.waves : Icons.route), icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: context.l10n.chat_routingMode, tooltip: context.l10n.chat_routingMode,
onSelected: (mode) async { onSelected: (mode) async {
if (mode == 'flood') { if (mode == 'flood') {
await connector.setPathOverride(contact, pathLen: -1); await connector.setPathOverride(contact, pathLen: -1);
} else if (mode == 'direct') {
await connector.setPathOverride(
contact,
pathLen: 0,
pathBytes: Uint8List(0),
);
} else { } else {
await connector.setPathOverride(contact, pathLen: null); await connector.setPathOverride(contact, pathLen: null);
} }
@@ -173,7 +161,7 @@ class _ChatScreenState extends State<ChatScreen> {
Icon( Icon(
Icons.auto_mode, Icons.auto_mode,
size: 20, size: 20,
color: activeMode == 'auto' color: !isFloodMode
? Theme.of(context).primaryColor ? Theme.of(context).primaryColor
: null, : null,
), ),
@@ -181,30 +169,7 @@ class _ChatScreenState extends State<ChatScreen> {
Text( Text(
context.l10n.chat_autoUseSavedPath, context.l10n.chat_autoUseSavedPath,
style: TextStyle( style: TextStyle(
fontWeight: activeMode == 'auto' fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'direct',
child: Row(
children: [
Icon(
Icons.near_me,
size: 20,
color: activeMode == 'direct'
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
context.l10n.chat_direct,
style: TextStyle(
fontWeight: activeMode == 'direct'
? FontWeight.bold ? FontWeight.bold
: FontWeight.normal, : FontWeight.normal,
), ),
@@ -219,7 +184,7 @@ class _ChatScreenState extends State<ChatScreen> {
Icon( Icon(
Icons.waves, Icons.waves,
size: 20, size: 20,
color: activeMode == 'flood' color: isFloodMode
? Theme.of(context).primaryColor ? Theme.of(context).primaryColor
: null, : null,
), ),
@@ -227,7 +192,7 @@ class _ChatScreenState extends State<ChatScreen> {
Text( Text(
context.l10n.chat_forceFloodMode, context.l10n.chat_forceFloodMode,
style: TextStyle( style: TextStyle(
fontWeight: activeMode == 'flood' fontWeight: isFloodMode
? FontWeight.bold ? FontWeight.bold
: FontWeight.normal, : FontWeight.normal,
), ),
@@ -286,9 +251,7 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
context.l10n.chat_sendMessageTo( context.l10n.chat_sendMessageTo(widget.contact.name),
_resolveContact(context.read<MeshCoreConnector>()).name,
),
style: TextStyle(fontSize: 14, color: Colors.grey[500]), style: TextStyle(fontSize: 14, color: Colors.grey[500]),
), ),
], ],
@@ -306,7 +269,6 @@ class _ChatScreenState extends State<ChatScreen> {
// Auto-scroll to bottom if user is already at bottom // Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_scrollController.scrollToBottomIfAtBottom(); _scrollController.scrollToBottomIfAtBottom();
}); });
@@ -331,10 +293,10 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
final messageIndex = index; final messageIndex = index;
Contact contact = _resolveContact(connector); Contact contact = widget.contact;
final message = reversedMessages[messageIndex]; final message = reversedMessages[messageIndex];
String fourByteHex = ''; String fourByteHex = '';
if (contact.type == advTypeRoom) { if (widget.contact.type == advTypeRoom) {
contact = _resolveContactFrom4Bytes( contact = _resolveContactFrom4Bytes(
connector, connector,
message.fourByteRoomContactKey.isEmpty message.fourByteRoomContactKey.isEmpty
@@ -352,13 +314,12 @@ class _ChatScreenState extends State<ChatScreen> {
final textScale = context.select<ChatTextScaleService, double>( final textScale = context.select<ChatTextScaleService, double>(
(service) => service.scale, (service) => service.scale,
); );
final resolvedContact = _resolveContact(connector);
return _MessageBubble( return _MessageBubble(
message: message, message: message,
senderName: resolvedContact.type == advTypeRoom senderName: widget.contact.type == advTypeRoom
? "${contact.name} [$fourByteHex]" ? "${contact.name} [$fourByteHex]"
: contact.name, : contact.name,
isRoomServer: resolvedContact.type == advTypeRoom, isRoomServer: widget.contact.type == advTypeRoom,
textScale: textScale, textScale: textScale,
onTap: () => _openMessagePath(message, contact), onTap: () => _openMessagePath(message, contact),
onLongPress: () => _showMessageActions(message, contact), onLongPress: () => _showMessageActions(message, contact),
@@ -496,7 +457,7 @@ class _ChatScreenState extends State<ChatScreen> {
return; return;
} }
connector.sendMessage(_resolveContact(connector), text); connector.sendMessage(widget.contact, text);
_textController.clear(); _textController.clear();
_textFieldFocusNode.requestFocus(); _textFieldFocusNode.requestFocus();
} }
@@ -693,7 +654,7 @@ class _ChatScreenState extends State<ChatScreen> {
// Set the path override to persist user's choice // Set the path override to persist user's choice
await connector.setPathOverride( await connector.setPathOverride(
_resolveContact(connector), widget.contact,
pathLen: pathLength, pathLen: pathLength,
pathBytes: pathBytes, pathBytes: pathBytes,
); );
@@ -702,7 +663,7 @@ class _ChatScreenState extends State<ChatScreen> {
Navigator.pop(context); Navigator.pop(context);
await _notifyPathSet( await _notifyPathSet(
connector, connector,
_resolveContact(connector), widget.contact,
pathBytes, pathBytes,
path.hopCount, path.hopCount,
); );
@@ -761,9 +722,7 @@ class _ChatScreenState extends State<ChatScreen> {
style: const TextStyle(fontSize: 11), style: const TextStyle(fontSize: 11),
), ),
onTap: () async { onTap: () async {
await connector.clearContactPath( await connector.clearContactPath(widget.contact);
_resolveContact(connector),
);
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@@ -791,7 +750,7 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
onTap: () async { onTap: () async {
await connector.setPathOverride( await connector.setPathOverride(
_resolveContact(connector), widget.contact,
pathLen: -1, pathLen: -1,
); );
if (!context.mounted) return; if (!context.mounted) return;
@@ -1046,7 +1005,11 @@ class _ChatScreenState extends State<ChatScreen> {
); );
if (result == null) { if (result == null) {
return; // Cancelled keep existing path appLogger.info(
'PathSelectionDialog was cancelled or returned null',
tag: 'ChatScreen',
);
return;
} }
if (!mounted) { if (!mounted) {
@@ -1062,19 +1025,14 @@ class _ChatScreenState extends State<ChatScreen> {
tag: 'ChatScreen', tag: 'ChatScreen',
); );
await connector.setPathOverride( await connector.setPathOverride(
_resolveContact(connector), widget.contact,
pathLen: result.length, pathLen: result.length,
pathBytes: result, pathBytes: result,
); );
appLogger.info('setPathOverride completed', tag: 'ChatScreen'); appLogger.info('setPathOverride completed', tag: 'ChatScreen');
if (!mounted) return; if (!mounted) return;
await _notifyPathSet( await _notifyPathSet(connector, widget.contact, result, result.length);
connector,
_resolveContact(connector),
result,
result.length,
);
} }
void _openMessagePath(Message message, Contact contact) { void _openMessagePath(Message message, Contact contact) {
@@ -1086,10 +1044,10 @@ class _ChatScreenState extends State<ChatScreen> {
final String senderName; final String senderName;
if (message.isOutgoing) { if (message.isOutgoing) {
senderName = connector.selfName ?? context.l10n.chat_me; senderName = connector.selfName ?? context.l10n.chat_me;
} else if (_resolveContact(connector).type == advTypeRoom) { } else if (widget.contact.type == advTypeRoom) {
senderName = "${contact.name} [$fourByteHex]"; senderName = "${contact.name} [$fourByteHex]";
} else { } else {
senderName = _resolveContact(connector).name; senderName = widget.contact.name;
} }
final pathMessage = ChannelMessage( final pathMessage = ChannelMessage(
senderKey: null, senderKey: null,
@@ -1152,8 +1110,7 @@ class _ChatScreenState extends State<ChatScreen> {
_retryMessage(message); _retryMessage(message);
}, },
), ),
if (_resolveContact(context.read<MeshCoreConnector>()).type == if (widget.contact.type == advTypeRoom)
advTypeRoom)
ListTile( ListTile(
leading: const Icon(Icons.chat), leading: const Icon(Icons.chat),
title: Text(context.l10n.contacts_openChat), title: Text(context.l10n.contacts_openChat),
@@ -1191,7 +1148,7 @@ class _ChatScreenState extends State<ChatScreen> {
void _retryMessage(Message message) { void _retryMessage(Message message) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
// Retry using the contact's current path override setting // Retry using the contact's current path override setting
connector.sendMessage(_resolveContact(connector), message.text); connector.sendMessage(widget.contact, message.text);
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage))); ).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage)));
@@ -1217,8 +1174,7 @@ class _ChatScreenState extends State<ChatScreen> {
// For room servers, include sender name (like channels) since multiple users // For room servers, include sender name (like channels) since multiple users
// For 1:1 chats, sender is implicit (null) // For 1:1 chats, sender is implicit (null)
final liveContact = _resolveContact(connector); final senderName = widget.contact.type == advTypeRoom
final senderName = liveContact.type == advTypeRoom
? senderContact.name ? senderContact.name
: null; : null;
final hash = ReactionHelper.computeReactionHash( final hash = ReactionHelper.computeReactionHash(
@@ -1227,7 +1183,7 @@ class _ChatScreenState extends State<ChatScreen> {
message.text, message.text,
); );
final reactionText = 'r:$hash:$emojiIndex'; final reactionText = 'r:$hash:$emojiIndex';
connector.sendMessage(_resolveContact(connector), reactionText); connector.sendMessage(widget.contact, reactionText);
} }
} }
-8
View File
@@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:meshcore_open/services/notification_service.dart';
import 'package:meshcore_open/utils/app_logger.dart'; import 'package:meshcore_open/utils/app_logger.dart';
import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -65,13 +64,6 @@ class _ContactsScreenState extends State<ContactsScreen>
super.initState(); super.initState();
_loadGroups(); _loadGroups();
_setupFrameListener(); _setupFrameListener();
_clearAdvertNotifications();
}
void _clearAdvertNotifications() {
final connector = context.read<MeshCoreConnector>();
final contactIds = connector.contacts.map((c) => c.publicKeyHex).toList();
NotificationService().clearAdvertNotifications(contactIds);
} }
@override @override
+7 -8
View File
@@ -7,7 +7,7 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/discovery_contact.dart';
import '../utils/contact_search.dart'; import '../utils/contact_search.dart';
import '../widgets/app_bar.dart'; import '../widgets/app_bar.dart';
import '../widgets/list_filter_widget.dart'; import '../widgets/list_filter_widget.dart';
@@ -129,7 +129,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
} }
Future<void> _showContactContextMenu( Future<void> _showContactContextMenu(
Contact contact, DiscoveryContact contact,
MeshCoreConnector connector, MeshCoreConnector connector,
) async { ) async {
final action = await showModalBottomSheet<String>( final action = await showModalBottomSheet<String>(
@@ -169,8 +169,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
connector.importDiscoveredContact(contact); connector.importDiscoveredContact(contact);
break; break;
case 'copy_contact': case 'copy_contact':
if (contact.rawPacket == null) return; final hexString = pubKeyToHex(contact.rawPacket);
final hexString = pubKeyToHex(contact.rawPacket!);
Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -208,7 +207,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
} }
Widget _buildFilters( Widget _buildFilters(
List<Contact> filteredAndSorted, List<DiscoveryContact> filteredAndSorted,
MeshCoreConnector connector, MeshCoreConnector connector,
) { ) {
String hintText = ""; String hintText = "";
@@ -310,8 +309,8 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
); );
} }
List<Contact> _filterAndSortContacts( List<DiscoveryContact> _filterAndSortContacts(
List<Contact> contacts, List<DiscoveryContact> contacts,
MeshCoreConnector connector, MeshCoreConnector connector,
) { ) {
var filtered = contacts.where((contact) { var filtered = contacts.where((contact) {
@@ -351,7 +350,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
return filtered; return filtered;
} }
bool _matchesTypeFilter(Contact contact) { bool _matchesTypeFilter(DiscoveryContact contact) {
switch (typeFilter) { switch (typeFilter) {
case ContactTypeFilter.all: case ContactTypeFilter.all:
return true; return true;
+39 -137
View File
@@ -1,7 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@@ -51,8 +50,7 @@ class MapScreen extends StatefulWidget {
} }
class _MapScreenState extends State<MapScreen> { class _MapScreenState extends State<MapScreen> {
// Zoom level at which node labels start to appear static const double _labelZoomThreshold = 8.5;
static const double _labelZoomThreshold = 12.0;
final MapController _mapController = MapController(); final MapController _mapController = MapController();
final MapMarkerService _markerService = MapMarkerService(); final MapMarkerService _markerService = MapMarkerService();
@@ -93,15 +91,6 @@ class _MapScreenState extends State<MapScreen> {
}); });
} }
bool _checkLocationPlausibility(double lat, double lon) {
const double epsilon = 1e-6;
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
lat >= -90.0 &&
lat <= 90.0 &&
lon >= -180.0 &&
lon <= 180.0;
}
double _standardDeviation(List<double> values) { double _standardDeviation(List<double> values) {
if (values.length <= 1) { if (values.length <= 1) {
return 0.0; return 0.0;
@@ -137,15 +126,7 @@ class _MapScreenState extends State<MapScreen> {
builder: (context, connector, settingsService, pathHistory, child) { builder: (context, connector, settingsService, pathHistory, child) {
final tileCache = context.read<MapTileCacheService>(); final tileCache = context.read<MapTileCacheService>();
final settings = settingsService.settings; final settings = settingsService.settings;
final allContacts = <Contact>[ final contacts = connector.contacts;
...connector.contacts,
...connector.discoveredContacts.where((c) => !c.isActive),
];
final contacts = settings.mapShowDiscoveryContacts
? allContacts
: allContacts.where((c) => c.isActive).toList();
final highlightPosition = widget.highlightPosition; final highlightPosition = widget.highlightPosition;
final sharedMarkers = settings.mapShowMarkers final sharedMarkers = settings.mapShowMarkers
? _collectSharedMarkers(connector) ? _collectSharedMarkers(connector)
@@ -178,21 +159,14 @@ class _MapScreenState extends State<MapScreen> {
: filteredByTime; : filteredByTime;
// Filter by location // Filter by location
final contactsWithLocation = filteredByKeyPrefix.where((c) { final contactsWithLocation = filteredByKeyPrefix
if (!c.hasLocation) { .where((c) => c.hasLocation)
return false; .toList();
}
return _checkLocationPlausibility(c.latitude!, c.longitude!);
}).toList();
// All contacts with a known location used as anchors regardless of // All contacts with a known location used as anchors regardless of
// time/key-prefix filters so that repeaters are always available. // time/key-prefix filters so that repeaters are always available.
final allContactsWithLocation = allContacts final allContactsWithLocation = contacts
.where( .where((c) => c.hasLocation)
(c) =>
c.hasLocation &&
_checkLocationPlausibility(c.latitude!, c.longitude!),
)
.toList(); .toList();
// Compute guessed locations with caching // Compute guessed locations with caching
@@ -494,10 +468,7 @@ class _MapScreenState extends State<MapScreen> {
), ),
), ),
if (!_isBuildingPathTrace) if (!_isBuildingPathTrace)
..._buildGuessedMarker( ...guessedLocations.map(_buildGuessedMarker),
guessedLocations,
showLabels: _showNodeLabels,
),
..._buildMarkers( ..._buildMarkers(
contactsWithLocation, contactsWithLocation,
settings, settings,
@@ -659,13 +630,6 @@ class _MapScreenState extends State<MapScreen> {
anchors[0].latitude + offsetDeg * cos(angle), anchors[0].latitude + offsetDeg * cos(angle),
anchors[0].longitude + offsetDeg * sin(angle), anchors[0].longitude + offsetDeg * sin(angle),
); );
if (!_checkLocationPlausibility(
position.latitude,
position.longitude,
)) {
continue; // discard implausible guesses near (0, 0)
}
} else { } else {
double lat = 0, lon = 0; double lat = 0, lon = 0;
for (final a in anchors) { for (final a in anchors) {
@@ -673,12 +637,6 @@ class _MapScreenState extends State<MapScreen> {
lon += a.longitude; lon += a.longitude;
} }
position = LatLng(lat / anchors.length, lon / anchors.length); position = LatLng(lat / anchors.length, lon / anchors.length);
if (!_checkLocationPlausibility(
position.latitude,
position.longitude,
)) {
continue; // discard implausible guesses near (0, 0
}
} }
result.add( result.add(
_GuessedLocation( _GuessedLocation(
@@ -752,61 +710,40 @@ class _MapScreenState extends State<MapScreen> {
.toList(); .toList();
} }
List<Marker> _buildGuessedMarker( Marker _buildGuessedMarker(_GuessedLocation guess) {
List<_GuessedLocation> guessed, { final color = _getNodeColor(guess.contact.type);
required bool showLabels, return Marker(
}) { point: guess.position,
final markers = <Marker>[]; width: 35,
height: 35,
for (final guess in guessed) { child: GestureDetector(
final color = _getNodeColor(guess.contact.type); onTap: () => _showNodeInfo(
final marker = Marker( context,
point: guess.position, guess.contact,
width: 35, guessedPosition: guess.position,
height: 35, ),
child: GestureDetector( child: Container(
onTap: () => _showNodeInfo( padding: const EdgeInsets.all(4),
context, decoration: BoxDecoration(
guess.contact, color: color.withValues(alpha: guess.highConfidence ? 0.55 : 0.30),
guessedPosition: guess.position, shape: BoxShape.circle,
), border: Border.all(color: Colors.white, width: 2),
child: Container( boxShadow: [
padding: const EdgeInsets.all(4), BoxShadow(
decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.3),
color: color.withValues( blurRadius: 4,
alpha: guess.highConfidence ? 0.55 : 0.30, offset: const Offset(0, 2),
), ),
shape: BoxShape.circle, ],
border: Border.all(color: Colors.white, width: 2), ),
boxShadow: [ child: const Icon(
BoxShadow( Icons.not_listed_location,
color: Colors.black.withValues(alpha: 0.3), color: Colors.white,
blurRadius: 4, size: 20,
offset: const Offset(0, 2),
),
],
),
child: const Icon(
Icons.not_listed_location,
color: Colors.white,
size: 20,
),
), ),
), ),
); ),
);
markers.add(marker);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: guess.position,
label: guess.contact.name,
),
);
}
}
return markers;
} }
List<Marker> _buildMarkers( List<Marker> _buildMarkers(
@@ -1266,7 +1203,6 @@ class _MapScreenState extends State<MapScreen> {
Contact contact, { Contact contact, {
LatLng? guessedPosition, LatLng? guessedPosition,
}) { }) {
final connector = context.read<MeshCoreConnector>();
showDialog( showDialog(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
@@ -1312,9 +1248,6 @@ class _MapScreenState extends State<MapScreen> {
advTypeChat) // Only show chat button for chat nodes advTypeChat) // Only show chat button for chat nodes
TextButton( TextButton(
onPressed: () { onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
Navigator.push( Navigator.push(
context, context,
@@ -1328,9 +1261,6 @@ class _MapScreenState extends State<MapScreen> {
if (contact.type == advTypeRepeater) if (contact.type == advTypeRepeater)
TextButton( TextButton(
onPressed: () { onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
_showRepeaterLogin(context, contact); _showRepeaterLogin(context, contact);
}, },
@@ -1339,9 +1269,6 @@ class _MapScreenState extends State<MapScreen> {
if (contact.type == advTypeRoom) if (contact.type == advTypeRoom)
TextButton( TextButton(
onPressed: () { onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
_showRoomLogin(context, contact); _showRoomLogin(context, contact);
}, },
@@ -1509,23 +1436,6 @@ class _MapScreenState extends State<MapScreen> {
); );
}, },
), ),
ListTile(
leading: const Icon(Icons.my_location),
title: Text(context.l10n.map_setAsMyLocation),
onTap: () async {
final messenger = ScaffoldMessenger.of(context);
final successMsg = context.l10n.settings_locationUpdated;
Navigator.pop(sheetContext);
if (!connector.isConnected) return;
await connector.setNodeLocation(
lat: position.latitude,
lon: position.longitude,
);
await connector.refreshDeviceInfo();
if (!mounted) return;
messenger.showSnackBar(SnackBar(content: Text(successMsg)));
},
),
ListTile( ListTile(
leading: const Icon(Icons.close), leading: const Icon(Icons.close),
title: Text(context.l10n.common_cancel), title: Text(context.l10n.common_cancel),
@@ -1835,14 +1745,6 @@ class _MapScreenState extends State<MapScreen> {
}, },
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
), ),
CheckboxListTile(
title: Text(context.l10n.map_showDiscoveryContacts),
value: settings.mapShowDiscoveryContacts,
onChanged: (value) {
service.setMapShowDiscoveryContacts(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
context.l10n.map_keyPrefix, context.l10n.map_keyPrefix,
+3 -5
View File
@@ -124,14 +124,12 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) { void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
final buffer = BufferReader(frame); final buffer = BufferReader(frame);
final contacts = <Contact>[
...connector.contacts,
...connector.discoveredContacts,
];
try { try {
final neighborCount = buffer.readUInt16LE(); final neighborCount = buffer.readUInt16LE();
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
contacts.where((c) => c.type == advTypeRepeater).forEach((repeater) { connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
repeater,
) {
for (var neighborData in parsedNeighbors) { for (var neighborData in parsedNeighbors) {
final publicKey = neighborData['publicKey']; final publicKey = neighborData['publicKey'];
if (listEquals( if (listEquals(
+13 -42
View File
@@ -114,37 +114,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
super.dispose(); super.dispose();
} }
Uint8List buildPath(Uint8List pathBytes) { Uint8List addReturnPath(Uint8List pathBytes) {
Uint8List traceBytes; Uint8List? traceBytes;
final len = (pathBytes.length + pathBytes.length - 1);
if (pathBytes.isEmpty) { traceBytes = Uint8List(len);
traceBytes = Uint8List(1); for (int i = 0; i < pathBytes.length; i++) {
traceBytes[0] = widget.targetContact?.publicKey[0] ?? 0; traceBytes[i] = pathBytes[i];
return traceBytes; if (i < pathBytes.length - 1) {
} traceBytes[len - 1 - i] = pathBytes[i];
if (widget.targetContact?.type == advTypeRepeater ||
widget.targetContact?.type == advTypeRoom) {
final len = (pathBytes.length + pathBytes.length + 1);
traceBytes = Uint8List(len);
traceBytes[pathBytes.length] = widget.targetContact?.publicKey[0] ?? 0;
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length) {
traceBytes[len - 1 - i] = pathBytes[i];
}
}
} else {
if (pathBytes.length < 2) {
return pathBytes[0] == 0 ? Uint8List(0) : pathBytes;
}
final len = (pathBytes.length + pathBytes.length - 1);
traceBytes = Uint8List(len);
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length - 1) {
traceBytes[len - 1 - i] = pathBytes[i];
}
} }
} }
return traceBytes; return traceBytes;
@@ -165,16 +142,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
: widget.path; : widget.path;
if (widget.flipPathRound) { if (widget.flipPathRound) {
path = buildPath(pathTmp); path = addReturnPath(pathTmp);
} else { } else {
path = pathTmp; path = pathTmp;
} }
appLogger.info(
'Initiating path trace with path: ${_formatPathPrefixes(path)}',
tag: 'PathTraceMapScreen',
);
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final frame = buildTraceReq( final frame = buildTraceReq(
DateTime.now().millisecondsSinceEpoch ~/ 1000, DateTime.now().millisecondsSinceEpoch ~/ 1000,
@@ -263,11 +235,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toList(); .toList();
Map<int, Contact> pathContacts = {}; Map<int, Contact> pathContacts = {};
final contacts = <Contact>[
...connector.contacts, connector.contacts.where((c) => c.type != advTypeChat).forEach((
...connector.discoveredContacts, repeater,
]; ) {
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
for (var repeaterData in pathData) { for (var repeaterData in pathData) {
if (listEquals( if (listEquals(
repeater.publicKey.sublist(0, 1), repeater.publicKey.sublist(0, 1),
+49 -67
View File
@@ -10,7 +10,6 @@ import '../utils/app_logger.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart'; import '../widgets/device_tile.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import 'tcp_screen.dart';
import 'usb_screen.dart'; import 'usb_screen.dart';
/// Screen for scanning and connecting to MeshCore devices /// Screen for scanning and connecting to MeshCore devices
@@ -126,78 +125,61 @@ class _ScannerScreenState extends State<ScannerScreen> {
connector.state == MeshCoreConnectionState.scanning; connector.state == MeshCoreConnectionState.scanning;
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off; final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
final usbSupported = PlatformInfo.supportsUsbSerial; final usbSupported = PlatformInfo.supportsUsbSerial;
final tcpSupported = !PlatformInfo.isWeb;
return SafeArea( return SafeArea(
top: false, top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: FittedBox( child: Row(
fit: BoxFit.scaleDown, mainAxisAlignment: MainAxisAlignment.end,
alignment: Alignment.centerRight, children: [
child: Row( if (usbSupported)
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (usbSupported)
FloatingActionButton.extended(
onPressed: () {
appLogger.info(
'USB selected, opening UsbScreen',
tag: 'ScannerScreen',
);
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const UsbScreen()),
);
},
heroTag: 'scanner_usb_action',
icon: const Icon(Icons.usb),
label: Text(context.l10n.connectionChoiceUsbLabel),
),
if (usbSupported) const SizedBox(width: 12),
if (tcpSupported)
FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const TcpScreen()),
);
},
heroTag: 'scanner_tcp_action',
icon: const Icon(Icons.lan),
label: Text(context.l10n.connectionChoiceTcpLabel),
),
if (tcpSupported) const SizedBox(width: 12),
FloatingActionButton.extended( FloatingActionButton.extended(
heroTag: 'scanner_ble_action', onPressed: () {
onPressed: isBluetoothOff appLogger.info(
? null 'USB selected, opening UsbScreen',
: () { tag: 'ScannerScreen',
if (isScanning) { );
connector.stopScan(); Navigator.of(context).push(
} else { MaterialPageRoute(builder: (_) => const UsbScreen()),
unawaited( );
connector.startScan().catchError((e) { },
appLogger.warn( heroTag: 'scanner_usb_action',
'startScan error: $e', icon: const Icon(Icons.usb),
tag: 'ScannerScreen', label: Text(context.l10n.connectionChoiceUsbLabel),
);
}),
);
}
},
icon: isScanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.bluetooth_searching),
label: Text(
isScanning
? context.l10n.scanner_stop
: context.l10n.scanner_scan,
),
), ),
], if (usbSupported) const SizedBox(width: 12),
), FloatingActionButton.extended(
heroTag: 'scanner_ble_action',
onPressed: isBluetoothOff
? null
: () {
if (isScanning) {
connector.stopScan();
} else {
unawaited(
connector.startScan().catchError((e) {
appLogger.warn(
'startScan error: $e',
tag: 'ScannerScreen',
);
}),
);
}
},
icon: isScanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.bluetooth_searching),
label: Text(
isScanning
? context.l10n.scanner_stop
: context.l10n.scanner_scan,
),
),
],
), ),
); );
}, },
-282
View File
@@ -1,282 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../utils/platform_info.dart';
import '../widgets/adaptive_app_bar_title.dart';
import 'contacts_screen.dart';
import 'usb_screen.dart';
class TcpScreen extends StatefulWidget {
const TcpScreen({super.key});
@override
State<TcpScreen> createState() => _TcpScreenState();
}
class _TcpScreenState extends State<TcpScreen> {
late final TextEditingController _hostController;
late final TextEditingController _portController;
late final MeshCoreConnector _connector;
late final VoidCallback _connectionListener;
bool _navigatedToContacts = false;
@override
void initState() {
super.initState();
_hostController = TextEditingController();
_portController = TextEditingController(text: '5000');
_connector = context.read<MeshCoreConnector>();
_connectionListener = () {
if (!mounted) return;
if (_connector.state == MeshCoreConnectionState.disconnected) {
_navigatedToContacts = false;
}
if (_connector.state == MeshCoreConnectionState.connected &&
_connector.isTcpTransportConnected &&
!_navigatedToContacts) {
_navigatedToContacts = true;
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const ContactsScreen()),
);
}
};
_connector.addListener(_connectionListener);
}
@override
void dispose() {
_hostController.dispose();
_portController.dispose();
_connector.removeListener(_connectionListener);
if (!_navigatedToContacts &&
_connector.activeTransport == MeshCoreTransportType.tcp &&
_connector.state != MeshCoreConnectionState.disconnected) {
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_connector.disconnect(manual: true));
});
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).maybePop(),
),
title: AdaptiveAppBarTitle(context.l10n.tcpScreenTitle),
centerTitle: true,
),
body: SafeArea(
top: false,
child: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
final isConnecting =
connector.state == MeshCoreConnectionState.connecting &&
connector.activeTransport == MeshCoreTransportType.tcp;
final isButtonDisabled =
isConnecting ||
connector.state == MeshCoreConnectionState.scanning;
return Column(
children: [
_buildStatusBar(context, connector),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _hostController,
decoration: InputDecoration(
labelText: context.l10n.tcpHostLabel,
hintText: context.l10n.tcpHostHint,
border: const OutlineInputBorder(),
),
enabled: !isConnecting,
keyboardType: TextInputType.url,
),
const SizedBox(height: 12),
TextField(
controller: _portController,
decoration: InputDecoration(
labelText: context.l10n.tcpPortLabel,
hintText: context.l10n.tcpPortHint,
border: const OutlineInputBorder(),
),
enabled: !isConnecting,
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
FilledButton.icon(
key: const Key('tcp_connect_button'),
onPressed: isButtonDisabled ? null : _connectTcp,
icon: isConnecting
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.lan),
label: Text(
isConnecting
? context.l10n.scanner_connecting
: context.l10n.common_connect,
),
),
],
),
),
],
);
},
),
),
bottomNavigationBar: SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (PlatformInfo.supportsUsbSerial)
FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const UsbScreen()),
);
},
heroTag: 'tcp_usb_action',
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
icon: const Icon(Icons.usb),
label: Text(context.l10n.connectionChoiceUsbLabel),
),
if (PlatformInfo.supportsUsbSerial) const SizedBox(width: 12),
FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).maybePop();
},
heroTag: 'tcp_ble_action',
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
icon: const Icon(Icons.bluetooth),
label: Text(context.l10n.connectionChoiceBluetoothLabel),
),
],
),
),
),
);
}
Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
String statusText;
Color statusColor;
if (connector.isTcpTransportConnected) {
statusText = l10n.scanner_connectedTo(
connector.activeTcpEndpoint ?? 'TCP',
);
statusColor = Colors.green;
} else if (connector.state == MeshCoreConnectionState.connecting &&
connector.activeTransport == MeshCoreTransportType.tcp) {
statusText = l10n.tcpStatus_connectingTo(
'${_hostController.text}:${_portController.text}',
);
statusColor = Colors.orange;
} else if (connector.state == MeshCoreConnectionState.disconnecting &&
connector.activeTransport == MeshCoreTransportType.tcp) {
statusText = l10n.scanner_disconnecting;
statusColor = Colors.orange;
} else {
statusText = l10n.tcpStatus_notConnected;
statusColor = Colors.grey;
}
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
color: statusColor.withValues(alpha: 0.1),
child: Row(
children: [
Icon(Icons.circle, size: 12, color: statusColor),
const SizedBox(width: 8),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
statusText,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
);
}
Future<void> _connectTcp() async {
if (_connector.state == MeshCoreConnectionState.connecting ||
_connector.state == MeshCoreConnectionState.connected ||
_connector.state == MeshCoreConnectionState.disconnecting) {
return;
}
final host = _hostController.text.trim();
final parsedPort = int.tryParse(_portController.text.trim());
if (host.isEmpty) {
_showError(context.l10n.tcpErrorHostRequired);
return;
}
if (parsedPort == null || parsedPort < 1 || parsedPort > 65535) {
_showError(context.l10n.tcpErrorPortInvalid);
return;
}
try {
await _connector.connectTcp(host: host, port: parsedPort);
} catch (error) {
if (!mounted) return;
_showError(_friendlyErrorMessage(error));
}
}
void _showError(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red),
);
}
String _friendlyErrorMessage(Object error) {
if (error is UnsupportedError) {
return context.l10n.tcpErrorUnsupported;
}
if (error is TimeoutException) {
return context.l10n.tcpErrorTimedOut;
}
if (error is StateError) {
return context.l10n.tcpConnectionFailed(error.message);
}
if (error is ArgumentError) {
return context.l10n.tcpConnectionFailed(
error.message?.toString() ?? error.toString(),
);
}
return context.l10n.tcpConnectionFailed(error.toString());
}
}
+38 -72
View File
@@ -12,7 +12,6 @@ import '../utils/usb_port_labels.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import 'scanner_screen.dart'; import 'scanner_screen.dart';
import 'tcp_screen.dart';
class UsbScreen extends StatefulWidget { class UsbScreen extends StatefulWidget {
const UsbScreen({super.key}); const UsbScreen({super.key});
@@ -108,69 +107,45 @@ class _UsbScreenState extends State<UsbScreen> {
bottomNavigationBar: Consumer<MeshCoreConnector>( bottomNavigationBar: Consumer<MeshCoreConnector>(
builder: (context, connector, child) { builder: (context, connector, child) {
final isLoading = _isLoadingPorts; final isLoading = _isLoadingPorts;
final showBle = true; final showBle =
final showTcp = !PlatformInfo.isWeb; PlatformInfo.isWeb ||
PlatformInfo.isAndroid ||
PlatformInfo.isIOS;
return SafeArea( return SafeArea(
top: false, top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: FittedBox( child: Row(
fit: BoxFit.scaleDown, mainAxisAlignment: MainAxisAlignment.end,
alignment: Alignment.centerRight, children: [
child: Row( if (showBle)
mainAxisAlignment: MainAxisAlignment.end, FloatingActionButton.extended(
children: [ onPressed: () {
if (showTcp) Navigator.of(context).pushReplacement(
FloatingActionButton.extended( MaterialPageRoute(
onPressed: () { builder: (_) => const ScannerScreen(),
Navigator.of(context).pushReplacement( ),
MaterialPageRoute(builder: (_) => const TcpScreen()), );
); },
}, heroTag: 'usb_ble_action',
heroTag: 'usb_tcp_action', icon: const Icon(Icons.bluetooth),
extendedPadding: const EdgeInsets.symmetric( label: Text(context.l10n.connectionChoiceBluetoothLabel),
horizontal: 12, ),
), if (showBle) const SizedBox(width: 12),
icon: const Icon(Icons.lan), if (!_supportsHotPlug)
label: Text(context.l10n.connectionChoiceTcpLabel), FloatingActionButton.extended(
), onPressed: isLoading ? null : _loadPorts,
if (showTcp && showBle) const SizedBox(width: 12), heroTag: 'usb_refresh_action',
if (showBle) icon: isLoading
FloatingActionButton.extended( ? const SizedBox(
onPressed: () { width: 20,
Navigator.of(context).pushReplacement( height: 20,
MaterialPageRoute( child: CircularProgressIndicator(strokeWidth: 2),
builder: (_) => const ScannerScreen(), )
), : const Icon(Icons.refresh),
); label: Text(context.l10n.repeater_refresh),
}, ),
heroTag: 'usb_ble_action', ],
extendedPadding: const EdgeInsets.symmetric(
horizontal: 12,
),
icon: const Icon(Icons.bluetooth),
label: Text(context.l10n.connectionChoiceBluetoothLabel),
),
if ((showTcp || showBle) && !_supportsHotPlug)
const SizedBox(width: 12),
if (!_supportsHotPlug)
FloatingActionButton.extended(
onPressed: isLoading ? null : _loadPorts,
heroTag: 'usb_refresh_action',
extendedPadding: const EdgeInsets.symmetric(
horizontal: 12,
),
icon: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.usb),
label: Text(context.l10n.scanner_scan),
),
],
),
), ),
); );
}, },
@@ -217,18 +192,9 @@ class _UsbScreenState extends State<UsbScreen> {
children: [ children: [
Icon(Icons.circle, size: 12, color: statusColor), Icon(Icons.circle, size: 12, color: statusColor),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Text(
child: FittedBox( statusText,
fit: BoxFit.scaleDown, style: TextStyle(color: statusColor, fontWeight: FontWeight.w500),
alignment: Alignment.centerLeft,
child: Text(
statusText,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.w500,
),
),
),
), ),
], ],
), ),
-4
View File
@@ -134,10 +134,6 @@ class AppSettingsService extends ChangeNotifier {
appLogger.setEnabled(value); appLogger.setEnabled(value);
} }
Future<void> setMapShowDiscoveryContacts(bool value) async {
await updateSettings(_settings.copyWith(mapShowDiscoveryContacts: value));
}
Future<void> setBatteryChemistryForDevice( Future<void> setBatteryChemistryForDevice(
String deviceId, String deviceId,
String chemistry, String chemistry,
+37 -133
View File
@@ -44,12 +44,6 @@ class MessageRetryService extends ChangeNotifier {
[]; // Rolling buffer of recent ACK hashes []; // Rolling buffer of recent ACK hashes
final Map<String, List<String>> _pendingMessageQueuePerContact = final Map<String, List<String>> _pendingMessageQueuePerContact =
{}; // contactPubKeyHex FIFO queue of messageIds (DEPRECATED - will be removed) {}; // contactPubKeyHex FIFO queue of messageIds (DEPRECATED - will be removed)
final Map<String, List<String>> _sendQueue =
{}; // contactPubKeyHex ordered list of messageIds awaiting send
final Set<String> _activeMessages =
{}; // messageIds currently in-flight (sent/retrying)
final Set<String> _resolvedMessages =
{}; // messageIds already resolved (prevents double _onMessageResolved)
final Map<String, String> _expectedHashToMessageId = final Map<String, String> _expectedHashToMessageId =
{}; // expectedAckHashHex messageId (for matching RESP_CODE_SENT by hash) {}; // expectedAckHashHex messageId (for matching RESP_CODE_SENT by hash)
@@ -162,49 +156,7 @@ class MessageRetryService extends ChangeNotifier {
_addMessageCallback!(contact.publicKeyHex, message); _addMessageCallback!(contact.publicKeyHex, message);
} }
// Queue per contact only one message in-flight at a time to avoid await _attemptSend(messageId);
// overflowing the firmware's 8-entry expected_ack_table.
final contactKey = contact.publicKeyHex;
_sendQueue[contactKey] ??= [];
_sendQueue[contactKey]!.add(messageId);
if (!_activeMessages.any(
(id) => _pendingContacts[id]?.publicKeyHex == contactKey,
)) {
_sendNextForContact(contactKey);
}
}
void _sendNextForContact(String contactKey) {
final queue = _sendQueue[contactKey];
if (queue == null) return;
// Drain stale entries iteratively instead of recursing.
while (queue.isNotEmpty) {
final messageId = queue.removeAt(0);
if (_pendingMessages.containsKey(messageId)) {
_activeMessages.add(messageId);
_attemptSend(messageId).catchError((e) {
debugPrint('_attemptSend threw for $messageId: $e');
final msg = _pendingMessages[messageId];
if (msg != null) {
final failed = msg.copyWith(status: MessageStatus.failed);
_pendingMessages[messageId] = failed;
_updateMessageCallback?.call(failed);
}
_onMessageResolved(messageId, contactKey);
});
return;
}
// Message was cancelled/cleaned up while queued try next
}
}
void _onMessageResolved(String messageId, String contactKey) {
if (_resolvedMessages.contains(messageId)) return;
_resolvedMessages.add(messageId);
_activeMessages.remove(messageId);
_sendNextForContact(contactKey);
} }
Future<void> _attemptSend(String messageId) async { Future<void> _attemptSend(String messageId) async {
@@ -217,11 +169,13 @@ class MessageRetryService extends ChangeNotifier {
// Use the path that was captured when the message was first sent // Use the path that was captured when the message was first sent
if (_setContactPathCallback != null && _clearContactPathCallback != null) { if (_setContactPathCallback != null && _clearContactPathCallback != null) {
if (message.pathLength != null && message.pathLength! < 0) { if (message.pathLength != null && message.pathLength! < 0) {
// Flood mode - clear the path
debugPrint( debugPrint(
'Setting flood mode for retry attempt ${message.retryCount}', 'Setting flood mode for retry attempt ${message.retryCount}',
); );
await _clearContactPathCallback!(contact); _clearContactPathCallback!(contact);
} else if (message.pathLength != null && message.pathLength! >= 0) { } else if (message.pathLength != null && message.pathLength! >= 0) {
// Specific path (including direct neighbor with pathLength=0)
final pathStr = message.pathBytes.isEmpty final pathStr = message.pathBytes.isEmpty
? 'direct' ? 'direct'
: message.pathBytes : message.pathBytes
@@ -238,24 +192,6 @@ class MessageRetryService extends ChangeNotifier {
} }
} }
// Re-validate after async gap a timer or ACK could have resolved/retried
// this message while we were awaiting the path callback.
final currentMessage = _pendingMessages[messageId];
if (currentMessage == null || _resolvedMessages.contains(messageId)) {
debugPrint(
'_attemptSend: message $messageId resolved during path sync, aborting',
);
return;
}
// If the message was retried by a timer during our await, the retryCount
// will have advanced. Only proceed if it still matches the attempt we started.
if (currentMessage.retryCount != message.retryCount) {
debugPrint(
'_attemptSend: message $messageId retryCount changed during path sync, aborting',
);
return;
}
final attempt = message.retryCount.clamp(0, 3); final attempt = message.retryCount.clamp(0, 3);
final timestampSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000; final timestampSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000;
@@ -295,15 +231,6 @@ class MessageRetryService extends ChangeNotifier {
if (_sendMessageCallback != null) { if (_sendMessageCallback != null) {
_sendMessageCallback!(contact, message.text, attempt, timestampSeconds); _sendMessageCallback!(contact, message.text, attempt, timestampSeconds);
} else {
// No send callback message would be stuck forever. Fail it immediately.
debugPrint(
'_attemptSend: no sendMessageCallback, failing message $messageId',
);
final failedMessage = message.copyWith(status: MessageStatus.failed);
_pendingMessages[messageId] = failedMessage;
_updateMessageCallback?.call(failedMessage);
_onMessageResolved(messageId, contact.publicKeyHex);
} }
} }
@@ -354,7 +281,6 @@ class MessageRetryService extends ChangeNotifier {
} }
// FALLBACK: Old queue-based matching (for messages sent before hash computation was added) // FALLBACK: Old queue-based matching (for messages sent before hash computation was added)
// Only match within a single contact's queue to avoid cross-contact mismatches.
if (messageId == null && allowQueueFallback) { if (messageId == null && allowQueueFallback) {
_debugLogService?.warn( _debugLogService?.warn(
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue', 'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
@@ -364,16 +290,13 @@ class MessageRetryService extends ChangeNotifier {
'Hash-based match failed for $ackHashHex, falling back to queue-based matching', 'Hash-based match failed for $ackHashHex, falling back to queue-based matching',
); );
// Search all contact queues so concurrent chats don't miss matches. for (var entry in _pendingMessageQueuePerContact.entries) {
final queuesToSearch = _pendingMessageQueuePerContact;
for (var entry in queuesToSearch.entries) {
final contactKey = entry.key; final contactKey = entry.key;
final queue = entry.value; final queue = entry.value;
// Drain stale entries until we find a valid one or exhaust the queue. if (queue.isNotEmpty) {
while (queue.isNotEmpty) {
final candidateMessageId = queue.removeAt(0); final candidateMessageId = queue.removeAt(0);
if (_pendingMessages.containsKey(candidateMessageId)) { if (_pendingMessages.containsKey(candidateMessageId)) {
messageId = candidateMessageId; messageId = candidateMessageId;
contact = _pendingContacts[candidateMessageId]; contact = _pendingContacts[candidateMessageId];
@@ -381,10 +304,21 @@ class MessageRetryService extends ChangeNotifier {
'Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey', 'Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey',
); );
break; break;
} else {
debugPrint('Dequeued stale message $candidateMessageId - skipping');
if (queue.isNotEmpty) {
final nextMessageId = queue.removeAt(0);
if (_pendingMessages.containsKey(nextMessageId)) {
messageId = nextMessageId;
contact = _pendingContacts[nextMessageId];
debugPrint(
'Queue-based match (fallback): $ackHashHex → message $messageId',
);
break;
}
}
} }
debugPrint('Dequeued stale message $candidateMessageId - skipping');
} }
if (messageId != null) break;
} }
} }
@@ -529,7 +463,22 @@ class MessageRetryService extends ChangeNotifier {
} else { } else {
// Max retries reached - mark as failed // Max retries reached - mark as failed
final failedMessage = message.copyWith(status: MessageStatus.failed); final failedMessage = message.copyWith(status: MessageStatus.failed);
_pendingMessages[messageId] = failedMessage;
// Move ACK hashes to history before removing
_moveAckHashesToHistory(messageId);
_pendingMessages.remove(messageId);
_pendingContacts.remove(messageId);
_pendingPathSelections.remove(messageId);
_timeoutTimers[messageId]?.cancel();
_timeoutTimers.remove(messageId);
// Clean up the queue entry for this contact
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
false) {
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
}
// Check if we should clear the path on max retry // Check if we should clear the path on max retry
if (_appSettingsService?.settings.clearPathOnMaxRetry == true && if (_appSettingsService?.settings.clearPathOnMaxRetry == true &&
@@ -550,30 +499,6 @@ class MessageRetryService extends ChangeNotifier {
} }
notifyListeners(); notifyListeners();
// Message is done retrying send next queued message for this contact
_onMessageResolved(messageId, contact.publicKeyHex);
// Keep message in pending maps for 30s grace period so late ACKs
// can still match and update the message to delivered.
_timeoutTimers[messageId] = Timer(const Duration(seconds: 30), () {
_moveAckHashesToHistory(messageId);
// Clean up ALL hash mappings for this message
_ackHashToMessageId.removeWhere(
(_, mapping) => mapping.messageId == messageId,
);
_expectedHashToMessageId.removeWhere((_, msgId) => msgId == messageId);
_pendingMessages.remove(messageId);
_pendingContacts.remove(messageId);
_pendingPathSelections.remove(messageId);
_timeoutTimers.remove(messageId);
_resolvedMessages.remove(messageId);
final contactKey = contact.publicKeyHex;
_pendingMessageQueuePerContact[contactKey]?.remove(messageId);
if (_pendingMessageQueuePerContact[contactKey]?.isEmpty ?? false) {
_pendingMessageQueuePerContact.remove(contactKey);
}
});
} }
} }
@@ -669,15 +594,7 @@ class MessageRetryService extends ChangeNotifier {
} }
if (matchedMessageId != null) { if (matchedMessageId != null) {
final message = _pendingMessages[matchedMessageId]; final message = _pendingMessages[matchedMessageId]!;
if (message == null) {
// Message was already cleaned up (e.g. grace period expired)
_ackHashToMessageId.remove(ackHashHex);
debugPrint(
'ACK matched $matchedMessageId but message already cleaned up',
);
return;
}
final contact = _pendingContacts[matchedMessageId]; final contact = _pendingContacts[matchedMessageId];
final selection = _pendingPathSelections[matchedMessageId]; final selection = _pendingPathSelections[matchedMessageId];
@@ -699,21 +616,12 @@ class MessageRetryService extends ChangeNotifier {
tripTimeMs: tripTimeMs, tripTimeMs: tripTimeMs,
); );
// Clean up ALL hash mappings for this message (from all retry attempts)
_ackHashToMessageId.removeWhere(
(_, mapping) => mapping.messageId == matchedMessageId,
);
_expectedHashToMessageId.removeWhere(
(_, msgId) => msgId == matchedMessageId,
);
// Move ACK hashes to history before removing // Move ACK hashes to history before removing
_moveAckHashesToHistory(matchedMessageId); _moveAckHashesToHistory(matchedMessageId);
_pendingMessages.remove(matchedMessageId); _pendingMessages.remove(matchedMessageId);
_pendingContacts.remove(matchedMessageId); _pendingContacts.remove(matchedMessageId);
_pendingPathSelections.remove(matchedMessageId); _pendingPathSelections.remove(matchedMessageId);
_resolvedMessages.remove(matchedMessageId);
// Clean up the queue entry for this contact (remove any remaining references to this message) // Clean up the queue entry for this contact (remove any remaining references to this message)
if (contact != null) { if (contact != null) {
@@ -738,7 +646,6 @@ class MessageRetryService extends ChangeNotifier {
true, true,
tripTimeMs, tripTimeMs,
); );
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
} }
notifyListeners(); notifyListeners();
@@ -876,9 +783,6 @@ class MessageRetryService extends ChangeNotifier {
_ackHistory.clear(); _ackHistory.clear();
_ackHashToMessageId.clear(); _ackHashToMessageId.clear();
_pendingMessageQueuePerContact.clear(); _pendingMessageQueuePerContact.clear();
_sendQueue.clear();
_activeMessages.clear();
_resolvedMessages.clear();
super.dispose(); super.dispose();
} }
} }
+1 -58
View File
@@ -232,9 +232,7 @@ class NotificationService {
try { try {
await _notifications.show( await _notifications.show(
id: contactId != null id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
? 'advert:$contactId'.hashCode
: DateTime.now().millisecondsSinceEpoch,
title: _l10n.notification_newTypeDiscovered(contactType), title: _l10n.notification_newTypeDiscovered(contactType),
body: contactName, body: contactName,
notificationDetails: notificationDetails, notificationDetails: notificationDetails,
@@ -333,61 +331,6 @@ class NotificationService {
await _notifications.cancel(id: id); await _notifications.cancel(id: id);
} }
/// Cancel the notification for a specific contact and update the app badge.
Future<void> clearContactNotification(
String contactId,
int totalUnreadCount,
) async {
if (!await _ensureInitialized()) return;
await _notifications.cancel(id: contactId.hashCode);
await _updateBadge(totalUnreadCount);
}
/// Cancel the notification for a specific channel and update the app badge.
Future<void> clearChannelNotification(
int channelIndex,
int totalUnreadCount,
) async {
if (!await _ensureInitialized()) return;
await _notifications.cancel(id: channelIndex.hashCode);
await _updateBadge(totalUnreadCount);
}
/// Cancel advert notifications for the given contact public key hexes.
Future<void> clearAdvertNotifications(List<String> contactIds) async {
if (!await _ensureInitialized()) return;
for (final id in contactIds) {
await _notifications.cancel(id: 'advert:$id'.hashCode);
}
}
Future<void> _updateBadge(int count) async {
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
// On Apple platforms, set the badge number directly via a silent update.
final darwinDetails = DarwinNotificationDetails(
presentAlert: false,
presentSound: false,
presentBadge: true,
badgeNumber: count,
);
final details = NotificationDetails(
iOS: darwinDetails,
macOS: darwinDetails,
);
// Use a fixed ID so each update replaces the previous one.
await _notifications.show(
id: 'badge_update'.hashCode,
title: null,
body: null,
notificationDetails: details,
);
// Immediately cancel the silent notification so it doesn't appear in tray.
await _notifications.cancel(id: 'badge_update'.hashCode);
}
// On Android, badge count is derived from active notifications,
// so cancelling the specific notification above is sufficient.
}
// //
// Public notification methods (rate limiting is enforced automatically) // Public notification methods (rate limiting is enforced automatically)
// //
-2
View File
@@ -1,2 +0,0 @@
export 'tcp_transport_service_native.dart'
if (dart.library.js_interop) 'tcp_transport_service_web.dart';
@@ -1,210 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'app_debug_log_service.dart';
import 'usb_serial_frame_codec.dart';
class TcpTransportService {
final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast();
final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder();
StreamSubscription<Uint8List>? _socketSubscription;
Socket? _socket;
AppDebugLogService? _debugLogService;
TcpTransportStatus _status = TcpTransportStatus.disconnected;
String? _activeHost;
int? _activePort;
Future<void> _pendingWrite = Future<void>.value();
int _connectGeneration = 0;
TcpTransportStatus get status => _status;
Stream<Uint8List> get frameStream => _frameController.stream;
bool get isConnected => _status == TcpTransportStatus.connected;
String? get activeEndpoint => _activeHost == null || _activePort == null
? null
: '$_activeHost:$_activePort';
void setDebugLogService(AppDebugLogService? service) {
_debugLogService = service;
}
Future<void> connect({
required String host,
required int port,
Duration timeout = const Duration(seconds: 10),
}) async {
if (_status == TcpTransportStatus.connected ||
_status == TcpTransportStatus.connecting) {
throw StateError('TCP transport is already active');
}
final trimmedHost = host.trim();
if (trimmedHost.isEmpty) {
throw ArgumentError.value(host, 'host', 'Host cannot be empty');
}
if (port < 1 || port > 65535) {
throw ArgumentError.value(port, 'port', 'Port must be in 1..65535');
}
_status = TcpTransportStatus.connecting;
final generation = ++_connectGeneration;
_frameDecoder.reset();
try {
final socket = await Socket.connect(trimmedHost, port, timeout: timeout);
if (generation != _connectGeneration ||
_status != TcpTransportStatus.connecting) {
try {
await socket.close();
} catch (_) {}
try {
socket.destroy();
} catch (_) {}
return;
}
socket.setOption(SocketOption.tcpNoDelay, true);
_socket = socket;
_activeHost = trimmedHost;
_activePort = port;
_socketSubscription = socket.listen(
_handleSocketData,
onError: _handleSocketError,
onDone: _handleSocketDone,
);
_status = TcpTransportStatus.connected;
_debugLogService?.info(
'TCP transport opened endpoint=$activeEndpoint',
tag: 'TCP',
);
} catch (error) {
await _cleanupFailedConnect();
_status = TcpTransportStatus.disconnected;
rethrow;
}
}
Future<void> write(Uint8List data) async {
if (!isConnected || _socket == null) {
throw StateError('TCP transport is not connected');
}
final packet = wrapUsbSerialTxFrame(data);
_logFrameSummary('TCP TX frame', data);
final writeTask = _pendingWrite.then((_) async {
final socket = _socket;
if (!isConnected || socket == null) {
throw StateError('TCP transport is not connected');
}
socket.add(packet);
await socket.flush();
});
_pendingWrite = writeTask.catchError((_) {});
await writeTask;
}
Future<void> disconnect() async {
_connectGeneration += 1;
if (_status == TcpTransportStatus.disconnected) return;
final endpoint = activeEndpoint;
_status = TcpTransportStatus.disconnecting;
_frameDecoder.reset();
_activeHost = null;
_activePort = null;
final subscription = _socketSubscription;
_socketSubscription = null;
await subscription?.cancel();
final socket = _socket;
_socket = null;
try {
await socket?.close();
} catch (_) {}
try {
socket?.destroy();
} catch (_) {}
_status = TcpTransportStatus.disconnected;
_debugLogService?.info(
'TCP transport closed endpoint=${endpoint ?? 'unknown'}',
tag: 'TCP',
);
}
void dispose() {
unawaited(disconnect().whenComplete(_closeFrameController));
}
Future<void> _cleanupFailedConnect() async {
final subscription = _socketSubscription;
_socketSubscription = null;
await subscription?.cancel();
final socket = _socket;
_socket = null;
try {
await socket?.close();
} catch (_) {}
try {
socket?.destroy();
} catch (_) {}
_activeHost = null;
_activePort = null;
_frameDecoder.reset();
}
void _handleSocketData(Uint8List bytes) {
for (final packet in _frameDecoder.ingest(bytes)) {
if (!packet.isRxFrame) {
_debugLogService?.info(
'TCP ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}',
tag: 'TCP',
);
continue;
}
_addFrame(packet.payload);
}
}
void _handleSocketError(Object error, [StackTrace? stackTrace]) {
_addFrameError(error, stackTrace);
unawaited(disconnect());
}
void _handleSocketDone() {
if (_status == TcpTransportStatus.disconnecting ||
_status == TcpTransportStatus.disconnected) {
return;
}
_addFrameError(StateError('TCP socket closed by remote endpoint'));
unawaited(disconnect());
}
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);
}
void _logFrameSummary(String prefix, Uint8List payload) {
final code = payload.isNotEmpty ? payload.first : -1;
_debugLogService?.info(
'$prefix code=$code len=${payload.length}',
tag: 'TCP',
);
}
Future<void> _closeFrameController() async {
if (_frameController.isClosed) return;
await _frameController.close();
}
}
enum TcpTransportStatus { disconnected, connecting, connected, disconnecting }
@@ -1,35 +0,0 @@
import 'dart:typed_data';
import 'app_debug_log_service.dart';
class TcpTransportService {
AppDebugLogService? _debugLogService;
Stream<Uint8List> get frameStream => const Stream<Uint8List>.empty();
bool get isConnected => false;
String? get activeEndpoint => null;
void setDebugLogService(AppDebugLogService? service) {
_debugLogService = service;
}
Future<void> connect({
required String host,
required int port,
Duration timeout = const Duration(seconds: 10),
}) async {
_debugLogService?.warn(
'TCP transport requested on web for $host:$port',
tag: 'TCP',
);
throw UnsupportedError('TCP transport is not supported on web.');
}
Future<void> write(Uint8List data) async {
throw UnsupportedError('TCP transport is not supported on web.');
}
Future<void> disconnect() async {}
void dispose() {}
}
+1
View File
@@ -42,6 +42,7 @@ class ChannelOrderStore {
if (jsonString == null || jsonString.isEmpty) { if (jsonString == null || jsonString.isEmpty) {
return []; return [];
} }
try { try {
final decoded = jsonDecode(jsonString); final decoded = jsonDecode(jsonString);
if (decoded is List) { if (decoded is List) {
+1 -1
View File
@@ -32,7 +32,7 @@ class ChannelSettingsStore {
await prefs.setBool(key, enabled); await prefs.setBool(key, enabled);
} }
} }
return enabled ?? false; return prefs.getBool(key) ?? false;
} }
Future<void> saveSmazEnabled(int channelIndex, bool enabled) async { Future<void> saveSmazEnabled(int channelIndex, bool enabled) async {
-3
View File
@@ -33,9 +33,6 @@ class ChannelStore {
} }
} }
if (jsonString == null || jsonString.isEmpty) {
jsonString = prefs.getString(keyFor);
}
if (jsonString == null || jsonString.isEmpty) { if (jsonString == null || jsonString.isEmpty) {
return []; return [];
} }
+1
View File
@@ -38,6 +38,7 @@ class CommunityStore {
jsonString = legacyJsonString; jsonString = legacyJsonString;
} }
} }
if (jsonString == null || jsonString.isEmpty) { if (jsonString == null || jsonString.isEmpty) {
jsonString = prefs.getString(keyFor); jsonString = prefs.getString(keyFor);
} }
+8 -30
View File
@@ -1,13 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import '../models/contact.dart'; import '../models/discovery_contact.dart';
import 'prefs_manager.dart'; import 'prefs_manager.dart';
class ContactDiscoveryStore { class ContactDiscoveryStore {
static const String _keyPrefix = 'discovered_contacts'; static const String _keyPrefix = 'discovered_contacts';
Future<List<Contact>> loadContacts() async { Future<List<DiscoveryContact>> loadContacts() async {
final prefs = PrefsManager.instance; final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_keyPrefix); final jsonStr = prefs.getString(_keyPrefix);
if (jsonStr == null) return []; if (jsonStr == null) return [];
@@ -22,62 +22,40 @@ class ContactDiscoveryStore {
} }
} }
Future<void> saveContacts(List<Contact> contacts) async { Future<void> saveContacts(List<DiscoveryContact> contacts) async {
final prefs = PrefsManager.instance; final prefs = PrefsManager.instance;
final jsonList = contacts.map(_toJson).toList(); final jsonList = contacts.map(_toJson).toList();
await prefs.setString(_keyPrefix, jsonEncode(jsonList)); await prefs.setString(_keyPrefix, jsonEncode(jsonList));
} }
Map<String, dynamic> _toJson(Contact contact) { Map<String, dynamic> _toJson(DiscoveryContact contact) {
return { return {
'rawPacket': base64Encode(contact.rawPacket),
'publicKey': base64Encode(contact.publicKey), 'publicKey': base64Encode(contact.publicKey),
'name': contact.name, 'name': contact.name,
'type': contact.type, 'type': contact.type,
'flags': contact.flags,
'pathLength': contact.pathLength, 'pathLength': contact.pathLength,
'path': base64Encode(contact.path), 'path': base64Encode(contact.path),
'pathOverride': contact.pathOverride,
'pathOverrideBytes': contact.pathOverrideBytes != null
? base64Encode(contact.pathOverrideBytes!)
: null,
'latitude': contact.latitude, 'latitude': contact.latitude,
'longitude': contact.longitude, 'longitude': contact.longitude,
'lastSeen': contact.lastSeen.millisecondsSinceEpoch, 'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch,
'rawPacket': contact.rawPacket != null
? base64Encode(contact.rawPacket!)
: null,
}; };
} }
Contact _fromJson(Map<String, dynamic> json) { DiscoveryContact _fromJson(Map<String, dynamic> json) {
final lastSeenMs = json['lastSeen'] as int? ?? 0; final lastSeenMs = json['lastSeen'] as int? ?? 0;
final lastMessageMs = json['lastMessageAt'] as int?; return DiscoveryContact(
return Contact( rawPacket: Uint8List.fromList(base64Decode(json['rawPacket'] as String)),
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
name: json['name'] as String? ?? 'Unknown', name: json['name'] as String? ?? 'Unknown',
type: json['type'] as int? ?? 0, type: json['type'] as int? ?? 0,
flags: json['flags'] as int? ?? 0,
pathLength: json['pathLength'] as int? ?? -1, pathLength: json['pathLength'] as int? ?? -1,
path: json['path'] != null path: json['path'] != null
? Uint8List.fromList(base64Decode(json['path'] as String)) ? Uint8List.fromList(base64Decode(json['path'] as String))
: Uint8List(0), : Uint8List(0),
pathOverride: json['pathOverride'] as int?,
pathOverrideBytes: json['pathOverrideBytes'] != null
? Uint8List.fromList(
base64Decode(json['pathOverrideBytes'] as String),
)
: null,
latitude: (json['latitude'] as num?)?.toDouble(), latitude: (json['latitude'] as num?)?.toDouble(),
longitude: (json['longitude'] as num?)?.toDouble(), longitude: (json['longitude'] as num?)?.toDouble(),
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs), lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
lastMessageMs ?? lastSeenMs,
),
isActive: false,
rawPacket: json['rawPacket'] != null
? Uint8List.fromList(base64Decode(json['rawPacket'] as String))
: null,
); );
} }
} }
-8
View File
@@ -76,10 +76,6 @@ class ContactStore {
'longitude': contact.longitude, 'longitude': contact.longitude,
'lastSeen': contact.lastSeen.millisecondsSinceEpoch, 'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch, 'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch,
'isActive': contact.isActive,
'rawPacket': contact.rawPacket != null
? base64Encode(contact.rawPacket!)
: null,
}; };
} }
@@ -107,10 +103,6 @@ class ContactStore {
lastMessageAt: DateTime.fromMillisecondsSinceEpoch( lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
lastMessageMs ?? lastSeenMs, lastMessageMs ?? lastSeenMs,
), ),
isActive: json['isActive'] as bool? ?? true,
rawPacket: json['rawPacket'] != null
? Uint8List.fromList(base64Decode(json['rawPacket'] as String))
: null,
); );
} }
} }
+1
View File
@@ -49,6 +49,7 @@ class MessageStore {
jsonString = legacyJsonString; jsonString = legacyJsonString;
} }
} }
if (jsonString == null || jsonString.isEmpty) { if (jsonString == null || jsonString.isEmpty) {
jsonString = prefs.getString(keyFor); jsonString = prefs.getString(keyFor);
} }
+3 -1
View File
@@ -1,3 +1,5 @@
import 'package:meshcore_open/models/discovery_contact.dart';
import '../models/contact.dart'; import '../models/contact.dart';
bool matchesContactQuery(Contact contact, String query) { bool matchesContactQuery(Contact contact, String query) {
@@ -14,7 +16,7 @@ bool matchesContactQuery(Contact contact, String query) {
return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix); return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix);
} }
bool matchesDiscoveryContactQuery(Contact contact, String query) { bool matchesDiscoveryContactQuery(DiscoveryContact contact, String query) {
final normalizedQuery = query.trim().toLowerCase(); final normalizedQuery = query.trim().toLowerCase();
if (normalizedQuery.isEmpty) return true; if (normalizedQuery.isEmpty) return true;
+2 -5
View File
@@ -157,11 +157,8 @@ class _SNRIndicatorState extends State<SNRIndicator> {
repeater.snr, repeater.snr,
widget.connector.currentSf, widget.connector.currentSf,
); );
final allContacts = [
...widget.connector.contacts, final name = widget.connector.contacts
...widget.connector.discoveredContacts,
];
final name = allContacts
.where((c) => c.publicKey.first == repeater.pubkeyFirstByte) .where((c) => c.publicKey.first == repeater.pubkeyFirstByte)
.map((c) => c.name) .map((c) => c.name)
.firstOrNull; .firstOrNull;
@@ -1,93 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
void main() {
group('shouldIgnoreLateTcpConnectError', () {
test('returns true for manual cancel during disconnecting state', () {
final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError(
manualDisconnect: true,
state: MeshCoreConnectionState.disconnecting,
activeTransport: MeshCoreTransportType.bluetooth,
tcpManagerConnected: false,
);
expect(result, isTrue);
});
test(
'returns true for manual cancel after reaching disconnected state',
() {
final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError(
manualDisconnect: true,
state: MeshCoreConnectionState.disconnected,
activeTransport: MeshCoreTransportType.bluetooth,
tcpManagerConnected: false,
);
expect(result, isTrue);
},
);
test('returns false when not a manual disconnect', () {
final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError(
manualDisconnect: false,
state: MeshCoreConnectionState.disconnecting,
activeTransport: MeshCoreTransportType.bluetooth,
tcpManagerConnected: false,
);
expect(result, isFalse);
});
test('returns false for connected state handshake failures', () {
final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError(
manualDisconnect: true,
state: MeshCoreConnectionState.connected,
activeTransport: MeshCoreTransportType.tcp,
tcpManagerConnected: true,
);
expect(result, isFalse);
});
test('returns false when TCP is still active while disconnecting', () {
final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError(
manualDisconnect: true,
state: MeshCoreConnectionState.disconnecting,
activeTransport: MeshCoreTransportType.tcp,
tcpManagerConnected: true,
);
expect(result, isFalse);
});
});
group('shouldResetStateAfterTcpConnectAbort', () {
test('returns true when TCP connect is still in connecting state', () {
final result = MeshCoreConnector.shouldResetStateAfterTcpConnectAbort(
state: MeshCoreConnectionState.connecting,
activeTransport: MeshCoreTransportType.tcp,
);
expect(result, isTrue);
});
test('returns false when state is already disconnected', () {
final result = MeshCoreConnector.shouldResetStateAfterTcpConnectAbort(
state: MeshCoreConnectionState.disconnected,
activeTransport: MeshCoreTransportType.tcp,
);
expect(result, isFalse);
});
test('returns false when transport switched away from TCP', () {
final result = MeshCoreConnector.shouldResetStateAfterTcpConnectAbort(
state: MeshCoreConnectionState.connecting,
activeTransport: MeshCoreTransportType.bluetooth,
);
expect(result, isFalse);
});
});
}
-192
View File
@@ -1,192 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/l10n/app_localizations.dart';
import 'package:meshcore_open/screens/scanner_screen.dart';
import 'package:meshcore_open/screens/tcp_screen.dart';
class _FakeMeshCoreConnector extends MeshCoreConnector {
_FakeMeshCoreConnector();
MeshCoreConnectionState initialState = MeshCoreConnectionState.disconnected;
MeshCoreTransportType initialTransport = MeshCoreTransportType.bluetooth;
String? initialEndpoint;
int connectTcpCalls = 0;
String? lastHost;
int? lastPort;
@override
MeshCoreConnectionState get state => initialState;
@override
MeshCoreTransportType get activeTransport => initialTransport;
@override
bool get isTcpTransportConnected =>
initialState == MeshCoreConnectionState.connected &&
initialTransport == MeshCoreTransportType.tcp;
@override
String? get activeTcpEndpoint => initialEndpoint;
@override
Future<void> connectTcp({required String host, required int port}) async {
connectTcpCalls += 1;
lastHost = host;
lastPort = port;
}
}
Widget _buildTestApp({
required MeshCoreConnector connector,
required Widget child,
Locale? locale,
}) {
return ChangeNotifierProvider<MeshCoreConnector>.value(
value: connector,
child: MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: child,
),
);
}
void main() {
testWidgets('TcpScreen uses localized TCP copy', (tester) async {
final connector = _FakeMeshCoreConnector();
await tester.pumpWidget(
_buildTestApp(
connector: connector,
child: const TcpScreen(),
locale: const Locale('en'),
),
);
await tester.pumpAndSettle();
final context = tester.element(find.byType(TcpScreen));
final l10n = AppLocalizations.of(context);
expect(find.text(l10n.tcpScreenTitle), findsOneWidget);
expect(find.text(l10n.tcpHostLabel), findsOneWidget);
expect(find.text(l10n.tcpPortLabel), findsOneWidget);
expect(find.text(l10n.tcpStatus_notConnected), findsOneWidget);
});
testWidgets('TcpScreen validation errors are localized', (tester) async {
final connector = _FakeMeshCoreConnector();
await tester.pumpWidget(
_buildTestApp(
connector: connector,
child: const TcpScreen(),
locale: const Locale('en'),
),
);
await tester.pumpAndSettle();
final context = tester.element(find.byType(TcpScreen));
final l10n = AppLocalizations.of(context);
await tester.enterText(find.byType(TextField).first, '');
await tester.tap(find.byKey(const Key('tcp_connect_button')));
await tester.pumpAndSettle();
expect(find.text(l10n.tcpErrorHostRequired), findsOneWidget);
expect(connector.connectTcpCalls, 0);
await tester.enterText(find.byType(TextField).first, '192.168.1.50');
await tester.enterText(find.byType(TextField).at(1), '99999');
await tester.tap(find.byKey(const Key('tcp_connect_button')));
await tester.pumpAndSettle();
expect(connector.connectTcpCalls, 0);
});
testWidgets('TCP Bluetooth action returns to existing scanner route', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const ScannerScreen()),
);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FloatingActionButton, 'TCP'));
await tester.pumpAndSettle();
expect(find.byType(TcpScreen), findsOneWidget);
await tester.tap(find.widgetWithText(FloatingActionButton, 'Bluetooth'));
await tester.pumpAndSettle();
expect(find.byType(TcpScreen), findsNothing);
expect(find.byType(ScannerScreen), findsOneWidget);
final navigatorState = tester.state<NavigatorState>(find.byType(Navigator));
expect(navigatorState.canPop(), isFalse);
// ScannerScreen.dispose() schedules disconnect work that debounces notify.
// Drain that debounce timer before test teardown.
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
});
testWidgets('TcpScreen disables connect button while connector is scanning', (
tester,
) async {
final connector = _FakeMeshCoreConnector()
..initialState = MeshCoreConnectionState.scanning;
await tester.pumpWidget(
_buildTestApp(
connector: connector,
child: const TcpScreen(),
locale: const Locale('en'),
),
);
await tester.pumpAndSettle();
final button = tester.widget<ButtonStyleButton>(
find.byKey(const Key('tcp_connect_button')),
);
expect(button.onPressed, isNull);
expect(connector.connectTcpCalls, 0);
});
testWidgets('TcpScreen narrow width long status text does not overflow', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(320, 700));
addTearDown(() => tester.binding.setSurfaceSize(null));
final connector = _FakeMeshCoreConnector()
..initialState = MeshCoreConnectionState.connected
..initialTransport = MeshCoreTransportType.tcp
..initialEndpoint = 'meshcore-room-server-very-long-hostname.local:5000';
await tester.pumpWidget(
_buildTestApp(
connector: connector,
child: const TcpScreen(),
locale: const Locale('en'),
),
);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
final context = tester.element(find.byType(TcpScreen));
final l10n = AppLocalizations.of(context);
expect(
find.text(l10n.scanner_connectedTo(connector.initialEndpoint!)),
findsOneWidget,
);
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
});
}
+18 -65
View File
@@ -116,7 +116,12 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.byType(ListTile).first); await tester.tap(
find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
),
);
await tester.pump(); await tester.pump();
expect(connector.connectUsbCalls, 0); expect(connector.connectUsbCalls, 0);
@@ -140,7 +145,12 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.byType(ListTile).first); await tester.tap(
find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
),
);
await tester.pump(); await tester.pump();
expect(connector.connectUsbCalls, 1); expect(connector.connectUsbCalls, 1);
@@ -169,68 +179,6 @@ void main() {
await tester.pump(const Duration(milliseconds: 60)); await tester.pump(const Duration(milliseconds: 60));
}); });
testWidgets('ScannerScreen narrow width keeps actions without overflow', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(320, 700));
addTearDown(() => tester.binding.setSurfaceSize(null));
final connector = _FakeMeshCoreConnector();
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const ScannerScreen()),
);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
final context = tester.element(find.byType(ScannerScreen));
final l10n = AppLocalizations.of(context);
expect(find.text(l10n.scanner_scan), findsOneWidget);
if (PlatformInfo.supportsUsbSerial) {
expect(find.text(l10n.connectionChoiceUsbLabel), findsOneWidget);
}
if (!PlatformInfo.isWeb) {
expect(find.text(l10n.connectionChoiceTcpLabel), findsOneWidget);
}
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
});
testWidgets('UsbScreen narrow width long status text does not overflow', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(320, 700));
addTearDown(() => tester.binding.setSurfaceSize(null));
final connector =
_FakeMeshCoreConnector(initialState: MeshCoreConnectionState.connected)
..fakeUsbTransportConnected = true
..fakeActiveUsbPortDisplayLabel =
'/dev/bus/usb/001/002 - KD3CGK mesh-utility.org very long label';
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
final context = tester.element(find.byType(UsbScreen));
final l10n = AppLocalizations.of(context);
expect(
find.text(
l10n.scanner_connectedTo(connector.fakeActiveUsbPortDisplayLabel!),
),
findsOneWidget,
);
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
});
group('Error Handling', () { group('Error Handling', () {
testWidgets('shows error SnackBar when listing ports fails', ( testWidgets('shows error SnackBar when listing ports fails', (
tester, tester,
@@ -264,7 +212,12 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.byType(ListTile).first); await tester.tap(
find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
),
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(connectAttempted, isTrue); expect(connectAttempted, isTrue);
@@ -1,136 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/services/tcp_transport_service_native.dart';
import 'package:meshcore_open/services/usb_serial_frame_codec.dart';
final class _DelayedConnectOverrides extends IOOverrides {
_DelayedConnectOverrides(this.delay);
final Duration delay;
@override
Future<Socket> socketConnect(
host,
int port, {
sourceAddress,
int sourcePort = 0,
Duration? timeout,
}) async {
await Future<void>.delayed(delay);
return super.socketConnect(
host,
port,
sourceAddress: sourceAddress,
sourcePort: sourcePort,
timeout: timeout,
);
}
}
void main() {
test('connect/disconnect updates TCP transport state', () async {
final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
final service = TcpTransportService();
try {
await service.connect(
host: InternetAddress.loopbackIPv4.address,
port: server.port,
);
expect(service.isConnected, isTrue);
expect(
service.activeEndpoint,
'${InternetAddress.loopbackIPv4.address}:${server.port}',
);
await service.disconnect();
expect(service.isConnected, isFalse);
expect(service.activeEndpoint, isNull);
} finally {
await service.disconnect();
await server.close();
}
});
test('disconnect is safe when already disconnected', () async {
final service = TcpTransportService();
await service.disconnect();
await service.disconnect();
expect(service.isConnected, isFalse);
expect(service.activeEndpoint, isNull);
});
test('emits only RX frames from socket stream', () async {
final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
final acceptedSocket = Completer<Socket>();
final service = TcpTransportService();
final receivedFrames = <Uint8List>[];
final serverSub = server.listen((socket) {
if (!acceptedSocket.isCompleted) {
acceptedSocket.complete(socket);
} else {
socket.destroy();
}
});
final frameSub = service.frameStream.listen(receivedFrames.add);
try {
await service.connect(
host: InternetAddress.loopbackIPv4.address,
port: server.port,
);
final socket = await acceptedSocket.future.timeout(
const Duration(seconds: 2),
);
socket.add(<int>[usbSerialTxFrameStart, 0x01, 0x00, 0x11]);
socket.add(<int>[usbSerialRxFrameStart, 0x02, 0x00, 0x33, 0x44]);
await socket.flush();
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(receivedFrames, hasLength(1));
expect(receivedFrames.single, orderedEquals(<int>[0x33, 0x44]));
} finally {
await service.disconnect();
await frameSub.cancel();
await serverSub.cancel();
await server.close();
}
});
test(
'disconnect during in-flight connect keeps transport disconnected',
() async {
final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
final service = TcpTransportService();
final host = InternetAddress.loopbackIPv4.address;
try {
await IOOverrides.runWithIOOverrides(() async {
final connectFuture = service.connect(host: host, port: server.port);
await Future<void>.delayed(const Duration(milliseconds: 10));
await service.disconnect();
await connectFuture;
expect(service.isConnected, isFalse);
expect(service.status, TcpTransportStatus.disconnected);
expect(service.activeEndpoint, isNull);
}, _DelayedConnectOverrides(const Duration(milliseconds: 120)));
} finally {
await service.disconnect();
await server.close();
}
},
);
}