mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
Merge pull request #297 from zjs81/dev-improments
Improvements to path tracing and location handling
This commit is contained in:
commit
9199aab7f7
19 changed files with 247 additions and 139 deletions
|
|
@ -294,6 +294,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
);
|
||||
}
|
||||
|
||||
List<Contact> get allContacts => List.unmodifiable([
|
||||
..._contacts,
|
||||
..._discoveredContacts.where((c) => !c.isActive),
|
||||
]);
|
||||
List<Contact> 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<void>.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);
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ class MeshCoreUsbManager {
|
|||
|
||||
Future<void> write(Uint8List data) => _service.write(data);
|
||||
|
||||
Future<void> writeRaw(Uint8List data) => _service.writeRaw(data);
|
||||
|
||||
// --- Label management ---
|
||||
void updateConnectedLabel(String selfName) {
|
||||
_service.updateConnectedLabel(selfName);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ class AppSettings {
|
|||
final UnitSystem unitSystem;
|
||||
final Set<String> 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<String>? 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<String>? 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = <String>[];
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -40,10 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||
final primaryPath = !channelMessage && !message.isOutgoing
|
||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||
: primaryPathTmp;
|
||||
final contacts = <Contact>[
|
||||
...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 = <Contact>[
|
||||
...connector.contacts,
|
||||
...connector.discoveredContacts,
|
||||
];
|
||||
final contacts = connector.allContacts;
|
||||
final hops = _buildPathHops(selectedPath, contacts, context.l10n);
|
||||
|
||||
final points = <LatLng>[];
|
||||
|
|
|
|||
|
|
@ -858,7 +858,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
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<ChatScreen> {
|
|||
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();
|
||||
|
||||
|
|
|
|||
|
|
@ -1239,7 +1239,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
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<ContactsScreen>
|
|||
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<ContactsScreen>
|
|||
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<ContactsScreen>
|
|||
title: context.l10n.contacts_pathTraceTo(
|
||||
contact.name,
|
||||
),
|
||||
path: contact.traceRouteBytes ?? Uint8List(0),
|
||||
path: contact.pathBytesForDisplay,
|
||||
flipPathAround: true,
|
||||
targetContact: contact,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -137,10 +137,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||
builder: (context, connector, settingsService, pathHistory, child) {
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final settings = settingsService.settings;
|
||||
final allContacts = <Contact>[
|
||||
...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<MapScreen> {
|
|||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -124,10 +124,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
|||
|
||||
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||
final buffer = BufferReader(frame);
|
||||
final contacts = <Contact>[
|
||||
...connector.contacts,
|
||||
...connector.discoveredContacts,
|
||||
];
|
||||
final contacts = connector.allContacts;
|
||||
try {
|
||||
final neighborCount = buffer.readUInt16LE();
|
||||
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
||||
|
|
|
|||
|
|
@ -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<PathTraceMapScreen> {
|
|||
ValueKey<String> _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<PathTraceMapScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
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<MeshCoreConnector>(context, listen: false);
|
||||
|
|
@ -263,10 +259,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||
.toList();
|
||||
|
||||
Map<int, Contact> pathContacts = {};
|
||||
final contacts = <Contact>[
|
||||
...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<PathTraceMapScreen> {
|
|||
// 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<PathTraceMapScreen> {
|
|||
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<PathTraceMapScreen> {
|
|||
|
||||
_points = <LatLng>[];
|
||||
_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<PathTraceMapScreen> {
|
|||
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<PathTraceMapScreen> {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (_hasData) _buildMapPathTrace(context, tileCache),
|
||||
if (_hasData)
|
||||
_buildMapPathTrace(context, tileCache, _targetContact),
|
||||
if (_points.isEmpty &&
|
||||
!_hasData &&
|
||||
!_isLoading &&
|
||||
|
|
@ -480,17 +510,28 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||
List<Marker> _buildHopMarkers(
|
||||
List<int> pathData, {
|
||||
required bool showLabels,
|
||||
required Contact? target,
|
||||
}) {
|
||||
final markers = <Marker>[];
|
||||
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<PathTraceMapScreen> {
|
|||
),
|
||||
);
|
||||
}
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hop;
|
||||
}
|
||||
|
||||
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||
|
|
@ -581,9 +624,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||
|
||||
// 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<PathTraceMapScreen> {
|
|||
Widget _buildMapPathTrace(
|
||||
BuildContext context,
|
||||
MapTileCacheService tileCache,
|
||||
Contact? target,
|
||||
) {
|
||||
return FlutterMap(
|
||||
key: _mapKey,
|
||||
|
|
@ -757,6 +801,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||
markers: _buildHopMarkers(
|
||||
_traceData!.pathData,
|
||||
showLabels: _showNodeLabels,
|
||||
target: target,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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<TcpScreen> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_hostController = TextEditingController();
|
||||
_portController = TextEditingController(text: '5000');
|
||||
_hostController = TextEditingController(
|
||||
text: context.read<AppSettingsService>().settings.tcpServerAddress,
|
||||
);
|
||||
_portController = TextEditingController(
|
||||
text: context.read<AppSettingsService>().settings.tcpServerPort > 0
|
||||
? context.read<AppSettingsService>().settings.tcpServerPort.toString()
|
||||
: '',
|
||||
);
|
||||
_connector = context.read<MeshCoreConnector>();
|
||||
|
||||
_connectionListener = () {
|
||||
|
|
@ -39,6 +46,12 @@ class _TcpScreenState extends State<TcpScreen> {
|
|||
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
_connector.isTcpTransportConnected &&
|
||||
!_navigatedToContacts) {
|
||||
context.read<AppSettingsService>().setTcpServerAddress(
|
||||
_hostController.text,
|
||||
);
|
||||
context.read<AppSettingsService>().setTcpServerPort(
|
||||
int.tryParse(_portController.text) ?? 0,
|
||||
);
|
||||
_navigatedToContacts = true;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -182,4 +182,12 @@ class AppSettingsService extends ChangeNotifier {
|
|||
..remove(channelName);
|
||||
await updateSettings(_settings.copyWith(mutedChannels: updated));
|
||||
}
|
||||
|
||||
Future<void> setTcpServerAddress(String value) async {
|
||||
await updateSettings(_settings.copyWith(tcpServerAddress: value));
|
||||
}
|
||||
|
||||
Future<void> setTcpServerPort(int value) async {
|
||||
await updateSettings(_settings.copyWith(tcpServerPort: value));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void>.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<void> writeRaw(Uint8List data) async {
|
||||
if (!isConnected) {
|
||||
throw StateError('USB serial port is not open');
|
||||
}
|
||||
if (_useAndroidUsbHost) {
|
||||
try {
|
||||
await _androidMethodChannel.invokeMethod<void>('write', {'data': data});
|
||||
} on PlatformException catch (error) {
|
||||
throw StateError(error.message ?? error.code);
|
||||
}
|
||||
} else {
|
||||
_serial!.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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 (_) {}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,17 @@ class UsbSerialService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> writeRaw(Uint8List data) async {
|
||||
if (!isConnected || _writer == null) {
|
||||
throw StateError('USB serial port is not open');
|
||||
}
|
||||
final promise = _writer!.callMethod<JSPromise<JSAny?>>(
|
||||
'write'.toJS,
|
||||
data.toJS,
|
||||
);
|
||||
await promise.toDart;
|
||||
}
|
||||
|
||||
Future<void> write(Uint8List data) async {
|
||||
if (!isConnected || _writer == null) {
|
||||
throw StateError('USB serial port is not open');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<PathSelectionDialog> {
|
|||
|
||||
void _filterValidContacts() {
|
||||
_validContacts = widget.availableContacts
|
||||
.where((c) => c.type == 2 || c.type == 3)
|
||||
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -157,10 +157,7 @@ class _SNRIndicatorState extends State<SNRIndicator> {
|
|||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue