mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-12 00:40:28 +00:00
Compare commits
2 Commits
v2026.0323
...
v2026.0324
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6978f24440 | ||
|
|
ba0f1fc141 |
@@ -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 = 126;
|
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 = 126;
|
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 = 126;
|
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 = 126;
|
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 = 126;
|
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 = 126;
|
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;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>126</string>
|
<string>128</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -5,22 +5,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000231">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000616">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.728696">
|
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.370543">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: build_app" time="101.589371">
|
<testcase classname="fastlane.lanes" name="2: build_app" time="105.689527">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="690.798016">
|
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="237.850068">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -666,7 +666,23 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
|
|
||||||
final matchingSentReplayIndex = _findMatchingSentReplayIndex(finalMessage);
|
final matchingSentReplayIndex = _findMatchingSentReplayIndex(finalMessage);
|
||||||
if (matchingSentReplayIndex != -1) {
|
if (matchingSentReplayIndex != -1) {
|
||||||
_clearChannelSendWarning(_messages[matchingSentReplayIndex].id);
|
final existingId = _messages[matchingSentReplayIndex].id;
|
||||||
|
_clearChannelSendWarning(existingId);
|
||||||
|
if (contactLocationSnapshot != null) {
|
||||||
|
_messageContactLocations[existingId] = contactLocationSnapshot;
|
||||||
|
}
|
||||||
|
_messageReceptionDetails[existingId] =
|
||||||
|
MessageReceptionDetails.mergeDuplicate(
|
||||||
|
existing: _messageReceptionDetails[existingId],
|
||||||
|
incoming: receptionDetailsSnapshot,
|
||||||
|
);
|
||||||
|
final existingMessage = _messages[matchingSentReplayIndex];
|
||||||
|
_messages[matchingSentReplayIndex] = existingMessage.copyWith(
|
||||||
|
pathLen: finalMessage.pathLen > 0 ? finalMessage.pathLen : existingMessage.pathLen,
|
||||||
|
pathBytes: finalMessage.pathBytes ?? existingMessage.pathBytes,
|
||||||
|
);
|
||||||
|
_persistMessages();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_messages.add(finalMessage);
|
_messages.add(finalMessage);
|
||||||
@@ -1214,9 +1230,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
for (final message in messages) {
|
for (final message in messages) {
|
||||||
final occurrenceCount = _messageOccurrenceCount(message);
|
final occurrenceCount = _messageOccurrenceCount(message);
|
||||||
final existingIndex = entries.indexWhere(
|
final existingIndex = entries.indexWhere(
|
||||||
(entry) =>
|
(entry) => _shouldCollapseDisplayMessage(entry.message, message),
|
||||||
entry.message.text == message.text &&
|
|
||||||
_matchesDuplicateScope(entry.message, message),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingIndex == -1) {
|
if (existingIndex == -1) {
|
||||||
@@ -1234,6 +1248,15 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _shouldCollapseDisplayMessage(Message existing, Message message) {
|
||||||
|
if (existing.isSentMessage || message.isSentMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing.text == message.text &&
|
||||||
|
_matchesDuplicateScope(existing, message);
|
||||||
|
}
|
||||||
|
|
||||||
int _messageOccurrenceCount(Message message) =>
|
int _messageOccurrenceCount(Message message) =>
|
||||||
_messageReceptionDetails[message.id]?.receivedCopies ?? 1;
|
_messageReceptionDetails[message.id]?.receivedCopies ?? 1;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ enum SensorRefreshState { idle, refreshing, success, timeout, unavailable }
|
|||||||
|
|
||||||
class SensorsProvider with ChangeNotifier {
|
class SensorsProvider with ChangeNotifier {
|
||||||
static const Duration _successStateRetention = Duration(minutes: 1);
|
static const Duration _successStateRetention = Duration(minutes: 1);
|
||||||
static const int selfAutoRefreshMinutes = 1;
|
static const Duration selfAutoRefreshInterval = Duration(seconds: 30);
|
||||||
static const String _watchedSensorsKey = 'watched_sensor_keys';
|
static const String _watchedSensorsKey = 'watched_sensor_keys';
|
||||||
static const String _visibleSensorMetricsKey = 'visible_sensor_metrics';
|
static const String _visibleSensorMetricsKey = 'visible_sensor_metrics';
|
||||||
static const String _fieldSpanKey = 'sensor_field_spans';
|
static const String _fieldSpanKey = 'sensor_field_spans';
|
||||||
@@ -374,18 +374,18 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
return List<String>.unmodifiable(dueKeys);
|
return List<String>.unmodifiable(dueKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isRefreshDue(
|
bool _isRefreshDueForInterval(
|
||||||
String publicKeyHex, {
|
String publicKeyHex, {
|
||||||
required int minutes,
|
required Duration interval,
|
||||||
required DateTime refreshTime,
|
required DateTime refreshTime,
|
||||||
}) {
|
}) {
|
||||||
if (minutes <= 0) {
|
if (interval <= Duration.zero) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final lastRefreshAt = _lastRefreshAttemptAt[publicKeyHex];
|
final lastRefreshAt = _lastRefreshAttemptAt[publicKeyHex];
|
||||||
return lastRefreshAt == null ||
|
return lastRefreshAt == null ||
|
||||||
refreshTime.difference(lastRefreshAt) >= Duration(minutes: minutes);
|
refreshTime.difference(lastRefreshAt) >= interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleMetric(
|
Future<void> toggleMetric(
|
||||||
@@ -769,9 +769,9 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
final self = selfContact(contactsProvider, connectionProvider);
|
final self = selfContact(contactsProvider, connectionProvider);
|
||||||
if (self != null &&
|
if (self != null &&
|
||||||
!dueKeys.contains(self.publicKeyHex) &&
|
!dueKeys.contains(self.publicKeyHex) &&
|
||||||
_isRefreshDue(
|
_isRefreshDueForInterval(
|
||||||
self.publicKeyHex,
|
self.publicKeyHex,
|
||||||
minutes: selfAutoRefreshMinutes,
|
interval: selfAutoRefreshInterval,
|
||||||
refreshTime: refreshTime,
|
refreshTime: refreshTime,
|
||||||
)) {
|
)) {
|
||||||
dueKeys.insert(0, self.publicKeyHex);
|
dueKeys.insert(0, self.publicKeyHex);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
|||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
import '../providers/connection_provider.dart';
|
import '../providers/connection_provider.dart';
|
||||||
import '../providers/contacts_provider.dart';
|
import '../providers/contacts_provider.dart';
|
||||||
|
import '../providers/map_provider.dart';
|
||||||
import '../providers/sensors_provider.dart';
|
import '../providers/sensors_provider.dart';
|
||||||
import '../widgets/sensors/bthome_met_history_sheet.dart';
|
import '../widgets/sensors/bthome_met_history_sheet.dart';
|
||||||
import '../widgets/sensors/sensor_telemetry_card.dart';
|
import '../widgets/sensors/sensor_telemetry_card.dart';
|
||||||
@@ -21,7 +22,10 @@ class SensorsTab extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SensorsTabState extends State<SensorsTab> {
|
class _SensorsTabState extends State<SensorsTab> {
|
||||||
|
static const Duration _autoRefreshTickInterval = Duration(seconds: 30);
|
||||||
Timer? _minuteTicker;
|
Timer? _minuteTicker;
|
||||||
|
final Map<String, DateTime> _lastCenteredTelemetryAtBySensor =
|
||||||
|
<String, DateTime>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -61,22 +65,8 @@ class _SensorsTabState extends State<SensorsTab> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final now = DateTime.now();
|
_minuteTicker = Timer.periodic(_autoRefreshTickInterval, (_) {
|
||||||
final nextMinute = DateTime(
|
|
||||||
now.year,
|
|
||||||
now.month,
|
|
||||||
now.day,
|
|
||||||
now.hour,
|
|
||||||
now.minute + 1,
|
|
||||||
);
|
|
||||||
final delay = nextMinute.difference(now);
|
|
||||||
|
|
||||||
_minuteTicker = Timer(delay, () {
|
|
||||||
if (!mounted) return;
|
|
||||||
unawaited(_handleMinuteTick());
|
unawaited(_handleMinuteTick());
|
||||||
_minuteTicker = Timer.periodic(const Duration(minutes: 1), (_) {
|
|
||||||
unawaited(_handleMinuteTick());
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +215,65 @@ class _SensorsTabState extends State<SensorsTab> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _maybeCenterMapOnTelemetryUpdate(
|
||||||
|
Iterable<String> sensorKeys, {
|
||||||
|
required SensorsProvider sensorsProvider,
|
||||||
|
required ContactsProvider contactsProvider,
|
||||||
|
required ConnectionProvider connectionProvider,
|
||||||
|
}) {
|
||||||
|
if (!widget.isActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Contact? latestContact;
|
||||||
|
DateTime? latestTimestamp;
|
||||||
|
|
||||||
|
for (final key in sensorKeys) {
|
||||||
|
final contact = sensorsProvider.contactForDisplay(
|
||||||
|
key,
|
||||||
|
contactsProvider: contactsProvider,
|
||||||
|
connectionProvider: connectionProvider,
|
||||||
|
);
|
||||||
|
final timestamp = contact?.telemetry?.timestamp;
|
||||||
|
final location = contact?.displayLocation;
|
||||||
|
if (contact == null || timestamp == null || location == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final previousTimestamp = _lastCenteredTelemetryAtBySensor[key];
|
||||||
|
if (previousTimestamp != null && !timestamp.isAfter(previousTimestamp)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestTimestamp == null || timestamp.isAfter(latestTimestamp)) {
|
||||||
|
latestContact = contact;
|
||||||
|
latestTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestContact == null || latestTimestamp == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastCenteredTelemetryAtBySensor[latestContact.publicKeyHex] =
|
||||||
|
latestTimestamp;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted || !widget.isActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final location = latestContact!.displayLocation;
|
||||||
|
if (location == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.read<MapProvider>().navigateToLocation(
|
||||||
|
location: location,
|
||||||
|
zoom: 15.0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -247,6 +296,16 @@ class _SensorsTabState extends State<SensorsTab> {
|
|||||||
contactsProvider: contactsProvider,
|
contactsProvider: contactsProvider,
|
||||||
connectionProvider: connectionProvider,
|
connectionProvider: connectionProvider,
|
||||||
);
|
);
|
||||||
|
final displayKeys = <String>[
|
||||||
|
...?selfDisplayKey == null ? null : <String>[selfDisplayKey],
|
||||||
|
...watchedKeys,
|
||||||
|
];
|
||||||
|
_maybeCenterMapOnTelemetryUpdate(
|
||||||
|
displayKeys,
|
||||||
|
sensorsProvider: sensorsProvider,
|
||||||
|
contactsProvider: contactsProvider,
|
||||||
|
connectionProvider: connectionProvider,
|
||||||
|
);
|
||||||
final hasAnyCards =
|
final hasAnyCards =
|
||||||
selfDisplayKey != null || watchedKeys.isNotEmpty;
|
selfDisplayKey != null || watchedKeys.isNotEmpty;
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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.0323.2+41
|
version: 2026.0324.1+43
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
|
|||||||
@@ -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', () {
|
||||||
|
|||||||
@@ -239,11 +239,12 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(provider.hasChannelSendWarning('c-warn-replay'), isFalse);
|
expect(provider.hasChannelSendWarning('c-warn-replay'), isFalse);
|
||||||
expect(provider.messages, hasLength(2));
|
expect(provider.messages, hasLength(1));
|
||||||
|
expect(provider.messages.single.id, equals('c-warn-replay'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('channel replay is kept separate for self sender within repeat window', () {
|
test('channel replay merges into sent message 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,10 +260,9 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(provider.messages, hasLength(2));
|
expect(provider.messages, hasLength(1));
|
||||||
expect(provider.messages.first.id, equals('c-echo'));
|
expect(provider.messages.single.id, equals('c-echo'));
|
||||||
expect(provider.messages.last.id, equals('c-echo-incoming'));
|
expect(provider.messages.single.pathLen, equals(1));
|
||||||
expect(provider.messages.last.senderName, equals('dz0ny (SI)'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
@@ -309,7 +309,7 @@ void main() {
|
|||||||
expect(provider.messages, hasLength(2));
|
expect(provider.messages, hasLength(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('channel replay stays separate using lazily resolved self name', () {
|
test('channel replay merges 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),
|
||||||
@@ -325,12 +325,11 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(provider.messages, hasLength(2));
|
expect(provider.messages, hasLength(1));
|
||||||
expect(provider.messages.first.id, equals('c-lazy'));
|
expect(provider.messages.single.id, equals('c-lazy'));
|
||||||
expect(provider.messages.last.id, equals('c-lazy-incoming'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('channel replay stays separate for meshcore-prefixed self sender name', () {
|
test('channel replay merges 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(
|
||||||
@@ -346,9 +345,8 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(provider.messages, hasLength(2));
|
expect(provider.messages, hasLength(1));
|
||||||
expect(provider.messages.first.id, equals('c-prefix'));
|
expect(provider.messages.single.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', () {
|
||||||
@@ -465,6 +463,30 @@ void main() {
|
|||||||
expect(display.single.occurrenceCount, equals(2));
|
expect(display.single.occurrenceCount, equals(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('display list keeps a single sent message for self replay', () {
|
||||||
|
final provider = MessagesProvider();
|
||||||
|
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
|
||||||
|
provider.addSentMessage(
|
||||||
|
_buildSentChannelMessage(id: 'display-sent', senderTimestamp: 1700000900),
|
||||||
|
);
|
||||||
|
provider.markMessageSent('display-sent', 0, 0);
|
||||||
|
|
||||||
|
provider.addMessage(
|
||||||
|
_buildReceivedChannelReplay(
|
||||||
|
id: 'display-received',
|
||||||
|
senderTimestamp: 1700000901,
|
||||||
|
senderName: 'dz0ny (SI)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final display = provider.buildDisplayMessages(
|
||||||
|
provider.getRecentMessages(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(display, hasLength(1));
|
||||||
|
expect(display.single.message.id, equals('display-sent'));
|
||||||
|
});
|
||||||
|
|
||||||
test('missing ACK schedules a delayed retransmission', () {
|
test('missing ACK schedules a delayed retransmission', () {
|
||||||
fakeAsync((async) {
|
fakeAsync((async) {
|
||||||
final provider = MessagesProvider();
|
final provider = MessagesProvider();
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ void main() {
|
|||||||
expect(connectionProvider.pingCalls, 2);
|
expect(connectionProvider.pingCalls, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('refreshDueSensors refreshes self every minute', () async {
|
test('refreshDueSensors refreshes self every 30 seconds', () async {
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
final selfKey = Uint8List(32)..[0] = 0x66;
|
final selfKey = Uint8List(32)..[0] = 0x66;
|
||||||
final contactsProvider = ContactsProvider();
|
final contactsProvider = ContactsProvider();
|
||||||
@@ -383,14 +383,21 @@ void main() {
|
|||||||
contactsProvider: contactsProvider,
|
contactsProvider: contactsProvider,
|
||||||
connectionProvider: connectionProvider,
|
connectionProvider: connectionProvider,
|
||||||
);
|
);
|
||||||
expect(connectionProvider.pingCalls, 1);
|
expect(connectionProvider.pingCalls, 2);
|
||||||
|
|
||||||
|
await provider.refreshDueSensors(
|
||||||
|
now: start.add(const Duration(seconds: 59)),
|
||||||
|
contactsProvider: contactsProvider,
|
||||||
|
connectionProvider: connectionProvider,
|
||||||
|
);
|
||||||
|
expect(connectionProvider.pingCalls, 2);
|
||||||
|
|
||||||
await provider.refreshDueSensors(
|
await provider.refreshDueSensors(
|
||||||
now: start.add(const Duration(minutes: 1)),
|
now: start.add(const Duration(minutes: 1)),
|
||||||
contactsProvider: contactsProvider,
|
contactsProvider: contactsProvider,
|
||||||
connectionProvider: connectionProvider,
|
connectionProvider: connectionProvider,
|
||||||
);
|
);
|
||||||
expect(connectionProvider.pingCalls, 2);
|
expect(connectionProvider.pingCalls, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|||||||
Reference in New Issue
Block a user