mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
fix: Merge self replay and refresh self telemetry
This commit is contained in:
@@ -5,22 +5,22 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00073">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000616">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.659743">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.370543">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="105.547745">
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="105.689527">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="252.935458">
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="237.850068">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -666,7 +666,23 @@ class MessagesProvider with ChangeNotifier {
|
||||
|
||||
final matchingSentReplayIndex = _findMatchingSentReplayIndex(finalMessage);
|
||||
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);
|
||||
@@ -1214,9 +1230,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
for (final message in messages) {
|
||||
final occurrenceCount = _messageOccurrenceCount(message);
|
||||
final existingIndex = entries.indexWhere(
|
||||
(entry) =>
|
||||
entry.message.text == message.text &&
|
||||
_matchesDuplicateScope(entry.message, message),
|
||||
(entry) => _shouldCollapseDisplayMessage(entry.message, message),
|
||||
);
|
||||
|
||||
if (existingIndex == -1) {
|
||||
@@ -1234,6 +1248,15 @@ class MessagesProvider with ChangeNotifier {
|
||||
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) =>
|
||||
_messageReceptionDetails[message.id]?.receivedCopies ?? 1;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ enum SensorRefreshState { idle, refreshing, success, timeout, unavailable }
|
||||
|
||||
class SensorsProvider with ChangeNotifier {
|
||||
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 _visibleSensorMetricsKey = 'visible_sensor_metrics';
|
||||
static const String _fieldSpanKey = 'sensor_field_spans';
|
||||
@@ -374,18 +374,18 @@ class SensorsProvider with ChangeNotifier {
|
||||
return List<String>.unmodifiable(dueKeys);
|
||||
}
|
||||
|
||||
bool _isRefreshDue(
|
||||
bool _isRefreshDueForInterval(
|
||||
String publicKeyHex, {
|
||||
required int minutes,
|
||||
required Duration interval,
|
||||
required DateTime refreshTime,
|
||||
}) {
|
||||
if (minutes <= 0) {
|
||||
if (interval <= Duration.zero) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final lastRefreshAt = _lastRefreshAttemptAt[publicKeyHex];
|
||||
return lastRefreshAt == null ||
|
||||
refreshTime.difference(lastRefreshAt) >= Duration(minutes: minutes);
|
||||
refreshTime.difference(lastRefreshAt) >= interval;
|
||||
}
|
||||
|
||||
Future<void> toggleMetric(
|
||||
@@ -769,9 +769,9 @@ class SensorsProvider with ChangeNotifier {
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self != null &&
|
||||
!dueKeys.contains(self.publicKeyHex) &&
|
||||
_isRefreshDue(
|
||||
_isRefreshDueForInterval(
|
||||
self.publicKeyHex,
|
||||
minutes: selfAutoRefreshMinutes,
|
||||
interval: selfAutoRefreshInterval,
|
||||
refreshTime: refreshTime,
|
||||
)) {
|
||||
dueKeys.insert(0, self.publicKeyHex);
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../providers/map_provider.dart';
|
||||
import '../providers/sensors_provider.dart';
|
||||
import '../widgets/sensors/bthome_met_history_sheet.dart';
|
||||
import '../widgets/sensors/sensor_telemetry_card.dart';
|
||||
@@ -21,7 +22,10 @@ class SensorsTab extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SensorsTabState extends State<SensorsTab> {
|
||||
static const Duration _autoRefreshTickInterval = Duration(seconds: 30);
|
||||
Timer? _minuteTicker;
|
||||
final Map<String, DateTime> _lastCenteredTelemetryAtBySensor =
|
||||
<String, DateTime>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -61,22 +65,8 @@ class _SensorsTabState extends State<SensorsTab> {
|
||||
return;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
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;
|
||||
_minuteTicker = Timer.periodic(_autoRefreshTickInterval, (_) {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -247,6 +296,16 @@ class _SensorsTabState extends State<SensorsTab> {
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
final displayKeys = <String>[
|
||||
...?selfDisplayKey == null ? null : <String>[selfDisplayKey],
|
||||
...watchedKeys,
|
||||
];
|
||||
_maybeCenterMapOnTelemetryUpdate(
|
||||
displayKeys,
|
||||
sensorsProvider: sensorsProvider,
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
final hasAnyCards =
|
||||
selfDisplayKey != null || watchedKeys.isNotEmpty;
|
||||
|
||||
|
||||
@@ -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
|
||||
# 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.
|
||||
version: 2026.0323.3+42
|
||||
version: 2026.0324.1+43
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.2
|
||||
|
||||
@@ -239,11 +239,12 @@ void main() {
|
||||
);
|
||||
|
||||
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();
|
||||
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
|
||||
provider.addSentMessage(
|
||||
@@ -259,10 +260,9 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(provider.messages, hasLength(2));
|
||||
expect(provider.messages.first.id, equals('c-echo'));
|
||||
expect(provider.messages.last.id, equals('c-echo-incoming'));
|
||||
expect(provider.messages.last.senderName, equals('dz0ny (SI)'));
|
||||
expect(provider.messages, hasLength(1));
|
||||
expect(provider.messages.single.id, equals('c-echo'));
|
||||
expect(provider.messages.single.pathLen, equals(1));
|
||||
});
|
||||
|
||||
test(
|
||||
@@ -309,7 +309,7 @@ void main() {
|
||||
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();
|
||||
provider.addSentMessage(
|
||||
_buildSentChannelMessage(id: 'c-lazy', senderTimestamp: 1700000400),
|
||||
@@ -325,12 +325,11 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(provider.messages, hasLength(2));
|
||||
expect(provider.messages.first.id, equals('c-lazy'));
|
||||
expect(provider.messages.last.id, equals('c-lazy-incoming'));
|
||||
expect(provider.messages, hasLength(1));
|
||||
expect(provider.messages.single.id, equals('c-lazy'));
|
||||
});
|
||||
|
||||
test('channel replay stays separate for meshcore-prefixed self sender name', () {
|
||||
test('channel replay merges for meshcore-prefixed self sender name', () {
|
||||
final provider = MessagesProvider();
|
||||
provider.resolveContactNameCallback = (_) => 'MeshCore-dz0ny (SI)';
|
||||
provider.addSentMessage(
|
||||
@@ -346,9 +345,8 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(provider.messages, hasLength(2));
|
||||
expect(provider.messages.first.id, equals('c-prefix'));
|
||||
expect(provider.messages.last.id, equals('c-prefix-incoming'));
|
||||
expect(provider.messages, hasLength(1));
|
||||
expect(provider.messages.single.id, equals('c-prefix'));
|
||||
});
|
||||
|
||||
test('duplicate incoming message increments received copy count', () {
|
||||
@@ -465,6 +463,30 @@ void main() {
|
||||
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', () {
|
||||
fakeAsync((async) {
|
||||
final provider = MessagesProvider();
|
||||
|
||||
@@ -356,7 +356,7 @@ void main() {
|
||||
expect(connectionProvider.pingCalls, 2);
|
||||
});
|
||||
|
||||
test('refreshDueSensors refreshes self every minute', () async {
|
||||
test('refreshDueSensors refreshes self every 30 seconds', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final selfKey = Uint8List(32)..[0] = 0x66;
|
||||
final contactsProvider = ContactsProvider();
|
||||
@@ -383,14 +383,21 @@ void main() {
|
||||
contactsProvider: contactsProvider,
|
||||
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(
|
||||
now: start.add(const Duration(minutes: 1)),
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
expect(connectionProvider.pingCalls, 2);
|
||||
expect(connectionProvider.pingCalls, 3);
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
Reference in New Issue
Block a user