mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-17 07:56:28 +10:00
Merge branch 'dev' of https://github.com/zjs81/meshcore-open into dev
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -537,27 +537,39 @@ class TranslationService extends ChangeNotifier {
|
||||
final lower = trimmed.toLowerCase();
|
||||
final patterns = <String, String>{
|
||||
'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 = <String, int>{};
|
||||
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()
|
||||
|
||||
@@ -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<int>.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());
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user