mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-12 08:50:30 +00:00
Compare commits
1 Commits
v2026.0323
...
v2026.0324
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6978f24440 |
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: build_app" time="105.547745">
|
<testcase classname="fastlane.lanes" name="2: build_app" time="105.689527">
|
||||||
|
|
||||||
</testcase>
|
</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>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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.3+42
|
version: 2026.0324.1+43
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
|
|||||||
@@ -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