Compare commits

...

3 Commits

Author SHA1 Message Date
Janez T
ba0f1fc141 feat: Add fast GPS send logs 2026-03-23 21:00:41 +01:00
Janez T
28a9235168 fix: Tame rapid GPS updates #41 2026-03-23 20:51:48 +01:00
Janez T
3e3c9a34d5 fix: Keep received replays separate 2026-03-23 20:07:06 +01:00
13 changed files with 196 additions and 42 deletions

View File

@@ -489,7 +489,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -511,7 +511,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -530,7 +530,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -547,7 +547,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -679,7 +679,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -702,7 +702,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -43,7 +43,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>125</string> <string>128</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -5,22 +5,22 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000216"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.00073">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="1.086406"> <testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.659743">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="96.919368"> <testcase classname="fastlane.lanes" name="2: build_app" time="105.547745">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="563.579992"> <testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="252.935458">
</testcase> </testcase>

View File

@@ -2879,8 +2879,13 @@ class AppProvider with ChangeNotifier {
timestampSeconds: DateTime.now().millisecondsSinceEpoch ~/ 1000, timestampSeconds: DateTime.now().millisecondsSinceEpoch ~/ 1000,
); );
debugPrint( debugPrint(
'📍 [AppProvider] Sending fast GPS update ($reason): ' '📤 [AppProvider] Fast GPS send '
'${position.latitude}, ${position.longitude} via channel $channelIdx', 'reason=$reason '
'sender=$senderKey6 '
'channel=$channelIdx '
'lat=${position.latitude} '
'lon=${position.longitude} '
'ts=${packet.timestampSeconds}',
); );
try { try {
await connectionProvider.sendChannelData( await connectionProvider.sendChannelData(
@@ -2888,11 +2893,32 @@ class AppProvider with ChangeNotifier {
dataType: MeshCoreConstants.dataTypeDev, dataType: MeshCoreConstants.dataTypeDev,
payload: packet.encodeBinary(), payload: packet.encodeBinary(),
); );
debugPrint(
'✅ [AppProvider] Fast GPS sent '
'sender=$senderKey6 channel=$channelIdx ts=${packet.timestampSeconds}',
);
} catch (e) { } catch (e) {
debugPrint('⚠️ [AppProvider] Fast GPS send failed: $e'); debugPrint('⚠️ [AppProvider] Fast GPS send failed: $e');
} }
} }
Future<bool> sendTestFastLocationUpdate() async {
if (!connectionProvider.deviceInfo.isConnected) {
return false;
}
final position = await locationTrackingService.getCurrentPosition(
timeLimit: const Duration(seconds: 10),
retryCount: 1,
);
if (position == null) {
return false;
}
await _sendFastLocationUpdate(position, reason: 'test');
return true;
}
Contact? _resolveVoiceFetchRequester(VoiceFetchRequest request) { Contact? _resolveVoiceFetchRequester(VoiceFetchRequest request) {
final liveContact = _resolveContactByPrefixHex(request.requesterKey6); final liveContact = _resolveContactByPrefixHex(request.requesterKey6);
if (liveContact != null) { if (liveContact != null) {

View File

@@ -1143,13 +1143,34 @@ class ContactsProvider with ChangeNotifier {
), ),
); );
debugPrint(
'📥 [ContactsProvider] Fast GPS received '
'sender=${packet.senderKey6} '
'contact=${contact.displayName} '
'contactKey=${contact.publicKeyHex} '
'lat=${packet.latitude} '
'lon=${packet.longitude} '
'ts=${packet.timestampSeconds}',
);
final updatedContact = contact.copyWith( final updatedContact = contact.copyWith(
telemetry: updatedTelemetry, telemetry: updatedTelemetry,
lastAdvert: packet.timestampSeconds, lastAdvert: packet.timestampSeconds,
lastMod: packet.timestampSeconds,
advLat: _coordinateToAdvertMicrodegrees(packet.latitude), advLat: _coordinateToAdvertMicrodegrees(packet.latitude),
advLon: _coordinateToAdvertMicrodegrees(packet.longitude), advLon: _coordinateToAdvertMicrodegrees(packet.longitude),
); );
_contacts[contact.publicKeyHex] = updatedContact; _contacts[contact.publicKeyHex] = updatedContact;
_estimatedLocations[contact.publicKeyHex] = LatLng(
packet.latitude,
packet.longitude,
);
debugPrint(
'✅ [ContactsProvider] Fast GPS applied '
'contact=${updatedContact.displayName} '
'contactKey=${updatedContact.publicKeyHex} '
'lastAdvert=${updatedContact.lastAdvert}',
);
_persistContacts(); _persistContacts();
notifyListeners(); notifyListeners();
} }

View File

@@ -664,6 +664,11 @@ class MessagesProvider with ChangeNotifier {
return; // Skip duplicate return; // Skip duplicate
} }
final matchingSentReplayIndex = _findMatchingSentReplayIndex(finalMessage);
if (matchingSentReplayIndex != -1) {
_clearChannelSendWarning(_messages[matchingSentReplayIndex].id);
}
_messages.add(finalMessage); _messages.add(finalMessage);
if (contactLocationSnapshot != null) { if (contactLocationSnapshot != null) {
_messageContactLocations[finalMessage.id] = contactLocationSnapshot; _messageContactLocations[finalMessage.id] = contactLocationSnapshot;
@@ -712,7 +717,8 @@ class MessagesProvider with ChangeNotifier {
for (int index = 0; index < _messages.length; index++) { for (int index = 0; index < _messages.length; index++) {
final existing = _messages[index]; final existing = _messages[index];
if (!_matchesDuplicateScope(existing, message) || if (existing.isSentMessage ||
!_matchesDuplicateScope(existing, message) ||
existing.text != message.text) { existing.text != message.text) {
continue; continue;
} }
@@ -790,6 +796,27 @@ class MessagesProvider with ChangeNotifier {
existingSenderName == incomingSenderName; existingSenderName == incomingSenderName;
} }
int _findMatchingSentReplayIndex(Message message) {
if (!message.isChannelMessage || message.isSentMessage) {
return -1;
}
for (int index = 0; index < _messages.length; index++) {
final existing = _messages[index];
if (!existing.isSentMessage ||
!existing.isChannelMessage ||
existing.text != message.text) {
continue;
}
if (_matchesDuplicateScope(existing, message)) {
return index;
}
}
return -1;
}
/// Add multiple messages /// Add multiple messages
void addMessages(List<Message> messages) { void addMessages(List<Message> messages) {
int addedCount = 0; int addedCount = 0;

View File

@@ -341,7 +341,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
keyboardType: const TextInputType.numberWithOptions(decimal: true), keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Meters', labelText: 'Meters',
helperText: 'Valid range: 1 to 1000 meters', helperText: 'Valid range: 10 to 1000 meters',
), ),
), ),
actions: [ actions: [
@@ -353,7 +353,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
onPressed: () { onPressed: () {
final parsed = double.tryParse(controller.text.trim()); final parsed = double.tryParse(controller.text.trim());
if (parsed == null) return; if (parsed == null) return;
Navigator.pop(context, parsed.clamp(1.0, 1000.0)); Navigator.pop(context, parsed.clamp(10.0, 1000.0));
}, },
child: const Text('Save'), child: const Text('Save'),
), ),
@@ -382,7 +382,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Seconds', labelText: 'Seconds',
helperText: 'Valid range: 5 to 60 seconds', helperText: 'Valid range: 10 to 31 seconds',
), ),
), ),
actions: [ actions: [
@@ -394,7 +394,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
onPressed: () { onPressed: () {
final parsed = int.tryParse(controller.text.trim()); final parsed = int.tryParse(controller.text.trim());
if (parsed == null) return; if (parsed == null) return;
Navigator.pop(context, parsed.clamp(5, 60)); Navigator.pop(context, parsed.clamp(10, 31));
}, },
child: const Text('Save'), child: const Text('Save'),
), ),
@@ -426,6 +426,23 @@ class _SettingsScreenState extends State<SettingsScreen> {
return 'Channel $channelIdx unavailable'; return 'Channel $channelIdx unavailable';
} }
Future<void> _sendTestFastLocationUpdate() async {
final appProvider = context.read<AppProvider>();
final sent = await appProvider.sendTestFastLocationUpdate();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
sent
? 'Test fast GPS update sent.'
: 'Unable to send test fast GPS update.',
),
backgroundColor: sent ? Colors.green : Colors.orange,
),
);
}
Future<void> _editFastLocationChannel() async { Future<void> _editFastLocationChannel() async {
final channels = final channels =
List<Contact>.from(context.read<ContactsProvider>().channels) List<Contact>.from(context.read<ContactsProvider>().channels)
@@ -1854,6 +1871,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: _editFastLocationChannel, onTap: _editFastLocationChannel,
), ),
ListTile(
leading: const Icon(Icons.send),
title: const Text('Test send update'),
subtitle: const Text(
'Send one fast GPS update immediately to the configured channel.',
),
trailing: const Icon(Icons.chevron_right),
onTap: _sendTestFastLocationUpdate,
),
ListTile( ListTile(
leading: Icon(Icons.location_on), leading: Icon(Icons.location_on),
title: Text(AppLocalizations.of(context)!.locationPermission), title: Text(AppLocalizations.of(context)!.locationPermission),

View File

@@ -144,11 +144,11 @@ class AppConfigSnapshotService {
} }
if (section.fastLocationMovementThresholdMeters != null) { if (section.fastLocationMovementThresholdMeters != null) {
locationTracking.fastLocationMovementThresholdMeters = locationTracking.fastLocationMovementThresholdMeters =
section.fastLocationMovementThresholdMeters!; section.fastLocationMovementThresholdMeters!.clamp(10.0, 1000.0);
} }
if (section.fastLocationActiveCadenceSeconds != null) { if (section.fastLocationActiveCadenceSeconds != null) {
locationTracking.fastLocationActiveCadenceSeconds = locationTracking.fastLocationActiveCadenceSeconds =
section.fastLocationActiveCadenceSeconds!; section.fastLocationActiveCadenceSeconds!.clamp(10, 31);
} }
await locationTracking.saveSettings(); await locationTracking.saveSettings();
await appProvider.reloadProfileScopedSettings(); await appProvider.reloadProfileScopedSettings();

View File

@@ -20,6 +20,14 @@ import 'profiles_feature_service.dart';
/// - MeshCore mesh network integration /// - MeshCore mesh network integration
/// - Real-time position updates via callbacks /// - Real-time position updates via callbacks
class LocationTrackingService { class LocationTrackingService {
static const double _defaultFastLocationMovementThresholdMeters = 10.0;
static const double _minFastLocationMovementThresholdMeters = 10.0;
static const int _defaultFastLocationActiveCadenceSeconds = 10;
static const int _minFastLocationActiveCadenceSeconds = 10;
static const int _maxFastLocationActiveCadenceSeconds = 31;
static const Duration _fastLocationMinimumUpdateInterval = Duration(
seconds: 31,
);
// ============================================================================ // ============================================================================
// Singleton Pattern // Singleton Pattern
// ============================================================================ // ============================================================================
@@ -75,10 +83,12 @@ class LocationTrackingService {
bool fastLocationUpdatesEnabled = false; bool fastLocationUpdatesEnabled = false;
/// Distance threshold for fast GPS updates /// Distance threshold for fast GPS updates
double fastLocationMovementThresholdMeters = 10.0; double fastLocationMovementThresholdMeters =
_defaultFastLocationMovementThresholdMeters;
/// Cadence for active-use fast GPS updates /// Cadence for active-use fast GPS updates
int fastLocationActiveCadenceSeconds = 10; int fastLocationActiveCadenceSeconds =
_defaultFastLocationActiveCadenceSeconds;
/// Target channel index for fast GPS updates; null means disabled/unset. /// Target channel index for fast GPS updates; null means disabled/unset.
int? fastLocationChannelIdx; int? fastLocationChannelIdx;
@@ -522,12 +532,18 @@ class LocationTrackingService {
} }
Future<void> updateFastLocationMovementThreshold(double meters) async { Future<void> updateFastLocationMovementThreshold(double meters) async {
fastLocationMovementThresholdMeters = meters.clamp(1.0, 1000.0); fastLocationMovementThresholdMeters = meters.clamp(
_minFastLocationMovementThresholdMeters,
1000.0,
);
await saveSettings(); await saveSettings();
} }
Future<void> updateFastLocationActiveCadenceSeconds(int seconds) async { Future<void> updateFastLocationActiveCadenceSeconds(int seconds) async {
fastLocationActiveCadenceSeconds = seconds.clamp(5, 60); fastLocationActiveCadenceSeconds = seconds.clamp(
_minFastLocationActiveCadenceSeconds,
_maxFastLocationActiveCadenceSeconds,
);
await saveSettings(); await saveSettings();
_refreshFastLocationTimer(); _refreshFastLocationTimer();
} }
@@ -583,6 +599,11 @@ class LocationTrackingService {
final now = DateTime.now(); final now = DateTime.now();
final previous = _lastFastLocationSentPosition; final previous = _lastFastLocationSentPosition;
final previousTime = _lastFastLocationSentAt; final previousTime = _lastFastLocationSentAt;
if (previousTime != null &&
now.difference(previousTime) < _fastLocationMinimumUpdateInterval) {
return;
}
if (previous != null && previousTime != null) { if (previous != null && previousTime != null) {
final distance = Geolocator.distanceBetween( final distance = Geolocator.distanceBetween(
previous.latitude, previous.latitude,
@@ -673,12 +694,14 @@ class LocationTrackingService {
prefs.getBool(_scopedKey(_prefKeyFastLocationEnabled)) ?? false; prefs.getBool(_scopedKey(_prefKeyFastLocationEnabled)) ?? false;
fastLocationMovementThresholdMeters = fastLocationMovementThresholdMeters =
(prefs.getDouble(_scopedKey(_prefKeyFastMovementThreshold)) ?? (prefs.getDouble(_scopedKey(_prefKeyFastMovementThreshold)) ??
gpsUpdateDistance) _defaultFastLocationMovementThresholdMeters)
.clamp(1.0, 1000.0); .clamp(_minFastLocationMovementThresholdMeters, 1000.0);
fastLocationActiveCadenceSeconds = fastLocationActiveCadenceSeconds =
(prefs.getInt(_scopedKey(_prefKeyFastActiveCadence)) ?? 10).clamp( (prefs.getInt(_scopedKey(_prefKeyFastActiveCadence)) ??
5, _defaultFastLocationActiveCadenceSeconds)
60, .clamp(
_minFastLocationActiveCadenceSeconds,
_maxFastLocationActiveCadenceSeconds,
); );
fastLocationChannelIdx = prefs.getInt(_scopedKey(_prefKeyFastChannelIdx)); fastLocationChannelIdx = prefs.getInt(_scopedKey(_prefKeyFastChannelIdx));

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2026.0322.2+39 version: 2026.0323.3+42
environment: environment:
sdk: ^3.9.2 sdk: ^3.9.2

View File

@@ -781,6 +781,19 @@ void main() {
expect(updated.advLat, equals((44.123456 * 1e6).round())); expect(updated.advLat, equals((44.123456 * 1e6).round()));
expect(updated.advLon, equals((13.654321 * 1e6).round())); expect(updated.advLon, equals((13.654321 * 1e6).round()));
expect(updated.lastAdvert, equals(1700001234)); expect(updated.lastAdvert, equals(1700001234));
expect(updated.lastMod, equals(1700001234));
expect(
provider.estimatedLocationFor(updated.publicKeyHex),
isNotNull,
);
expect(
provider.estimatedLocationFor(updated.publicKeyHex)!.latitude,
closeTo(44.123456, 0.000001),
);
expect(
provider.estimatedLocationFor(updated.publicKeyHex)!.longitude,
closeTo(13.654321, 0.000001),
);
}); });
test('ignores unknown sender prefix safely', () { test('ignores unknown sender prefix safely', () {

View File

@@ -214,7 +214,7 @@ void main() {
}); });
}); });
test('channel warning clears when replay is deduped into sent bubble', () { test('channel warning clears when replay arrives after send', () {
fakeAsync((async) { fakeAsync((async) {
final provider = MessagesProvider(); final provider = MessagesProvider();
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)'; provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
@@ -239,11 +239,11 @@ void main() {
); );
expect(provider.hasChannelSendWarning('c-warn-replay'), isFalse); expect(provider.hasChannelSendWarning('c-warn-replay'), isFalse);
expect(provider.messages, hasLength(1)); expect(provider.messages, hasLength(2));
}); });
}); });
test('channel replay is deduped for self sender within repeat window', () { test('channel replay is kept separate for self sender within repeat window', () {
final provider = MessagesProvider(); final provider = MessagesProvider();
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)'; provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
provider.addSentMessage( provider.addSentMessage(
@@ -259,9 +259,10 @@ void main() {
), ),
); );
expect(provider.messages, hasLength(1)); expect(provider.messages, hasLength(2));
expect(provider.messages.single.id, equals('c-echo')); expect(provider.messages.first.id, equals('c-echo'));
expect(provider.messages.single.senderName, equals('dz0ny (SI)')); expect(provider.messages.last.id, equals('c-echo-incoming'));
expect(provider.messages.last.senderName, equals('dz0ny (SI)'));
}); });
test( test(
@@ -308,7 +309,7 @@ void main() {
expect(provider.messages, hasLength(2)); expect(provider.messages, hasLength(2));
}); });
test('channel replay can dedupe using lazily resolved self name', () { test('channel replay stays separate using lazily resolved self name', () {
final provider = MessagesProvider(); final provider = MessagesProvider();
provider.addSentMessage( provider.addSentMessage(
_buildSentChannelMessage(id: 'c-lazy', senderTimestamp: 1700000400), _buildSentChannelMessage(id: 'c-lazy', senderTimestamp: 1700000400),
@@ -324,11 +325,12 @@ void main() {
), ),
); );
expect(provider.messages, hasLength(1)); expect(provider.messages, hasLength(2));
expect(provider.messages.single.id, equals('c-lazy')); expect(provider.messages.first.id, equals('c-lazy'));
expect(provider.messages.last.id, equals('c-lazy-incoming'));
}); });
test('channel replay dedupes meshcore-prefixed self sender name', () { test('channel replay stays separate for meshcore-prefixed self sender name', () {
final provider = MessagesProvider(); final provider = MessagesProvider();
provider.resolveContactNameCallback = (_) => 'MeshCore-dz0ny (SI)'; provider.resolveContactNameCallback = (_) => 'MeshCore-dz0ny (SI)';
provider.addSentMessage( provider.addSentMessage(
@@ -344,8 +346,9 @@ void main() {
), ),
); );
expect(provider.messages, hasLength(1)); expect(provider.messages, hasLength(2));
expect(provider.messages.single.id, equals('c-prefix')); expect(provider.messages.first.id, equals('c-prefix'));
expect(provider.messages.last.id, equals('c-prefix-incoming'));
}); });
test('duplicate incoming message increments received copy count', () { test('duplicate incoming message increments received copy count', () {

View File

@@ -19,6 +19,13 @@ void main() {
service.fastLocationChannelIdx = null; service.fastLocationChannelIdx = null;
}); });
test('loads conservative fast location defaults', () async {
await service.loadSettings();
expect(service.fastLocationMovementThresholdMeters, 10.0);
expect(service.fastLocationActiveCadenceSeconds, 10);
});
test('persists and restores fast location channel idx', () async { test('persists and restores fast location channel idx', () async {
await service.updateFastLocationChannelIdx(3); await service.updateFastLocationChannelIdx(3);
@@ -37,4 +44,12 @@ void main() {
expect(service.fastLocationChannelIdx, isNull); expect(service.fastLocationChannelIdx, isNull);
}); });
test('clamps fast location settings to conservative limits', () async {
await service.updateFastLocationMovementThreshold(3);
await service.updateFastLocationActiveCadenceSeconds(45);
expect(service.fastLocationMovementThresholdMeters, 10.0);
expect(service.fastLocationActiveCadenceSeconds, 31);
});
} }