chore: Initial commit

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPovRHpejR4zRxuhjY4Qs3
This commit is contained in:
Janez Troha
2026-09-04 16:53:52 +02:00
commit 7924803351
516 changed files with 207265 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
import 'dart:typed_data';
/// Fast-GPS position beacon, wire-compatible with the MeshUI firmware
/// (`maybeSendFastGpsUpdate` in MyMesh.cpp).
///
/// 16-byte binary payload sent as a `DATA_TYPE_DEV` group datagram on a
/// non-public channel:
///
/// ```
/// [0] magic 0x47 ('G')
/// [1..6] sender public-key prefix (6 bytes)
/// [7..10] latitude in microdegrees (int32, little-endian)
/// [11..14] longitude in microdegrees (int32, little-endian)
/// [15] ground speed in km/h (uint8, clamped 0..255)
/// ```
///
/// The beacon carries no timestamp — receivers stamp their own RX time.
class FastGpsPacket {
static const int magic = 0x47; // 'G'
static const int _payloadLength = 16;
// Store coordinates in microdegrees. This preserves sub-meter precision,
// which comfortably satisfies the meter-accuracy requirement.
static const double coordinateScale = 1e6;
final String senderKey6;
final double latitude;
final double longitude;
/// Ground speed in km/h (0..255), as carried in the beacon's trailing byte.
final int speedKmh;
const FastGpsPacket({
required this.senderKey6,
required this.latitude,
required this.longitude,
this.speedKmh = 0,
});
static bool isFastGpsBinary(Uint8List payload) =>
payload.length == _payloadLength && payload[0] == magic;
static FastGpsPacket? tryParseBinary(Uint8List payload) {
if (!isFastGpsBinary(payload)) return null;
final key6 = payload
.sublist(1, 7)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
final data = ByteData.sublistView(payload);
final latitude = data.getInt32(7, Endian.little) / coordinateScale;
final longitude = data.getInt32(11, Endian.little) / coordinateScale;
final speedKmh = data.getUint8(15);
if (!_isValidCoordinate(latitude, longitude)) {
return null;
}
return FastGpsPacket(
senderKey6: key6,
latitude: latitude,
longitude: longitude,
speedKmh: speedKmh,
);
}
Uint8List encodeBinary() {
final out = Uint8List(_payloadLength);
final data = ByteData.sublistView(out);
out[0] = magic;
for (var i = 0; i < 6; i++) {
out[1 + i] = int.parse(senderKey6.substring(i * 2, i * 2 + 2), radix: 16);
}
data.setInt32(7, (latitude * coordinateScale).round(), Endian.little);
data.setInt32(11, (longitude * coordinateScale).round(), Endian.little);
data.setUint8(15, speedKmh.clamp(0, 255));
return out;
}
static bool _isValidCoordinate(double latitude, double longitude) {
if (!latitude.isFinite || !longitude.isFinite) return false;
return latitude >= -90.0 &&
latitude <= 90.0 &&
longitude >= -180.0 &&
longitude <= 180.0;
}
}