diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 33e5c48..7211992 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -294,6 +294,10 @@ class MeshCoreConnector extends ChangeNotifier { ); } + List get allContacts => List.unmodifiable([ + ..._contacts, + ..._discoveredContacts.where((c) => !c.isActive), + ]); List get discoveredContacts { return List.unmodifiable(_discoveredContacts); } @@ -726,6 +730,9 @@ class MeshCoreConnector extends ChangeNotifier { _knownContactKeys ..clear() ..addAll(cached.map((c) => c.publicKeyHex)); + _contacts + ..clear() + ..addAll(cached); for (final contact in cached) { _ensureContactSmazSettingLoaded(contact.publicKeyHex); } @@ -1558,6 +1565,10 @@ class MeshCoreConnector extends ChangeNotifier { if (_activeTransport == MeshCoreTransportType.usb) { await _usbManager.write(data); + // Brief pause so the device firmware can process each frame before the + // next arrives. Without this, rapid-fire frames over USB can cause the + // device to miss responses (especially on reconnect). + await Future.delayed(const Duration(milliseconds: 10)); } else if (_activeTransport == MeshCoreTransportType.tcp) { await _tcpConnector.write(data); } else { @@ -2962,6 +2973,8 @@ class MeshCoreConnector extends ChangeNotifier { void _handleContact(Uint8List frame, {bool isContact = true}) { final contact = Contact.fromFrame(frame); if (contact != null) { + _handleDiscovery(contact, frame, noNotify: true, addActive: true); + if (contact.type == advTypeRepeater) { _contactUnreadCount.remove(contact.publicKeyHex); _unreadStore.saveContactUnreadCount( @@ -4770,6 +4783,12 @@ class MeshCoreConnector extends ChangeNotifier { (_autoAddRoomServers && type == advTypeRoom) || (_autoAddSensors && type == advTypeSensor)) { _handleContactAdvert(newContact); + _handleDiscovery( + newContact, + rawPacket, + noNotify: true, + addActive: true, + ); } else { _handleDiscovery(newContact, rawPacket); } @@ -4794,8 +4813,20 @@ class MeshCoreConnector extends ChangeNotifier { // CRITICAL: Preserve user's path override when contact is refreshed from device _contacts[existingIndex] = existing.copyWith( - latitude: hasLocation ? latitude : existing.latitude, - longitude: hasLocation ? longitude : existing.longitude, + latitude: + hasLocation && + latitude != null && + latitude.abs() <= 90 && + (latitude != 0 || longitude != 0) + ? latitude + : existing.latitude, + longitude: + hasLocation && + longitude != null && + longitude.abs() <= 180 && + (latitude != 0 || longitude != 0) + ? longitude + : existing.longitude, name: hasName ? name : existing.name, path: Uint8List.fromList(path.reversed.toList()), pathLength: path.length, @@ -4866,11 +4897,11 @@ class MeshCoreConnector extends ChangeNotifier { try { reader.skipBytes(1); // Skip the response code byte final flags = reader.readByte(); - _autoAddUsers = flags & autoAddChatFlag != 0; - _autoAddRepeaters = flags & autoAddRepeaterFlag != 0; - _autoAddRoomServers = flags & autoAddRoomServerFlag != 0; - _autoAddSensors = flags & autoAddSensorFlag != 0; - _overwriteOldest = flags & autoAddOverwriteOldestFlag != 0; + _autoAddUsers = (flags & autoAddChatFlag) != 0; + _autoAddRepeaters = (flags & autoAddRepeaterFlag) != 0; + _autoAddRoomServers = (flags & autoAddRoomServerFlag) != 0; + _autoAddSensors = (flags & autoAddSensorFlag) != 0; + _overwriteOldest = (flags & autoAddOverwriteOldestFlag) != 0; } catch (e) { appLogger.error('Failed to parse auto-add config: $e', tag: 'Connector'); } @@ -4880,6 +4911,7 @@ class MeshCoreConnector extends ChangeNotifier { Contact contact, Uint8List rawPacket, { bool noNotify = false, + bool addActive = false, }) { appLogger.info('Discovered new contact: ${contact.name}', tag: 'Connector'); @@ -4900,7 +4932,7 @@ class MeshCoreConnector extends ChangeNotifier { longitude: contact.longitude, lastSeen: contact.lastSeen, flags: 0, - isActive: false, + isActive: addActive, ); notifyListeners(); unawaited(_persistDiscoveredContacts()); @@ -4918,7 +4950,7 @@ class MeshCoreConnector extends ChangeNotifier { longitude: contact.longitude, lastSeen: contact.lastSeen, lastMessageAt: contact.lastMessageAt, - isActive: false, + isActive: addActive, flags: 0, ); _discoveredContacts.add(disContact); diff --git a/lib/connector/meshcore_connector_usb.dart b/lib/connector/meshcore_connector_usb.dart index 74e7355..56718bc 100644 --- a/lib/connector/meshcore_connector_usb.dart +++ b/lib/connector/meshcore_connector_usb.dart @@ -64,6 +64,8 @@ class MeshCoreUsbManager { Future write(Uint8List data) => _service.write(data); + Future writeRaw(Uint8List data) => _service.writeRaw(data); + // --- Label management --- void updateConnectedLabel(String selfName) { _service.updateConnectedLabel(selfName); diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index c89ac27..fc84851 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -40,6 +40,8 @@ class AppSettings { final UnitSystem unitSystem; final Set mutedChannels; final bool mapShowDiscoveryContacts; + final String tcpServerAddress; + final int tcpServerPort; AppSettings({ this.clearPathOnMaxRetry = false, @@ -68,6 +70,8 @@ class AppSettings { this.unitSystem = UnitSystem.metric, Set? mutedChannels, this.mapShowDiscoveryContacts = true, + this.tcpServerAddress = '', + this.tcpServerPort = 0, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, mutedChannels = mutedChannels ?? {}; @@ -100,6 +104,8 @@ class AppSettings { 'unit_system': unitSystem.value, 'muted_channels': mutedChannels.toList(), 'map_show_discovery_contacts': mapShowDiscoveryContacts, + 'tcp_server_address': tcpServerAddress, + 'tcp_server_port': tcpServerPort, }; } @@ -157,6 +163,8 @@ class AppSettings { {}, mapShowDiscoveryContacts: json['map_show_discovery_contacts'] as bool? ?? true, + tcpServerAddress: json['tcp_server_address'] as String? ?? '', + tcpServerPort: json['tcp_server_port'] as int? ?? 0, ); } @@ -187,6 +195,8 @@ class AppSettings { UnitSystem? unitSystem, Set? mutedChannels, bool? mapShowDiscoveryContacts, + String? tcpServerAddress, + int? tcpServerPort, }) { return AppSettings( clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, @@ -225,6 +235,8 @@ class AppSettings { mutedChannels: mutedChannels ?? this.mutedChannels, mapShowDiscoveryContacts: mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts, + tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress, + tcpServerPort: tcpServerPort ?? this.tcpServerPort, ); } } diff --git a/lib/models/contact.dart b/lib/models/contact.dart index cab58cb..c047622 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -65,7 +65,17 @@ class Contact { return '$pathLength hops'; } - bool get hasLocation => latitude != null && longitude != null; + bool get hasLocation { + const double epsilon = 1e-6; + final lat = latitude ?? 0.0; + final lon = longitude ?? 0.0; + return (lat.abs() > epsilon || lon.abs() > epsilon) && + lat >= -90.0 && + lat <= 90.0 && + lon >= -180.0 && + lon <= 180.0; + } + bool get isFavorite => (flags & contactFlagFavorite) != 0; Contact copyWith({ @@ -108,7 +118,7 @@ class Contact { } String get pathIdList { - final pathBytes = _pathBytesForDisplay; + final pathBytes = pathBytesForDisplay; if (pathBytes.isEmpty) return ''; final parts = []; final groupSize = pathHashSize; @@ -130,43 +140,7 @@ class Contact { return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; } - Uint8List? get traceRouteBytes { - final pathBytes = _pathBytesForDisplay; - Uint8List? traceBytes; - - if (pathBytes.isEmpty) { - traceBytes = Uint8List(1); - traceBytes[0] = publicKey[0]; - return traceBytes; - } - - if (type == advTypeRepeater || type == advTypeRoom) { - final len = (pathBytes.length + pathBytes.length + 1); - traceBytes = Uint8List(len); - traceBytes[pathBytes.length] = publicKey[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 ? null : 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; - } - - Uint8List get _pathBytesForDisplay { + Uint8List get pathBytesForDisplay { if (pathOverride != null) { if (pathOverride! < 0) return Uint8List(0); return pathOverrideBytes ?? Uint8List(0); @@ -197,6 +171,7 @@ class Contact { double? lat, lon; final latRaw = reader.readInt32LE(); final lonRaw = reader.readInt32LE(); + if (latRaw != 0 || lonRaw != 0) { lat = latRaw / 1e6; lon = lonRaw / 1e6; diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index c2c57f0..747c2bf 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -40,10 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget { final primaryPath = !channelMessage && !message.isOutgoing ? Uint8List.fromList(primaryPathTmp.reversed.toList()) : primaryPathTmp; - final contacts = [ - ...connector.contacts, - ...connector.discoveredContacts, - ]; + final contacts = connector.allContacts; final hops = _buildPathHops(primaryPath, contacts, l10n); final hasHopDetails = primaryPath.isNotEmpty; final observedLabel = _formatObservedHops( @@ -65,8 +62,9 @@ class ChannelMessagePathScreen extends StatelessWidget { builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_repeaterPathTrace, path: primaryPath, - flipPathRound: true, - reversePathRound: !message.isOutgoing && !channelMessage, + flipPathAround: true, + reversePathAround: + !(!channelMessage && !message.isOutgoing), ), ), ), @@ -367,10 +365,7 @@ class _ChannelMessagePathMapScreenState : selectedPathTmp; final selectedIndex = _indexForPath(selectedPath, observedPaths); - final contacts = [ - ...connector.contacts, - ...connector.discoveredContacts, - ]; + final contacts = connector.allContacts; final hops = _buildPathHops(selectedPath, contacts, context.l10n); final points = []; diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 96203ea..5209b41 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -858,7 +858,7 @@ class _ChatScreenState extends State { builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), - flipPathRound: true, + flipPathAround: true, targetContact: widget.contact, ), ), @@ -1027,7 +1027,7 @@ class _ChatScreenState extends State { final currentPathLabel = _currentPathLabel(currentContact); // Filter out the current contact from available contacts - final availableContacts = connector.contacts + final availableContacts = connector.allContacts .where((c) => c != widget.contact) .toList(); diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 7937398..23844fb 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1239,7 +1239,7 @@ class _ContactsScreenState extends State if (isRepeater) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathLength > 0 + title: contact.pathBytesForDisplay.isNotEmpty ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping), onTap: () { @@ -1247,10 +1247,12 @@ class _ContactsScreenState extends State context, MaterialPageRoute( builder: (context) => PathTraceMapScreen( - title: contact.pathLength > 0 + title: contact.pathBytesForDisplay.isNotEmpty ? context.l10n.contacts_repeaterPathTrace : context.l10n.contacts_repeaterPing, - path: contact.traceRouteBytes ?? Uint8List(0), + path: contact.pathBytesForDisplay, + flipPathAround: true, + targetContact: contact, ), ), ); @@ -1275,10 +1277,12 @@ class _ContactsScreenState extends State context, MaterialPageRoute( builder: (context) => PathTraceMapScreen( - title: contact.pathLength > 0 + title: contact.pathBytesForDisplay.isNotEmpty ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing, - path: contact.traceRouteBytes ?? Uint8List(0), + path: contact.pathBytesForDisplay, + flipPathAround: contact.pathBytesForDisplay.isNotEmpty, + targetContact: contact, ), ), ); @@ -1320,7 +1324,8 @@ class _ContactsScreenState extends State title: context.l10n.contacts_pathTraceTo( contact.name, ), - path: contact.traceRouteBytes ?? Uint8List(0), + path: contact.pathBytesForDisplay, + flipPathAround: true, targetContact: contact, ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 1dd3a5f..df16a59 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -137,10 +137,7 @@ class _MapScreenState extends State { builder: (context, connector, settingsService, pathHistory, child) { final tileCache = context.read(); final settings = settingsService.settings; - final allContacts = [ - ...connector.contacts, - ...connector.discoveredContacts.where((c) => !c.isActive), - ]; + final allContacts = connector.allContacts; final contacts = settings.mapShowDiscoveryContacts ? allContacts @@ -179,20 +176,13 @@ class _MapScreenState extends State { // Filter by location final contactsWithLocation = filteredByKeyPrefix.where((c) { - if (!c.hasLocation) { - return false; - } - return _checkLocationPlausibility(c.latitude!, c.longitude!); + return c.hasLocation; }).toList(); // All contacts with a known location — used as anchors regardless of // time/key-prefix filters so that repeaters are always available. final allContactsWithLocation = allContacts - .where( - (c) => - c.hasLocation && - _checkLocationPlausibility(c.latitude!, c.longitude!), - ) + .where((c) => c.hasLocation) .toList(); // Compute guessed locations with caching diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index 5cb8e45..5afeda4 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -124,10 +124,7 @@ class _NeighborsScreenState extends State { void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) { final buffer = BufferReader(frame); - final contacts = [ - ...connector.contacts, - ...connector.discoveredContacts, - ]; + final contacts = connector.allContacts; try { final neighborCount = buffer.readUInt16LE(); final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index ceb60a6..e64a906 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -52,8 +52,8 @@ class PathTraceMapScreen extends StatefulWidget { final String title; final Uint8List path; final int? repeaterId; - final bool flipPathRound; - final bool reversePathRound; + final bool flipPathAround; + final bool reversePathAround; final Contact? targetContact; const PathTraceMapScreen({ @@ -61,8 +61,8 @@ class PathTraceMapScreen extends StatefulWidget { required this.title, required this.path, this.repeaterId, - this.flipPathRound = false, - this.reversePathRound = false, + this.flipPathAround = false, + this.reversePathAround = false, this.targetContact, }); @@ -93,6 +93,7 @@ class _PathTraceMapScreenState extends State { ValueKey _mapKey = const ValueKey('initial'); double _pathDistanceMeters = 0.0; bool _showNodeLabels = true; + Contact? _targetContact; String _formatPathPrefixes(Uint8List pathBytes) { return pathBytes @@ -158,21 +159,16 @@ class _PathTraceMapScreenState extends State { }); } - final Uint8List path; - - Uint8List pathTmp = widget.reversePathRound + final pathTmp = widget.reversePathAround ? Uint8List.fromList(widget.path.reversed.toList()) : widget.path; - if (widget.flipPathRound) { - path = buildPath(pathTmp); - } else { - path = pathTmp; - } + final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp; appLogger.info( 'Initiating path trace with path: ${_formatPathPrefixes(path)}', tag: 'PathTraceMapScreen', + noNotify: !mounted, ); final connector = Provider.of(context, listen: false); @@ -263,10 +259,7 @@ class _PathTraceMapScreenState extends State { .toList(); Map pathContacts = {}; - final contacts = [ - ...connector.contacts, - ...connector.discoveredContacts, - ]; + final contacts = connector.allContacts; contacts.where((c) => c.type != advTypeChat).forEach((repeater) { for (var repeaterData in pathData) { if (listEquals( @@ -312,18 +305,21 @@ class _PathTraceMapScreenState extends State { // Compute endpoint position for the target contact. LatLng? targetPos; bool targetGuessed = false; - final target = widget.targetContact; - if (target != null) { - if (target.hasLocation) { - targetPos = LatLng(target.latitude!, target.longitude!); - } else if (pathData.isNotEmpty) { + _targetContact = widget.targetContact; + + if (_targetContact != null) { + final tc = _targetContact!; + if (tc.hasLocation) { + targetPos = LatLng(tc.latitude!, tc.longitude!); + } else if (widget.path.length > 1) { // Infer from the last hop: average GPS contacts sharing that hop. - // For a round-trip path (flipPathRound), the target-side hop sits - // in the middle of the symmetric sequence; .last is the local side. - final lastHop = (widget.flipPathRound && pathData.length > 1) - ? pathData[(pathData.length - 1) ~/ 2] - : pathData.last; - final peers = connector.contacts + // For a round-trip path (flipPathAround/reversePathAround), the target-side hop + // sits in the middle of the symmetric sequence; .last is the local side. + final lastHop = widget.reversePathAround + ? widget.path.first + : widget.path.last; + + final peers = connector.allContacts .where( (c) => c.hasLocation && @@ -339,12 +335,34 @@ class _PathTraceMapScreenState extends State { peers.map((c) => c.longitude!).reduce((a, b) => a + b) / peers.length; const offsetDeg = 0.003; - final angle = (target.publicKey[1] / 255.0) * 2 * pi; + final angle = (tc.publicKey[1] / 255.0) * 2 * pi; targetPos = LatLng( lat + offsetDeg * cos(angle), lon + offsetDeg * sin(angle), ); targetGuessed = true; + } else if (inferredPositions.containsKey(lastHop)) { + final lat = inferredPositions[lastHop]!.latitude; + final lon = inferredPositions[lastHop]!.longitude; + const offsetDeg = 0.003; + final angle = (tc.publicKey[1] / 255.0) * 2 * pi; + targetPos = LatLng( + lat + offsetDeg * cos(angle), + lon + offsetDeg * sin(angle), + ); + targetGuessed = true; + } else { + // As a last resort, just place it at the same position as the last hop. + final contact = pathContacts[lastHop]; + if (contact != null && contact.hasLocation) { + const offsetDeg = 0.003; + final angle = (tc.publicKey[1] / 255.0) * 2 * pi; + targetPos = LatLng( + contact.latitude! + offsetDeg * cos(angle), + contact.longitude! + offsetDeg * sin(angle), + ); + targetGuessed = true; + } } } } @@ -353,7 +371,12 @@ class _PathTraceMapScreenState extends State { _points = []; _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + int hopLast = 0; + int hopLastLast = 0; for (final hop in _traceData!.pathData) { + if (hop == hopLastLast && widget.flipPathAround) { + break; //skip duplicate hops in round-trip paths + } final contact = _traceData!.pathContacts[hop]; if (contact != null && contact.hasLocation) { _points.add(LatLng(contact.latitude!, contact.longitude!)); @@ -361,8 +384,14 @@ class _PathTraceMapScreenState extends State { final inferred = inferredPositions[hop]; if (inferred != null) _points.add(inferred); } + hopLastLast = hopLast; + hopLast = hop; + } + if (targetPos != null) { + if (_targetContact != null && _targetContact!.type == advTypeChat) { + _points.add(targetPos); + } } - if (targetPos != null) _points.add(targetPos); _polylines = _points.length > 1 ? [ Polyline( @@ -451,7 +480,8 @@ class _PathTraceMapScreenState extends State { ], ), ), - if (_hasData) _buildMapPathTrace(context, tileCache), + if (_hasData) + _buildMapPathTrace(context, tileCache, _targetContact), if (_points.isEmpty && !_hasData && !_isLoading && @@ -480,17 +510,28 @@ class _PathTraceMapScreenState extends State { List _buildHopMarkers( List pathData, { required bool showLabels, + required Contact? target, }) { final markers = []; + int hopLast = 0; + int hopLastLast = 0; for (final hop in pathData) { final contact = _traceData!.pathContacts[hop]; final inferred = _inferredHopPositions[hop]; final hasGps = contact != null && contact.hasLocation; - if (!hasGps && inferred == null) continue; + if (hop == hopLastLast && widget.flipPathAround) { + continue; //skip duplicate hops in round-trip paths + } + if (!hasGps && inferred == null) { + hopLastLast = hopLast; + hopLast = hop; + continue; //skip hops with no GPS and no inferred position + } final point = hasGps ? LatLng(contact.latitude!, contact.longitude!) : inferred!; final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); + markers.add( Marker( point: point, @@ -532,6 +573,8 @@ class _PathTraceMapScreenState extends State { ), ); } + hopLastLast = hopLast; + hopLast = hop; } final selfLat = context.read().selfLatitude; @@ -581,9 +624,9 @@ class _PathTraceMapScreenState extends State { // Add target contact endpoint marker. final targetPos = _targetContactPosition; - if (targetPos != null) { + if (targetPos != null && target != null && target.type == advTypeChat) { final isGuessed = _targetContactIsGuessed; - final targetName = widget.targetContact?.name ?? '?'; + final targetName = target.name; markers.add( Marker( point: targetPos, @@ -719,6 +762,7 @@ class _PathTraceMapScreenState extends State { Widget _buildMapPathTrace( BuildContext context, MapTileCacheService tileCache, + Contact? target, ) { return FlutterMap( key: _mapKey, @@ -757,6 +801,7 @@ class _PathTraceMapScreenState extends State { markers: _buildHopMarkers( _traceData!.pathData, showLabels: _showNodeLabels, + target: target, ), ), ], diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index cf87382..11ab80a 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../services/app_settings_service.dart'; import '../utils/platform_info.dart'; import '../widgets/adaptive_app_bar_title.dart'; import 'contacts_screen.dart'; @@ -27,8 +28,14 @@ class _TcpScreenState extends State { @override void initState() { super.initState(); - _hostController = TextEditingController(); - _portController = TextEditingController(text: '5000'); + _hostController = TextEditingController( + text: context.read().settings.tcpServerAddress, + ); + _portController = TextEditingController( + text: context.read().settings.tcpServerPort > 0 + ? context.read().settings.tcpServerPort.toString() + : '', + ); _connector = context.read(); _connectionListener = () { @@ -39,6 +46,12 @@ class _TcpScreenState extends State { if (_connector.state == MeshCoreConnectionState.connected && _connector.isTcpTransportConnected && !_navigatedToContacts) { + context.read().setTcpServerAddress( + _hostController.text, + ); + context.read().setTcpServerPort( + int.tryParse(_portController.text) ?? 0, + ); _navigatedToContacts = true; Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const ContactsScreen()), diff --git a/lib/services/app_debug_log_service.dart b/lib/services/app_debug_log_service.dart index c63e625..d31c3e5 100644 --- a/lib/services/app_debug_log_service.dart +++ b/lib/services/app_debug_log_service.dart @@ -51,6 +51,7 @@ class AppDebugLogService extends ChangeNotifier { String message, { String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info, + bool noNotify = false, }) { if (!_enabled && !kDebugMode) return; if (!_enabled) { @@ -72,22 +73,24 @@ class AppDebugLogService extends ChangeNotifier { _entries.removeRange(0, _entries.length - maxEntries); } - notifyListeners(); + if (!noNotify) { + notifyListeners(); + } // Also print to console for development debugPrint('[$tag] $message'); } - void info(String message, {String tag = 'App'}) { - log(message, tag: tag, level: AppDebugLogLevel.info); + void info(String message, {String tag = 'App', bool noNotify = false}) { + log(message, tag: tag, level: AppDebugLogLevel.info, noNotify: noNotify); } - void warn(String message, {String tag = 'App'}) { - log(message, tag: tag, level: AppDebugLogLevel.warning); + void warn(String message, {String tag = 'App', bool noNotify = false}) { + log(message, tag: tag, level: AppDebugLogLevel.warning, noNotify: noNotify); } - void error(String message, {String tag = 'App'}) { - log(message, tag: tag, level: AppDebugLogLevel.error); + void error(String message, {String tag = 'App', bool noNotify = false}) { + log(message, tag: tag, level: AppDebugLogLevel.error, noNotify: noNotify); } void clear() { diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index a52e364..88c1f81 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -182,4 +182,12 @@ class AppSettingsService extends ChangeNotifier { ..remove(channelName); await updateSettings(_settings.copyWith(mutedChannels: updated)); } + + Future setTcpServerAddress(String value) async { + await updateSettings(_settings.copyWith(tcpServerAddress: value)); + } + + Future setTcpServerPort(int value) async { + await updateSettings(_settings.copyWith(tcpServerPort: value)); + } } diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index fca3d19..40861db 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -189,6 +189,10 @@ class UsbSerialService { serial.setStopBits1(); serial.setFlowControlNone(); serial.setRTS(false); + // Toggle DTR low→high so the device sees a fresh connection even + // if the previous disconnect didn't cleanly signal DTR drop. + serial.setDTR(false); + await Future.delayed(const Duration(milliseconds: 50)); serial.setDTR(true); _serial = serial; // Update the normalized port name to whichever candidate succeeded. @@ -249,6 +253,21 @@ class UsbSerialService { _status = UsbSerialStatus.connected; } + Future writeRaw(Uint8List data) async { + if (!isConnected) { + throw StateError('USB serial port is not open'); + } + if (_useAndroidUsbHost) { + try { + await _androidMethodChannel.invokeMethod('write', {'data': data}); + } on PlatformException catch (error) { + throw StateError(error.message ?? error.code); + } + } else { + _serial!.write(data); + } + } + Future write(Uint8List data) async { if (!isConnected) { throw StateError('USB serial port is not open'); @@ -300,6 +319,7 @@ class UsbSerialService { _serial = null; try { if (serial?.isOpen() == FlOpenStatus.open) { + serial?.setDTR(false); serial?.closePort(); } } catch (_) { @@ -350,6 +370,7 @@ class UsbSerialService { final serial = _serial; try { if (serial?.isOpen() == FlOpenStatus.open) { + serial?.setDTR(false); serial?.closePort(); // synchronous C call — kills the SerialThread } } catch (_) {} diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 4c83d7d..5261308 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -127,6 +127,17 @@ class UsbSerialService { } } + Future writeRaw(Uint8List data) async { + if (!isConnected || _writer == null) { + throw StateError('USB serial port is not open'); + } + final promise = _writer!.callMethod>( + 'write'.toJS, + data.toJS, + ); + await promise.toDart; + } + Future write(Uint8List data) async { if (!isConnected || _writer == null) { throw StateError('USB serial port is not open'); diff --git a/lib/utils/app_logger.dart b/lib/utils/app_logger.dart index e57261e..1f34a5e 100644 --- a/lib/utils/app_logger.dart +++ b/lib/utils/app_logger.dart @@ -23,23 +23,23 @@ class AppLogger { bool get isEnabled => _enabled; /// Log an info message - void info(String message, {String tag = 'App'}) { + void info(String message, {String tag = 'App', bool noNotify = false}) { if (_enabled && _service != null) { - _service!.info(message, tag: tag); + _service!.info(message, tag: tag, noNotify: noNotify); } } /// Log a warning message - void warn(String message, {String tag = 'App'}) { + void warn(String message, {String tag = 'App', bool noNotify = false}) { if (_enabled && _service != null) { - _service!.warn(message, tag: tag); + _service!.warn(message, tag: tag, noNotify: noNotify); } } /// Log an error message - void error(String message, {String tag = 'App'}) { + void error(String message, {String tag = 'App', bool noNotify = false}) { if (_enabled && _service != null) { - _service!.error(message, tag: tag); + _service!.error(message, tag: tag, noNotify: noNotify); } } @@ -48,9 +48,10 @@ class AppLogger { String message, { String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info, + bool noNotify = false, }) { if (_enabled && _service != null) { - _service!.log(message, tag: tag, level: level); + _service!.log(message, tag: tag, level: level, noNotify: noNotify); } } } diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index 384f92b..861241b 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -78,7 +78,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), - flipPathRound: true, + flipPathAround: true, targetContact: widget.contact, ), ), @@ -107,7 +107,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { } final pathForInput = currentContact.pathIdList; - final availableContacts = connector.contacts + final availableContacts = connector.allContacts .where((c) => c.publicKeyHex != currentContact.publicKeyHex) .toList(); diff --git a/lib/widgets/path_selection_dialog.dart b/lib/widgets/path_selection_dialog.dart index 4e6cfe5..b1733fc 100644 --- a/lib/widgets/path_selection_dialog.dart +++ b/lib/widgets/path_selection_dialog.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; @@ -65,7 +66,7 @@ class _PathSelectionDialogState extends State { void _filterValidContacts() { _validContacts = widget.availableContacts - .where((c) => c.type == 2 || c.type == 3) + .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) .toList(); } diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index f122836..30956e2 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -157,10 +157,7 @@ class _SNRIndicatorState extends State { repeater.snr, widget.connector.currentSf, ); - final allContacts = [ - ...widget.connector.contacts, - ...widget.connector.discoveredContacts, - ]; + final allContacts = widget.connector.allContacts; final name = allContacts .where((c) => c.publicKey.first == repeater.pubkeyFirstByte) .map((c) => c.name)