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

@@ -85,6 +85,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
double _fastLocationMovementThresholdMeters = 10.0;
int _fastLocationActiveCadenceSeconds = 10;
int? _fastLocationChannelIdx;
FastGpsRegionState? _fastGpsRegionState;
bool _rotateMapWithHeading = false;
bool _showMapDebugInfo = false;
bool _openMapInFullscreen = false;
@@ -410,6 +411,58 @@ class _SettingsScreenState extends State<SettingsScreen> {
_locationService.fastLocationActiveCadenceSeconds;
_fastLocationChannelIdx = _locationService.fastLocationChannelIdx;
});
await _loadFastGpsRegionState();
}
Future<void> _loadFastGpsRegionState() async {
final appProvider = context.read<AppProvider>();
FastGpsRegionState? regionState;
try {
regionState = await appProvider.getFastGpsRegionState();
} catch (_) {
regionState = null;
}
if (!mounted) return;
setState(() => _fastGpsRegionState = regionState);
}
Future<void> _editFastGpsRegion() async {
final regionState = _fastGpsRegionState;
if (regionState == null) return;
final selected = await showModalBottomSheet<int>(
context: context,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (var i = 0; i < regionState.labels.length; i++)
ListTile(
leading: Icon(i == 0 ? Icons.public : Icons.travel_explore),
title: Text(regionState.labels[i]),
trailing: regionState.selectedIndex == i
? const Icon(Icons.check)
: null,
onTap: () => Navigator.pop(sheetContext, i),
),
],
),
),
);
if (selected == null || selected == regionState.selectedIndex) return;
try {
await context.read<AppProvider>().setFastGpsRegion(selected);
await _loadFastGpsRegionState();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to set region: $e'),
backgroundColor: Colors.orange,
),
);
}
}
Future<void> _setFastLocationUpdatesEnabled(bool enabled) async {
@@ -1766,6 +1819,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
trailing: const Icon(Icons.chevron_right),
onTap: _editFastLocationChannel,
),
if (_fastGpsRegionState != null)
ListTile(
dense: true,
leading: const Icon(Icons.travel_explore, size: 20),
title: const Text('Region scope'),
subtitle: Text(_fastGpsRegionState!.selectedLabel),
trailing: const Icon(Icons.chevron_right),
onTap: _editFastGpsRegion,
),
ListTile(
dense: true,
leading: const Icon(Icons.send, size: 20),