Skip to content

Commit dd61de8

Browse files
committed
Multi-hop CARpeater packets now strip the CARpeater hop and report coverage from the underlying repeater with null SNR/RSSI, instead of being dropped entirely. Single-hop packets and discovery responses are still dropped. The -30 dBm failsafe is bypassed for packets matching your CARpeater prefix but stays active for everything else.
1 parent 65e81d3 commit dd61de8

File tree

11 files changed

+229
-141
lines changed

11 files changed

+229
-141
lines changed

.build_version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.1
1+
1.1.0

lib/models/log_entry.dart

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,31 @@ class TxLogEntry {
3131
String toCsv() {
3232
final eventsStr = events.isEmpty
3333
? 'None'
34-
: events.map((e) => '${e.repeaterId}(${e.snr.toStringAsFixed(2)})').join(',');
34+
: events.map((e) => e.snr != null ? '${e.repeaterId}(${e.snr!.toStringAsFixed(2)})' : '${e.repeaterId}(null)').join(',');
3535
return '${timestamp.toIso8601String()},$latitude,$longitude,$power,$eventsStr';
3636
}
3737
}
3838

3939
/// RX Event (repeater that heard a TX ping)
4040
class RxEvent {
4141
final String repeaterId; // Hex ID (e.g., "4e", "b7")
42-
final double snr; // Signal-to-noise ratio in dB
43-
final int rssi; // RSSI in dBm
42+
final double? snr; // Signal-to-noise ratio in dB (null for CARpeater pass-through)
43+
final int? rssi; // RSSI in dBm (null for CARpeater pass-through)
4444

4545
RxEvent({
4646
required this.repeaterId,
47-
required this.snr,
48-
this.rssi = 0,
47+
this.snr,
48+
this.rssi,
4949
});
5050

5151
/// Get SNR color severity (red, orange, green)
52+
/// Returns null for CARpeater pass-through (no signal data)
5253
/// Reference: getSnrSeverityClass() in wardrive.js
53-
SnrSeverity get severity {
54-
if (snr <= -1) {
54+
SnrSeverity? get severity {
55+
if (snr == null) return null;
56+
if (snr! <= -1) {
5557
return SnrSeverity.poor; // Red: -12 to -1 dB
56-
} else if (snr <= 5) {
58+
} else if (snr! <= 5) {
5759
return SnrSeverity.fair; // Orange: 0 to 5 dB
5860
} else {
5961
return SnrSeverity.good; // Green: 6 to 13+ dB
@@ -66,8 +68,8 @@ class RxEvent {
6668
class RxLogEntry {
6769
final DateTime timestamp;
6870
final String repeaterId; // Hex ID (e.g., "4e", "b7")
69-
final double snr; // Signal-to-noise ratio in dB
70-
final int rssi; // Received signal strength indicator in dBm
71+
final double? snr; // Signal-to-noise ratio in dB (null for CARpeater pass-through)
72+
final int? rssi; // Received signal strength indicator in dBm (null for CARpeater pass-through)
7173
final int pathLength; // Number of hops
7274
final int header; // Packet header byte
7375
final double latitude;
@@ -76,8 +78,8 @@ class RxLogEntry {
7678
RxLogEntry({
7779
required this.timestamp,
7880
required this.repeaterId,
79-
required this.snr,
80-
required this.rssi,
81+
this.snr,
82+
this.rssi,
8183
required this.pathLength,
8284
required this.header,
8385
required this.latitude,
@@ -97,10 +99,12 @@ class RxLogEntry {
9799
}
98100

99101
/// Get SNR color severity
100-
SnrSeverity get severity {
101-
if (snr <= -1) {
102+
/// Returns null for CARpeater pass-through (no signal data)
103+
SnrSeverity? get severity {
104+
if (snr == null) return null;
105+
if (snr! <= -1) {
102106
return SnrSeverity.poor;
103-
} else if (snr <= 5) {
107+
} else if (snr! <= 5) {
104108
return SnrSeverity.fair;
105109
} else {
106110
return SnrSeverity.good;
@@ -109,7 +113,7 @@ class RxLogEntry {
109113

110114
/// Get CSV row
111115
String toCsv() {
112-
return '${timestamp.toIso8601String()},$repeaterId,$snr,$rssi,'
116+
return '${timestamp.toIso8601String()},$repeaterId,${snr ?? 'null'},${rssi ?? 'null'},'
113117
'$pathLength,0x${header.toRadixString(16).padLeft(2, '0')},'
114118
'$latitude,$longitude';
115119
}

lib/models/ping_data.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,14 @@ class RxPing {
111111
/// Repeater that heard a TX ping (from echo tracking)
112112
class HeardRepeater {
113113
final String repeaterId; // Hex ID (e.g., "4e", "77")
114-
final double snr; // Best SNR observed
115-
final int rssi; // RSSI in dBm
114+
final double? snr; // Best SNR observed (null for CARpeater pass-through)
115+
final int? rssi; // RSSI in dBm (null for CARpeater pass-through)
116116
final int seenCount; // How many times this repeater was heard
117117

118118
const HeardRepeater({
119119
required this.repeaterId,
120-
required this.snr,
121-
this.rssi = 0,
120+
this.snr,
121+
this.rssi,
122122
this.seenCount = 1,
123123
});
124124
}

lib/providers/app_state_provider.dart

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,7 +1129,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
11291129
// Handle real-time echo updates - update TxLogEntry as echoes are received
11301130
_pingService!.onEchoReceived = (txPing, repeater, isNew) {
11311131
debugLog('[APP] ========== ECHO CALLBACK RECEIVED ==========');
1132-
debugLog('[APP] Real-time echo: ${repeater.repeaterId} (SNR: ${repeater.snr}, isNew: $isNew)');
1132+
debugLog('[APP] Real-time echo: ${repeater.repeaterId} (SNR: ${repeater.snr ?? 'null'}, isNew: $isNew)');
11331133
debugLog('[APP] TxLogEntries count: ${_txLogEntries.length}');
11341134

11351135
// Find the matching TxLogEntry and update its events
@@ -1216,8 +1216,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
12161216
if (lastTx.events.isNotEmpty) {
12171217
repeaters = lastTx.events.map((e) => MarkerRepeaterInfo(
12181218
repeaterId: e.repeaterId,
1219-
snr: e.snr,
1220-
rssi: e.rssi,
1219+
snr: e.snr ?? 0.0,
1220+
rssi: e.rssi ?? 0,
12211221
)).toList();
12221222
}
12231223
}
@@ -1404,6 +1404,10 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
14041404
_txTracker = TxTracker();
14051405
_txTracker!.disableRssiFilter = _preferences.disableRssiFilter;
14061406

1407+
// Set CARpeater prefix for pass-through (replaces shouldIgnoreRepeater)
1408+
_txTracker!.carpeaterPrefix = _preferences.ignoreCarpeater ? _preferences.ignoreRepeaterId : null;
1409+
debugLog('[APP] TxTracker.carpeaterPrefix set to ${_txTracker!.carpeaterPrefix ?? 'null'}');
1410+
14071411
// Log TX carpeater drops to error log (without navigating to error tab)
14081412
_txTracker!.onCarpeaterDrop = (String repeaterId, String reason) {
14091413
debugLog('[APP] TX carpeater drop: repeater=$repeaterId, reason=$reason');
@@ -1412,37 +1416,16 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
14121416
};
14131417
debugLog('[APP] TxTracker.onCarpeaterDrop callback SET');
14141418

1415-
// Function to check if repeater should be ignored (carpeater filter)
1416-
_txTracker!.shouldIgnoreRepeater = (String repeaterId) {
1417-
final prefs = _preferences;
1418-
if (prefs.ignoreCarpeater && prefs.ignoreRepeaterId != null) {
1419-
final ignored = prefs.ignoreRepeaterId!.toUpperCase();
1420-
return repeaterId.toUpperCase() == ignored;
1421-
}
1422-
return false;
1423-
};
1424-
debugLog('[APP] TxTracker.shouldIgnoreRepeater callback SET');
1425-
14261419
// Create RX logger (stored for use when enabling Passive Mode)
14271420
_rxLogger = RxLogger(
1428-
// Function to check if repeater should be ignored (carpeater filter)
1429-
shouldIgnoreRepeater: (String repeaterId) {
1430-
// Check user preferences for ignored repeater ID
1431-
final prefs = _preferences;
1432-
if (prefs.ignoreCarpeater && prefs.ignoreRepeaterId != null) {
1433-
// Case-insensitive comparison (both uppercase)
1434-
final ignored = prefs.ignoreRepeaterId!.toUpperCase();
1435-
final current = repeaterId.toUpperCase();
1436-
return current == ignored;
1437-
}
1438-
return false;
1439-
},
1421+
// CARpeater prefix for pass-through (replaces shouldIgnoreRepeater)
1422+
carpeaterPrefix: _preferences.ignoreCarpeater ? _preferences.ignoreRepeaterId : null,
14401423
// Immediate observation callback - fires when packet is first validated
14411424
// Creates pin IMMEDIATELY for NEW repeaters (first time in current batch)
14421425
onObservation: (observation) {
14431426
try {
14441427
debugLog('[APP] Immediate RX observation: repeater=${observation.repeaterId}, '
1445-
'snr=${observation.snr}, location=${observation.lat.toStringAsFixed(5)},${observation.lon.toStringAsFixed(5)}');
1428+
'snr=${observation.snr ?? 'null'}, location=${observation.lat.toStringAsFixed(5)},${observation.lon.toStringAsFixed(5)}');
14461429

14471430
// Log current batch tracking state for debugging
14481431
debugLog('[APP] Current batch tracking: ${_currentBatchRepeaters.length} repeaters: $_currentBatchRepeaters');
@@ -1457,8 +1440,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
14571440
longitude: observation.lon,
14581441
repeaterId: observation.repeaterId,
14591442
timestamp: DateTime.now(),
1460-
snr: observation.snr,
1461-
rssi: observation.rssi,
1443+
snr: observation.snr ?? 0.0,
1444+
rssi: observation.rssi ?? 0,
14621445
);
14631446
_rxPings.add(rxPing);
14641447
if (_rxPings.length > _maxMapPins) _rxPings.removeAt(0);
@@ -1480,8 +1463,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
14801463
repeaters: [
14811464
MarkerRepeaterInfo(
14821465
repeaterId: observation.repeaterId,
1483-
snr: observation.snr,
1484-
rssi: observation.rssi,
1466+
snr: observation.snr ?? 0.0,
1467+
rssi: observation.rssi ?? 0,
14851468
),
14861469
],
14871470
);
@@ -1501,7 +1484,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
15011484
try {
15021485
debugLog('[APP] ========== BATCH FLUSH CALLBACK ==========');
15031486
debugLog('[APP] Finalized RX entry (best SNR): repeater=${entry.repeaterId}, '
1504-
'snr=${entry.snr}, location=${entry.lat.toStringAsFixed(5)},${entry.lon.toStringAsFixed(5)}');
1487+
'snr=${entry.snr ?? 'null'}, location=${entry.lat.toStringAsFixed(5)},${entry.lon.toStringAsFixed(5)}');
15051488

15061489
final repeaterKey = entry.repeaterId.toUpperCase();
15071490

@@ -1518,20 +1501,22 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
15181501
if (lastPinIndex != -1) {
15191502
// Update the pin's SNR to the best from this batch
15201503
final existingPin = _rxPings[lastPinIndex];
1521-
if (entry.snr > existingPin.snr) {
1504+
// Only update if new SNR is non-null and better (null never replaces non-null)
1505+
final shouldUpdateSnr = entry.snr != null && entry.snr! > existingPin.snr;
1506+
if (shouldUpdateSnr) {
15221507
_rxPings[lastPinIndex] = RxPing(
15231508
latitude: existingPin.latitude, // KEEP batch start location
15241509
longitude: existingPin.longitude, // KEEP batch start location
15251510
repeaterId: entry.repeaterId,
15261511
timestamp: entry.timestamp,
1527-
snr: entry.snr, // UPDATE to best SNR from batch
1528-
rssi: entry.rssi,
1512+
snr: entry.snr ?? existingPin.snr, // UPDATE to best SNR from batch
1513+
rssi: entry.rssi ?? existingPin.rssi,
15291514
);
15301515
debugLog('[APP] Updated RX pin SNR for repeater=${entry.repeaterId}: '
1531-
'${existingPin.snr.toStringAsFixed(2)} -> ${entry.snr.toStringAsFixed(2)}');
1516+
'${existingPin.snr.toStringAsFixed(2)} -> ${entry.snr?.toStringAsFixed(2) ?? 'null'}');
15321517
} else {
15331518
debugLog('[APP] RX pin SNR unchanged for repeater=${entry.repeaterId}: '
1534-
'batch best ${entry.snr.toStringAsFixed(2)} <= pin ${existingPin.snr.toStringAsFixed(2)}');
1519+
'batch best ${entry.snr?.toStringAsFixed(2) ?? 'null'} <= pin ${existingPin.snr.toStringAsFixed(2)}');
15351520
}
15361521
} else {
15371522
// Edge case: pin not found (should have been created in onObservation)
@@ -1540,8 +1525,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
15401525
longitude: entry.lon,
15411526
repeaterId: entry.repeaterId,
15421527
timestamp: entry.timestamp,
1543-
snr: entry.snr,
1544-
rssi: entry.rssi,
1528+
snr: entry.snr ?? 0.0,
1529+
rssi: entry.rssi ?? 0,
15451530
);
15461531
_rxPings.add(newRxPing);
15471532
if (_rxPings.length > _maxMapPins) _rxPings.removeAt(0);
@@ -1571,13 +1556,15 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
15711556
_rxLogEntries.add(rxLogEntry);
15721557
if (_rxLogEntries.length > _maxLogEntries) _rxLogEntries.removeAt(0);
15731558
debugLog('[APP] Added RX log entry: repeater=${entry.repeaterId}, '
1574-
'snr=${entry.snr}, pathLen=${entry.pathLength}');
1559+
'snr=${entry.snr ?? 'null'}, pathLen=${entry.pathLength}');
15751560

15761561
// Note: RX count is incremented in onObservation when pin is created (immediate feedback)
15771562

15781563
// Enqueue to API with formatted heard_repeats string
1579-
// Format: "repeaterId(snr)" e.g. "4e(12.25)"
1580-
final heardRepeats = '${entry.repeaterId}(${entry.snr.toStringAsFixed(2)})';
1564+
// Format: "repeaterId(snr)" e.g. "4e(12.25)" or "4e(null)" for CARpeater pass-through
1565+
final heardRepeats = entry.snr != null
1566+
? '${entry.repeaterId}(${entry.snr!.toStringAsFixed(2)})'
1567+
: '${entry.repeaterId}(null)';
15811568
await _apiQueueService.enqueueRx(
15821569
latitude: entry.lat,
15831570
longitude: entry.lon,
@@ -2729,10 +2716,26 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
27292716
// Propagate RSSI filter setting to live trackers/validators
27302717
_syncRssiFilterSetting(preferences.disableRssiFilter);
27312718

2719+
// Propagate CARpeater prefix to live trackers
2720+
_syncCarpeaterPrefix();
2721+
27322722
notifyListeners();
27332723
_savePreferences();
27342724
}
27352725

2726+
/// Propagate carpeaterPrefix to live TxTracker and RxLogger
2727+
void _syncCarpeaterPrefix() {
2728+
final prefix = _preferences.ignoreCarpeater ? _preferences.ignoreRepeaterId : null;
2729+
if (_txTracker != null) {
2730+
_txTracker!.carpeaterPrefix = prefix;
2731+
debugLog('[APP] Synced TxTracker.carpeaterPrefix = ${prefix ?? 'null'}');
2732+
}
2733+
if (_rxLogger != null) {
2734+
_rxLogger!.carpeaterPrefix = prefix;
2735+
debugLog('[APP] Synced RxLogger.carpeaterPrefix = ${prefix ?? 'null'}');
2736+
}
2737+
}
2738+
27362739
/// Propagate disableRssiFilter to all active trackers and validators
27372740
void _syncRssiFilterSetting(bool disableRssiFilter) {
27382741
if (_txTracker != null) {

lib/screens/log_screen.dart

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -488,13 +488,17 @@ class _TxLogTab extends StatelessWidget {
488488
snrColor = Colors.orange;
489489
case SnrSeverity.good:
490490
snrColor = Colors.green;
491+
case null:
492+
snrColor = Colors.grey;
491493
}
492494

493495
// RSSI color based on signal strength
494496
Color rssiColor;
495-
if (event.rssi >= -70) {
497+
if (event.rssi == null) {
498+
rssiColor = Colors.grey;
499+
} else if (event.rssi! >= -70) {
496500
rssiColor = Colors.green;
497-
} else if (event.rssi >= -100) {
501+
} else if (event.rssi! >= -100) {
498502
rssiColor = Colors.orange;
499503
} else {
500504
rssiColor = Colors.red;
@@ -511,13 +515,13 @@ class _TxLogTab extends StatelessWidget {
511515
// SNR
512516
Expanded(
513517
child: Center(
514-
child: _buildTxChip(event.snr.toStringAsFixed(1), snrColor),
518+
child: _buildTxChip(event.snr?.toStringAsFixed(1) ?? '-', snrColor),
515519
),
516520
),
517521
// RSSI
518522
Expanded(
519523
child: Center(
520-
child: _buildTxChip('${event.rssi}', rssiColor),
524+
child: _buildTxChip(event.rssi != null ? '${event.rssi}' : '-', rssiColor),
521525
),
522526
),
523527
],
@@ -591,13 +595,17 @@ class _RxLogTab extends StatelessWidget {
591595
snrColor = Colors.orange;
592596
case SnrSeverity.good:
593597
snrColor = Colors.green;
598+
case null:
599+
snrColor = Colors.grey;
594600
}
595601

596602
// RSSI color based on signal strength
597603
Color rssiColor;
598-
if (entry.rssi >= -70) {
604+
if (entry.rssi == null) {
605+
rssiColor = Colors.grey;
606+
} else if (entry.rssi! >= -70) {
599607
rssiColor = Colors.green; // Strong: -30 to -70 dBm
600-
} else if (entry.rssi >= -100) {
608+
} else if (entry.rssi! >= -100) {
601609
rssiColor = Colors.orange; // Medium: -70 to -100 dBm
602610
} else {
603611
rssiColor = Colors.red; // Weak: -100 to -120 dBm
@@ -721,13 +729,13 @@ class _RxLogTab extends StatelessWidget {
721729
// SNR
722730
Expanded(
723731
child: Center(
724-
child: _buildRxChip(entry.snr.toStringAsFixed(1), snrColor),
732+
child: _buildRxChip(entry.snr?.toStringAsFixed(1) ?? '-', snrColor),
725733
),
726734
),
727735
// RSSI
728736
Expanded(
729737
child: Center(
730-
child: _buildRxChip('${entry.rssi}', rssiColor),
738+
child: _buildRxChip(entry.rssi != null ? '${entry.rssi}' : '-', rssiColor),
731739
),
732740
),
733741
],

0 commit comments

Comments
 (0)