Skip to content

Commit ed1ec8b

Browse files
committed
Add DiscoveryWindowTimer and enhance ping control UI for Passive Mode
1 parent 6471ebf commit ed1ec8b

File tree

4 files changed

+107
-31
lines changed

4 files changed

+107
-31
lines changed

lib/providers/app_state_provider.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class AppStateProvider extends ChangeNotifier {
7171
late final CooldownTimer _cooldownTimer; // Shared cooldown for TX Ping and TX/RX Auto
7272
late final AutoPingTimer _autoPingTimer;
7373
late final RxWindowTimer _rxWindowTimer;
74+
late final DiscoveryWindowTimer _discoveryWindowTimer; // Discovery listening window (Passive Mode)
7475
MeshCoreConnection? _meshCoreConnection;
7576
PingService? _pingService;
7677
UnifiedRxHandler? _unifiedRxHandler;
@@ -186,6 +187,8 @@ class AppStateProvider extends ChangeNotifier {
186187
bool get autoPingEnabled => _autoPingEnabled;
187188
AutoMode get autoMode => _autoMode;
188189
bool get isPingSending => _isPingSending;
190+
bool get isPingInProgress => _pingService?.pingInProgress ?? false; // True during entire ping + RX window (for auto pings)
191+
bool get isDiscoveryListening => _pingService?.isDiscoveryListening ?? false; // True during discovery listening window (for Passive Mode)
189192
int get queueSize => _queueSize;
190193
int? get currentNoiseFloor => _currentNoiseFloor;
191194
int? get currentBatteryPercent => _currentBatteryPercent;
@@ -258,6 +261,7 @@ class AppStateProvider extends ChangeNotifier {
258261
CooldownTimer get cooldownTimer => _cooldownTimer; // Shared cooldown for TX Ping and TX/RX Auto
259262
AutoPingTimer get autoPingTimer => _autoPingTimer;
260263
RxWindowTimer get rxWindowTimer => _rxWindowTimer;
264+
DiscoveryWindowTimer get discoveryWindowTimer => _discoveryWindowTimer; // Discovery listening window (Passive Mode)
261265

262266
// ============================================
263267
// Initialization
@@ -281,10 +285,11 @@ class AppStateProvider extends ChangeNotifier {
281285

282286
// Initialize status message and countdown timers
283287
_statusMessageService = StatusMessageService();
284-
// Pass notifyListeners callback to cooldown timer for smooth UI updates
288+
// Pass notifyListeners callback to timers for smooth UI updates
285289
_cooldownTimer = CooldownTimer(_statusMessageService, onUpdate: notifyListeners);
286-
_autoPingTimer = AutoPingTimer(_statusMessageService);
287-
_rxWindowTimer = RxWindowTimer(_statusMessageService);
290+
_autoPingTimer = AutoPingTimer(_statusMessageService, onUpdate: notifyListeners);
291+
_rxWindowTimer = RxWindowTimer(_statusMessageService, onUpdate: notifyListeners);
292+
_discoveryWindowTimer = DiscoveryWindowTimer(_statusMessageService, onUpdate: notifyListeners);
288293

289294
// Auto-enable debug logging for development builds
290295
await _autoEnableDebugLogsIfDevelopmentBuild();
@@ -624,6 +629,7 @@ class AppStateProvider extends ChangeNotifier {
624629
wakelockService: WakelockService(),
625630
cooldownTimer: _cooldownTimer,
626631
rxWindowTimer: _rxWindowTimer,
632+
discoveryWindowTimer: _discoveryWindowTimer,
627633
deviceId: _deviceId,
628634
txTracker: _txTracker,
629635
);
@@ -2263,6 +2269,7 @@ class AppStateProvider extends ChangeNotifier {
22632269
_cooldownTimer.dispose();
22642270
_autoPingTimer.dispose();
22652271
_rxWindowTimer.dispose();
2272+
_discoveryWindowTimer.dispose();
22662273
super.dispose();
22672274
}
22682275
}

lib/services/countdown_timer_service.dart

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,11 @@ class CooldownTimer extends CountdownTimerService {
140140
class AutoPingTimer extends CountdownTimerService {
141141
String? _skipReason;
142142

143-
AutoPingTimer(StatusMessageService statusService)
143+
AutoPingTimer(StatusMessageService statusService, {VoidCallback? onUpdate})
144144
: super(
145145
statusService,
146146
getStatusMessage: null, // Custom logic in override
147+
onUpdate: onUpdate,
147148
);
148149

149150
/// Set skip reason (e.g., "too close", "gps too old")
@@ -189,6 +190,9 @@ class AutoPingTimer extends CountdownTimerService {
189190

190191
// Mark first update as complete
191192
_isFirstUpdate = false;
193+
194+
// Trigger UI refresh callback after each update
195+
onUpdate?.call();
192196
}
193197

194198
@override
@@ -215,12 +219,26 @@ class AutoPingTimer extends CountdownTimerService {
215219
/// Specialized countdown timer for RX listening window
216220
/// Reference: wardrive.js state.rxListeningEndTime
217221
class RxWindowTimer extends CountdownTimerService {
218-
RxWindowTimer(StatusMessageService statusService)
222+
RxWindowTimer(StatusMessageService statusService, {VoidCallback? onUpdate})
219223
: super(
220224
statusService,
221225
getStatusMessage: (remainingSec) => CountdownResult(
222226
'Listening for responses (${remainingSec}s)',
223227
StatusColor.info,
224228
),
229+
onUpdate: onUpdate,
230+
);
231+
}
232+
233+
/// Specialized countdown timer for discovery listening window (Passive Mode)
234+
class DiscoveryWindowTimer extends CountdownTimerService {
235+
DiscoveryWindowTimer(StatusMessageService statusService, {VoidCallback? onUpdate})
236+
: super(
237+
statusService,
238+
getStatusMessage: (remainingSec) => CountdownResult(
239+
'Listening for nodes (${remainingSec}s)',
240+
StatusColor.info,
241+
),
242+
onUpdate: onUpdate,
225243
);
226244
}

lib/services/ping_service.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ import 'wakelock_service.dart';
3636
/// 3. Collect discovery responses (0x8E packets)
3737
/// 4. After window ends, create log entry and queue DISC API payloads
3838
class PingService {
39-
/// RX listening window duration (6 seconds - matches JS RX_LOG_LISTEN_WINDOW_MS = 6000)
40-
static const Duration _rxListeningWindow = Duration(seconds: 6);
39+
/// RX listening window duration (7 seconds - matches cooldown duration)
40+
static const Duration _rxListeningWindow = Duration(seconds: 7);
4141
/// Cooldown period between pings (7 seconds - matches JS COOLDOWN_MS = 7000)
4242
static const Duration _autoPingCooldown = Duration(seconds: 7);
4343
/// Discovery listening window duration (7 seconds)
@@ -51,6 +51,7 @@ class PingService {
5151
final WakelockService _wakelockService;
5252
final CooldownTimer _cooldownTimer;
5353
final RxWindowTimer _rxWindowCountdown;
54+
final DiscoveryWindowTimer _discoveryWindowCountdown;
5455
final String _deviceId;
5556
final TxTracker? _txTracker;
5657

@@ -111,6 +112,7 @@ class PingService {
111112
required WakelockService wakelockService,
112113
required CooldownTimer cooldownTimer,
113114
required RxWindowTimer rxWindowTimer,
115+
required DiscoveryWindowTimer discoveryWindowTimer,
114116
required String deviceId,
115117
TxTracker? txTracker,
116118
}) : _gpsService = gpsService,
@@ -119,6 +121,7 @@ class PingService {
119121
_wakelockService = wakelockService,
120122
_cooldownTimer = cooldownTimer,
121123
_rxWindowCountdown = rxWindowTimer,
124+
_discoveryWindowCountdown = discoveryWindowTimer,
122125
_deviceId = deviceId,
123126
_txTracker = txTracker;
124127

@@ -134,6 +137,9 @@ class PingService {
134137
/// Check if RX-only mode is active
135138
bool get rxOnlyMode => _rxOnlyMode;
136139

140+
/// Check if discovery tracker is currently listening (for Passive Mode UI)
141+
bool get isDiscoveryListening => _discTracker?.isListening ?? false;
142+
137143
/// Get current auto-ping interval in milliseconds
138144
int get autoPingIntervalMs => _autoPingIntervalMs;
139145

@@ -781,6 +787,9 @@ class PingService {
781787
windowDuration: _discoveryListeningWindow,
782788
);
783789

790+
// Start discovery window countdown display (7 seconds)
791+
_discoveryWindowCountdown.start(_discoveryListeningWindow.inMilliseconds);
792+
784793
// Store noise floor for later use
785794
_pendingTxNoiseFloor = noiseFloor;
786795

lib/widgets/ping_controls.dart

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@ class PingControls extends StatelessWidget {
2020
final canStartAuto = autoValidation == PingValidation.valid;
2121
final isTxRxAutoRunning = appState.autoPingEnabled && appState.autoMode == AutoMode.txRx;
2222
final isRxAutoRunning = appState.autoPingEnabled && appState.autoMode == AutoMode.rxOnly;
23-
final cooldownActive = appState.cooldownTimer.isRunning; // Shared cooldown for both TX Ping and TX/RX Auto
23+
final cooldownActive = appState.cooldownTimer.isRunning; // Shared cooldown after disabling Active Mode
2424
final cooldownRemaining = appState.cooldownTimer.remainingSec;
2525
final rxWindowActive = appState.rxWindowTimer.isRunning; // RX listening window after ping
2626
final rxWindowRemaining = appState.rxWindowTimer.remainingSec;
27-
final isPingSending = appState.isPingSending; // True immediately when button clicked
27+
final isPingSending = appState.isPingSending; // True immediately when manual ping button clicked
28+
final isPingInProgress = appState.isPingInProgress; // True during entire ping + RX window (includes auto pings)
29+
final autoPingWaiting = appState.autoPingTimer.isRunning; // Waiting for next auto ping
30+
final autoPingRemaining = appState.autoPingTimer.remainingSec;
31+
final discoveryWindowActive = appState.discoveryWindowTimer.isRunning; // Discovery listening window countdown (Passive Mode)
32+
final discoveryWindowRemaining = appState.discoveryWindowTimer.remainingSec;
2833

2934
// TX is blocked when offline mode is active and connected
3035
final txBlockedByOffline = appState.offlineMode && appState.isConnected;
@@ -80,63 +85,100 @@ class PingControls extends StatelessWidget {
8085
// Action buttons row
8186
Row(
8287
children: [
83-
// Send Ping button - disabled when offline mode is active, but works during Passive Mode
84-
// Shows active state with pulse animation during sending and RX listening window
88+
// Send Ping button
89+
// State flow: "Send Ping" → "Sending..." → "Listening Xs" → "Send Ping"
90+
// Also shows cooldown countdown when Active Mode is disabled or Passive Mode is listening
91+
// When Active/Passive Mode is running, just shows "Send Ping" or "Cooldown" (disabled)
8592
Expanded(
8693
child: _ActionButton(
8794
icon: Icons.cell_tower,
8895
label: txBlockedByOffline
8996
? 'TX Disabled'
90-
: isPingSending
91-
? 'Sending...'
92-
: rxWindowActive
93-
? 'Listening ${rxWindowRemaining}s'
94-
: (cooldownActive && !isTxRxAutoRunning ? '$cooldownRemaining s' : 'Send Ping'),
97+
: isTxRxAutoRunning
98+
? 'Send Ping' // Just disabled when Active Mode is running
99+
: isPingSending
100+
? 'Sending...'
101+
: rxWindowActive
102+
? 'Listening ${rxWindowRemaining}s' // Manual ping listening (works during Passive Mode too)
103+
: discoveryWindowActive
104+
? 'Cooldown ${discoveryWindowRemaining}s' // Cooldown during Passive Mode listening
105+
: cooldownActive
106+
? 'Cooldown ${cooldownRemaining}s' // After Active Mode disabled
107+
: 'Send Ping',
95108
color: const Color(0xFF0EA5E9), // sky-500
96-
enabled: canPing && !isTxRxAutoRunning && !cooldownActive && !txBlockedByOffline && !rxWindowActive && !isPingSending,
97-
isActive: isPingSending || rxWindowActive,
109+
enabled: canPing && !isTxRxAutoRunning && !cooldownActive && !txBlockedByOffline && !rxWindowActive && !isPingSending && !discoveryWindowActive,
110+
isActive: (isPingSending || rxWindowActive) && !isTxRxAutoRunning, // Only active during manual ping flow
98111
onPressed: () => _sendPing(context, appState),
99-
showCooldown: cooldownActive && !isTxRxAutoRunning && !txBlockedByOffline,
100-
subtitle: txBlockedByOffline ? 'Offline Mode' : ((isPingSending || rxWindowActive) ? null : moveSubtitle),
112+
showCooldown: false, // No longer needed - countdown shown in label
113+
subtitle: txBlockedByOffline ? 'Offline Mode' : ((isPingSending || rxWindowActive || cooldownActive || discoveryWindowActive) ? null : moveSubtitle),
101114
subtitleColor: txBlockedByOffline ? Colors.orange : Colors.orange.shade600,
102115
),
103116
),
104117
const SizedBox(width: 10),
105118

106-
// Active Mode button - disabled when offline mode is active
107-
// Can start even when tooCloseToLastPing - ping will be skipped until user moves
119+
// Active Mode button (toggle)
120+
// When ON: shows "Sending..." → "Listening Xs" → "Next ping in Xs" cycle
121+
// When OFF after being ON: shows "Cooldown Xs" like other buttons
122+
// During manual ping: shows "Cooldown Xs" (disabled)
108123
Expanded(
109124
child: _ActionButton(
110125
icon: Icons.sensors,
111126
label: txBlockedByOffline
112127
? 'TX Disabled'
113-
: (cooldownActive && !isTxRxAutoRunning && !isRxAutoRunning
114-
? '$cooldownRemaining s'
115-
: 'Active Mode'),
128+
: isTxRxAutoRunning
129+
? (isPingInProgress && !rxWindowActive
130+
? 'Sending...' // Brief moment while ping is being sent
131+
: rxWindowActive
132+
? 'Listening ${rxWindowRemaining}s' // During RX window
133+
: autoPingWaiting
134+
? 'Next ping ${autoPingRemaining}s' // Waiting for next auto ping
135+
: 'Active Mode') // Initial state before first ping
136+
: rxWindowActive
137+
? 'Cooldown ${rxWindowRemaining}s' // During manual ping
138+
: cooldownActive
139+
? 'Cooldown ${cooldownRemaining}s' // After Active Mode disabled
140+
: 'Active Mode',
116141
color: isTxRxAutoRunning
117142
? const Color(0xFF22C55E) // green-500
118143
: const Color(0xFF6366F1), // indigo-500
119-
enabled: (isTxRxAutoRunning || (canStartAuto && !isRxAutoRunning && !cooldownActive)) && !txBlockedByOffline,
120-
isActive: isTxRxAutoRunning,
144+
enabled: (isTxRxAutoRunning || (canStartAuto && !isRxAutoRunning && !cooldownActive && !isPingSending && !rxWindowActive)) && !txBlockedByOffline,
145+
isActive: isTxRxAutoRunning && (isPingInProgress || rxWindowActive || autoPingWaiting), // Active during sending/listening/waiting phases
121146
onPressed: () => _toggleTxRxAuto(context, appState),
147+
showCooldown: false, // No longer needed - countdown shown in label
122148
subtitle: txBlockedByOffline ? 'Offline Mode' : null,
123149
subtitleColor: Colors.orange,
124150
),
125151
),
126152
const SizedBox(width: 10),
127153

128-
// Passive Mode button
129-
// Passive Mode is passive listening - needs connection + antenna + power config, no cooldown/GPS/distance checks
154+
// Passive Mode button (toggle)
155+
// When ON: shows "Listening..." → "Next Disc Xs" cycle
156+
// When OFF: returns to normal, Active Mode re-enables immediately
157+
// Disabled during manual ping countdown phases, shows "Cooldown Xs"
158+
// When Active Mode is running, just shows "Passive Mode" (disabled, no countdown)
130159
Expanded(
131160
child: _ActionButton(
132161
icon: Icons.hearing,
133-
label: 'Passive Mode',
162+
label: isRxAutoRunning
163+
? (discoveryWindowActive
164+
? 'Listening ${discoveryWindowRemaining}s' // During discovery listening window
165+
: autoPingWaiting
166+
? 'Next Disc ${autoPingRemaining}s' // Waiting for next discovery
167+
: 'Passive Mode') // Initial state before first discovery
168+
: isTxRxAutoRunning
169+
? 'Passive Mode' // Just disabled when Active Mode is running
170+
: rxWindowActive
171+
? 'Cooldown ${rxWindowRemaining}s' // During manual ping listening
172+
: cooldownActive
173+
? 'Cooldown ${cooldownRemaining}s' // After Active Mode disabled
174+
: 'Passive Mode',
134175
color: isRxAutoRunning
135176
? const Color(0xFF22C55E) // green-500
136177
: const Color(0xFF6366F1), // indigo-500
137178
enabled: isRxAutoRunning || (appState.isConnected && !isTxRxAutoRunning &&
179+
!isPingSending && !rxWindowActive && !cooldownActive &&
138180
prefs.externalAntennaSet && isPowerSet),
139-
isActive: isRxAutoRunning,
181+
isActive: isRxAutoRunning && (discoveryWindowActive || autoPingWaiting), // Active during listening/waiting phases
140182
onPressed: () => _toggleRxAuto(context, appState),
141183
),
142184
),

0 commit comments

Comments
 (0)