feat: align fast-GPS beacons with MeshUI firmware (16-byte format + cadence parity)

Switch the fast-GPS beacon to the firmware's canonical 16-byte wire format
(magic, key6, lat/lon microdegrees, speed km/h) — no on-wire timestamp; the
receiver stamps its own RX time. Drops compatibility with the older 19-byte app
format.

Mirror the firmware's adaptive send cadence in the app-fallback path
(LocationTrackingService): anchor + dwell motion hysteresis, EMA ground speed,
9-min stationary keepalive, fix-quality (accuracy) gate, and a post-RX hold-off
to avoid channel collisions. Parked nodes now beacon the stable anchor instead
of GPS wander.

Add the fast_gps_region config: region-scope picker in settings backed by the
firmware's fast_gps_region / fast_gps_regions custom vars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Janez T
2026-06-24 13:17:02 +02:00
parent 3283d4ca20
commit 3c08e150ef
7 changed files with 490 additions and 113 deletions

View File

@@ -762,10 +762,11 @@ void main() {
senderKey6: '323334353637',
latitude: 44.123456,
longitude: 13.654321,
timestampSeconds: 1700001234,
speedKmh: 12,
),
);
final nowSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final updated = provider.findContactByKey(publicKey)!;
expect(updated.telemetry, isNotNull);
expect(updated.telemetry!.gpsLocation, isNotNull);
@@ -780,8 +781,9 @@ void main() {
expect(updated.telemetry!.batteryMilliVolts, isNotNull);
expect(updated.advLat, equals((44.123456 * 1e6).round()));
expect(updated.advLon, equals((13.654321 * 1e6).round()));
expect(updated.lastAdvert, equals(1700001234));
expect(updated.lastMod, equals(1700001234));
// The beacon carries no timestamp; the receiver stamps its own RX time.
expect(updated.lastAdvert, closeTo(nowSeconds, 5));
expect(updated.lastMod, closeTo(nowSeconds, 5));
expect(
provider.estimatedLocationFor(updated.publicKeyHex),
isNotNull,
@@ -804,7 +806,7 @@ void main() {
senderKey6: '010203040506',
latitude: 10,
longitude: 20,
timestampSeconds: 99,
speedKmh: 0,
),
);
final after = provider.findContactByKey(publicKey)!;

View File

@@ -6,22 +6,38 @@ import 'package:meshcore_sar_app/utils/fast_gps_packet.dart';
void main() {
group('FastGpsPacket', () {
test('encodes and parses a valid packet', () {
test('encodes and parses a valid 16-byte packet', () {
final packet = FastGpsPacket(
senderKey6: 'aabbccddeeff',
latitude: 46.0569,
longitude: 14.5058,
timestampSeconds: 1700000000,
speedKmh: 37,
);
final encoded = packet.encodeBinary();
expect(encoded.length, equals(16));
expect(encoded[0], equals(FastGpsPacket.magic));
final parsed = FastGpsPacket.tryParseBinary(encoded);
expect(parsed, isNotNull);
expect(parsed!.senderKey6, equals('aabbccddeeff'));
expect(parsed.latitude, closeTo(46.0569, 0.000001));
expect(parsed.longitude, closeTo(14.5058, 0.000001));
expect(parsed.timestampSeconds, equals(1700000000));
expect(parsed.speedKmh, equals(37));
});
test('clamps speed to a single unsigned byte', () {
final packet = FastGpsPacket(
senderKey6: '001122334455',
latitude: 1.0,
longitude: 2.0,
speedKmh: 999,
);
final parsed = FastGpsPacket.tryParseBinary(packet.encodeBinary());
expect(parsed, isNotNull);
expect(parsed!.speedKmh, equals(255));
});
test('supports negative coordinates', () {
@@ -29,7 +45,7 @@ void main() {
senderKey6: '001122334455',
latitude: -33.8688,
longitude: -151.2093,
timestampSeconds: 42,
speedKmh: 0,
);
final parsed = FastGpsPacket.tryParseBinary(packet.encodeBinary());
@@ -39,20 +55,29 @@ void main() {
});
test('rejects malformed payloads', () {
// Too short.
expect(
FastGpsPacket.tryParseBinary(Uint8List.fromList([0x47, 0x01])),
isNull,
);
// Wrong magic at the correct length.
expect(
FastGpsPacket.tryParseBinary(
Uint8List.fromList(List<int>.filled(19, 0)..[0] = 0x48),
Uint8List.fromList(List<int>.filled(16, 0)..[0] = 0x48),
),
isNull,
);
// Legacy 19-byte format is no longer accepted.
expect(
FastGpsPacket.tryParseBinary(
Uint8List.fromList(List<int>.filled(19, 0)..[0] = 0x47),
),
isNull,
);
});
test('rejects invalid coordinate ranges', () {
final payload = Uint8List(19);
final payload = Uint8List(16);
payload[0] = FastGpsPacket.magic;
payload.setRange(1, 7, [0, 1, 2, 3, 4, 5]);
final data = ByteData.sublistView(payload);
@@ -66,7 +91,7 @@ void main() {
(14.5 * FastGpsPacket.coordinateScale).round(),
Endian.little,
);
data.setUint32(15, 1, Endian.little);
data.setUint8(15, 1);
expect(FastGpsPacket.tryParseBinary(payload), isNull);
});
@@ -78,7 +103,7 @@ void main() {
senderKey6: 'aabbccddeeff',
latitude: latitude,
longitude: longitude,
timestampSeconds: 1700000000,
speedKmh: 5,
);
final parsed = FastGpsPacket.tryParseBinary(packet.encodeBinary());