From 5572c9ee7543556a360dcdd81795d9ca92eca7e2 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Tue, 5 May 2026 10:59:36 -0700 Subject: [PATCH 1/3] increase CR for off grid default should be higher assuming trees etc --- lib/models/radio_settings.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/models/radio_settings.dart b/lib/models/radio_settings.dart index 37ef3ccb..099d9201 100644 --- a/lib/models/radio_settings.dart +++ b/lib/models/radio_settings.dart @@ -228,7 +228,7 @@ class RadioSettings { frequencyMHz: 433.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, - codingRate: LoRaCodingRate.cr4_5, + codingRate: LoRaCodingRate.cr4_8, txPowerDbm: 20, ), ), @@ -238,7 +238,7 @@ class RadioSettings { frequencyMHz: 869.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, - codingRate: LoRaCodingRate.cr4_5, + codingRate: LoRaCodingRate.cr4_8, txPowerDbm: 14, ), ), @@ -248,7 +248,7 @@ class RadioSettings { frequencyMHz: 918.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, - codingRate: LoRaCodingRate.cr4_5, + codingRate: LoRaCodingRate.cr4_8, txPowerDbm: 20, ), ), From ae32e76563b2524769358a5241b667446300e454 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Tue, 5 May 2026 11:07:32 -0700 Subject: [PATCH 2/3] fix someones formatting --- lib/services/translation_service.dart | 40 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/lib/services/translation_service.dart b/lib/services/translation_service.dart index 46cf7ea8..294a7848 100644 --- a/lib/services/translation_service.dart +++ b/lib/services/translation_service.dart @@ -537,27 +537,39 @@ class TranslationService extends ChangeNotifier { final lower = trimmed.toLowerCase(); final patterns = { 'uk': r'\b(привіт|дякую|будь|ласка|як|де|не|так|це|є|най|ще|може|для)\b', - 'ru': r'\b(что|это|как|не|да|нет|он|она|они|быть|есть|для|сегодня|если|уже|может)\b', + 'ru': + r'\b(что|это|как|не|да|нет|он|она|они|быть|есть|для|сегодня|если|уже|может)\b', 'bg': r'\b(ще|няма|благодаря|моля|това|какво|тук|ние|вие|не|със|за)\b', - 'de': r'\b(der|die|das|und|ist|nicht|ein|eine|ich|für|mit|auf|zu|auch|als|an|im|am|es|dem|den|sich|von)\b', - 'en': r'\b(the|and|is|you|for|with|from|not|that|this|have|be|are|was|were|but|can|will|your|what|when|how|they)\b', - 'es': r'\b(el|la|los|las|es|que|de|en|con|por|para|no|un|una|se|como|su|al|del|está)\b', - 'fr': r'\b(le|la|les|un|une|et|est|que|qui|pour|dans|pas|avec|sur|ne|vous|il|elle|des|ce|cette|je|tu|nous|vous)\b', - 'it': r'\b(il|la|lo|un|una|che|di|da|in|per|con|non|si|mi|ti|noi|voi|lui|lei)\b', - 'pt': r'\b(os|as|que|de|do|da|em|para|com|por|não|uma|um|se|você|também)\b', - 'nl': r'\b(de|het|een|en|is|niet|dat|wat|je|ik|op|aan|voor|met|als|nog|zijn)\b', - 'sv': r'\b(och|är|det|att|som|en|på|inte|har|var|men|du|jag|vi|ni|den|detta)\b', - 'pl': r'\b(na|się|nie|jest|to|że|do|od|dla|czy|tak|ale|ma|jak|on|ona|my)\b', + 'de': + r'\b(der|die|das|und|ist|nicht|ein|eine|ich|für|mit|auf|zu|auch|als|an|im|am|es|dem|den|sich|von)\b', + 'en': + r'\b(the|and|is|you|for|with|from|not|that|this|have|be|are|was|were|but|can|will|your|what|when|how|they)\b', + 'es': + r'\b(el|la|los|las|es|que|de|en|con|por|para|no|un|una|se|como|su|al|del|está)\b', + 'fr': + r'\b(le|la|les|un|une|et|est|que|qui|pour|dans|pas|avec|sur|ne|vous|il|elle|des|ce|cette|je|tu|nous|vous)\b', + 'it': + r'\b(il|la|lo|un|una|che|di|da|in|per|con|non|si|mi|ti|noi|voi|lui|lei)\b', + 'pt': + r'\b(os|as|que|de|do|da|em|para|com|por|não|uma|um|se|você|também)\b', + 'nl': + r'\b(de|het|een|en|is|niet|dat|wat|je|ik|op|aan|voor|met|als|nog|zijn)\b', + 'sv': + r'\b(och|är|det|att|som|en|på|inte|har|var|men|du|jag|vi|ni|den|detta)\b', + 'pl': + r'\b(na|się|nie|jest|to|że|do|od|dla|czy|tak|ale|ma|jak|on|ona|my)\b', 'sk': r'\b(je|na|so|že|do|od|za|si|to|ten|tá|tí|ako|má|nie|som|sa)\b', 'sl': r'\b(in|je|na|se|da|za|od|ne|to|ta|so|kako|bo|sem|si)\b', - 'hu': r'\b(az|és|nem|van|volt|hogy|mit|mire|ki|mi|ez|azért|is|de|ha|te|ő|mi|itt)\b', + 'hu': + r'\b(az|és|nem|van|volt|hogy|mit|mire|ki|mi|ez|azért|is|de|ha|te|ő|mi|itt)\b', }; final scores = {}; for (final entry in patterns.entries) { - scores[entry.key] = RegExp(entry.value, caseSensitive: false) - .allMatches(lower) - .length; + scores[entry.key] = RegExp( + entry.value, + caseSensitive: false, + ).allMatches(lower).length; } final sorted = scores.entries.toList() From dfcf13a97b561da3c54258a227336ca158c722c4 Mon Sep 17 00:00:00 2001 From: kingult <82867213+kingult@users.noreply.github.com> Date: Wed, 6 May 2026 16:03:03 -0700 Subject: [PATCH 3/3] fix: lat/lon double-write in buildUpdateContactPathFrame The function emitted two consecutive 8-byte position blocks instead of one, producing a frame 8 bytes longer than the documented layout. When a caller passed lastModified, the firmware parsed the duplicated second lat as the timestamp, giving wildly wrong "last seen" values on imported contacts. Delete the unconditional first block; keep the conditional block that correctly skips the optional tail when neither location nor lastModified is set, zero-fills position slots when only lastModified is present, and appends the optional timestamp. Adds test/connector/build_update_contact_path_frame_test.dart with five cases covering all four optional-tail combinations plus the fixed-point lat/lon encoding. Fixes #427 --- lib/connector/meshcore_protocol.dart | 21 +--- .../build_update_contact_path_frame_test.dart | 105 ++++++++++++++++++ 2 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 test/connector/build_update_contact_path_frame_test.dart diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index c212acc3..f55e9687 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -720,27 +720,16 @@ Uint8List buildUpdateContactPathFrame( final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; writer.writeUInt32LE(timestamp); - if ((lat == null || lon == null) && lastModified != null) { - // If lat/lon not provided, write zeros - writer.writeInt32LE(0); - writer.writeInt32LE(0); - } else { - // Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6 - // Latitude - final latitude = lat ?? 0.0; - writer.writeInt32LE((latitude * 1e6).round()); - - // Longitude - final longitude = lon ?? 0.0; - writer.writeInt32LE((longitude * 1e6).round()); - } - + // Optional [Lat x4, Lon x4][timestamp x4] tail per the doc comment above. + // Emit 8 bytes of position (zero-filled when only lastModified is provided) + // followed by an optional 4-byte timestamp. Earlier code emitted the + // position block twice, which corrupted the tail and caused the firmware + // to parse the second lat as the timestamp. See #427. final hasLocation = lat != null && lon != null; if (hasLocation || lastModified != null) { writer.writeInt32LE(hasLocation ? (lat * 1e6).round() : 0); writer.writeInt32LE(hasLocation ? (lon * 1e6).round() : 0); if (lastModified != null) { - // Last modified final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000; writer.writeUInt32LE(lastModifiedTimestamp); } diff --git a/test/connector/build_update_contact_path_frame_test.dart b/test/connector/build_update_contact_path_frame_test.dart new file mode 100644 index 00000000..da88f9aa --- /dev/null +++ b/test/connector/build_update_contact_path_frame_test.dart @@ -0,0 +1,105 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; + +void main() { + // Frame layout per the doc comment on buildUpdateContactPathFrame: + // [cmd][pub_key x32][type][flags][path_len][path x64][name x32] + // [timestamp x4][Lat? x4, Lon? x4][timestamp? x4] + // + // Base (mandatory) bytes: + // 1 cmd + 32 pubKey + 1 type + 1 flags + 1 pathLen + 64 path + // + 32 name + 4 timestamp = 136 bytes + const int baseFrameLength = 136; + + final pubKey = Uint8List.fromList(List.generate(32, (i) => i)); + final path = Uint8List.fromList([0xAA, 0xBB]); + + group('buildUpdateContactPathFrame', () { + test('omits lat/lon and timestamp tail when neither is provided', () { + final frame = buildUpdateContactPathFrame( + pubKey, + path, + path.length, + name: 'Alice', + ); + + // Should be exactly the base frame, no optional tail. + expect(frame.length, baseFrameLength); + }); + + test('appends only an 8-byte lat/lon tail when location is provided', () { + final frame = buildUpdateContactPathFrame( + pubKey, + path, + path.length, + lat: 49.123456, + lon: -123.123456, + ); + + expect(frame.length, baseFrameLength + 8); + }); + + test( + 'appends 8 bytes lat/lon + 4 bytes timestamp when both are provided', + () { + final frame = buildUpdateContactPathFrame( + pubKey, + path, + path.length, + lat: 49.0, + lon: -123.0, + lastModified: DateTime.utc(2026, 1, 2, 3, 4, 5), + ); + + expect(frame.length, baseFrameLength + 8 + 4); + }, + ); + + test('zero-fills the lat/lon slots and appends timestamp when only ' + 'lastModified is provided', () { + final frame = buildUpdateContactPathFrame( + pubKey, + path, + path.length, + lastModified: DateTime.utc(2026, 1, 2, 3, 4, 5), + ); + + // 8 zero bytes for lat/lon + 4 bytes timestamp + expect(frame.length, baseFrameLength + 8 + 4); + + // Verify the lat/lon slot is actually zero — guards against a + // regression where the function writes garbage into those bytes. + final tailStart = baseFrameLength; + for (var i = tailStart; i < tailStart + 8; i++) { + expect(frame[i], 0, reason: 'byte $i in lat/lon slot must be 0'); + } + }); + + test('encodes positive lat/lon as little-endian fixed-point (×1e6)', () { + final frame = buildUpdateContactPathFrame( + pubKey, + path, + path.length, + lat: 49.123456, + lon: -123.123456, + ); + + // Latitude is the first 4 bytes of the optional tail. + final latBytes = ByteData.sublistView( + frame, + baseFrameLength, + baseFrameLength + 4, + ); + final lonBytes = ByteData.sublistView( + frame, + baseFrameLength + 4, + baseFrameLength + 8, + ); + + expect(latBytes.getInt32(0, Endian.little), (49.123456 * 1e6).round()); + expect(lonBytes.getInt32(0, Endian.little), (-123.123456 * 1e6).round()); + }); + }); +}