@@ -25,8 +25,8 @@ class ApiService {
2525 /// API key (matching wardrive.js)
2626 static const String apiKey = '59C7754DABDF5C11CA5F5D8368F89' ;
2727
28- /// Heartbeat idle timeout (send keepalive after 3 minutes of no API activity)
29- static const Duration heartbeatIdleTimeout = Duration (minutes: 3 );
28+ /// Heartbeat buffer - schedule heartbeat 1 minute before session expiry
29+ static const Duration heartbeatBuffer = Duration (minutes: 1 );
3030
3131 final http.Client _client;
3232 bool _heartbeatEnabled = false ; // Track if heartbeat mode is active
@@ -215,12 +215,10 @@ class ApiService {
215215
216216 final data = json.decode (response.body) as Map <String , dynamic >;
217217
218- // Update expires_at if provided and reset heartbeat idle timer
219- if (data['success' ] == true ) {
220- if (data['expires_at' ] != null ) {
221- _sessionExpiresAt = data['expires_at' ] as int ;
222- }
223- _resetHeartbeatTimer (); // Resets 3-min idle timer on each successful upload
218+ // Update expires_at and schedule heartbeat if provided
219+ if (data['success' ] == true && data['expires_at' ] != null ) {
220+ _sessionExpiresAt = data['expires_at' ] as int ;
221+ scheduleHeartbeat (_sessionExpiresAt! );
224222 }
225223
226224 return data;
@@ -236,6 +234,7 @@ class ApiService {
236234 double ? lon,
237235 }) async {
238236 if (_sessionId == null ) {
237+ debugLog ('[HEARTBEAT] Cannot send heartbeat: no session_id' );
239238 return null ;
240239 }
241240
@@ -254,6 +253,10 @@ class ApiService {
254253 };
255254 }
256255
256+ // Log the full request payload
257+ debugLog ('[HEARTBEAT] POST $wardriveEndpoint ' );
258+ debugLog ('[HEARTBEAT] Payload: ${json .encode (payload )}' );
259+
257260 final response = await _client.post (
258261 Uri .parse (wardriveEndpoint),
259262 headers: {'Content-Type' : 'application/json' },
@@ -262,25 +265,34 @@ class ApiService {
262265
263266 final data = json.decode (response.body) as Map <String , dynamic >;
264267
265- // Update expires_at if provided (timer reset handled by _resetHeartbeatTimer in caller)
268+ // Log the response (matching wardrive.js logging)
269+ debugLog ('[HEARTBEAT] Response status: ${response .statusCode }' );
270+ debugLog ('[HEARTBEAT] Response data: ${json .encode (data )}' );
271+
272+ // Update expires_at and schedule next heartbeat if provided
266273 if (data['success' ] == true && data['expires_at' ] != null ) {
267274 _sessionExpiresAt = data['expires_at' ] as int ;
275+ scheduleHeartbeat (_sessionExpiresAt! );
268276 }
269277
270278 return data;
271279 } catch (e) {
280+ debugError ('[HEARTBEAT] Request failed: $e ' );
272281 return null ;
273282 }
274283 }
275284
276285 /// Enable heartbeat mode (called when auto mode starts)
277- /// Heartbeat fires after 3 minutes of API inactivity to keep session alive
286+ /// Heartbeat is scheduled based on expires_at from API responses
278287 /// @param gpsProvider Callback to get current GPS coordinates for heartbeat
279288 void enableHeartbeat ({({double lat, double lon})? Function ()? gpsProvider}) {
280289 _heartbeatEnabled = true ;
281290 _gpsProvider = gpsProvider;
282- _resetHeartbeatTimer ();
283- debugLog ('[API] Heartbeat mode enabled (3 min idle timeout)' );
291+ // Schedule initial heartbeat if we have an expiry time
292+ if (_sessionExpiresAt != null ) {
293+ scheduleHeartbeat (_sessionExpiresAt! );
294+ }
295+ debugLog ('[HEARTBEAT] Heartbeat mode enabled' );
284296 }
285297
286298 /// Disable heartbeat mode (called when auto mode stops)
@@ -289,32 +301,54 @@ class ApiService {
289301 _gpsProvider = null ;
290302 _heartbeatTimer? .cancel ();
291303 _heartbeatTimer = null ;
292- debugLog ('[API ] Heartbeat mode disabled' );
304+ debugLog ('[HEARTBEAT ] Heartbeat mode disabled' );
293305 }
294306
295- /// Reset the heartbeat timer (call after any API activity)
296- /// Timer fires 3 minutes after last API post to send keepalive
297- void _resetHeartbeatTimer () {
307+ /// Schedule heartbeat to fire before session expires
308+ /// Matches scheduleHeartbeat() in wardrive.js
309+ /// @param expiresAt Unix timestamp when session expires
310+ void scheduleHeartbeat (int expiresAt) {
311+ // Cancel any existing heartbeat timer
312+ _heartbeatTimer? .cancel ();
313+ _heartbeatTimer = null ;
314+
298315 if (! _heartbeatEnabled) return ;
299316
300- _heartbeatTimer? .cancel ();
301- _heartbeatTimer = Timer (heartbeatIdleTimeout, () async {
302- debugLog ('[API] Heartbeat timer fired (3 min idle), sending keepalive' );
317+ // Calculate when to send heartbeat (1 minute before expiry)
318+ final now = DateTime .now ().millisecondsSinceEpoch ~ / 1000 ;
319+ final secondsUntilExpiry = expiresAt - now;
320+ final secondsUntilHeartbeat = secondsUntilExpiry - heartbeatBuffer.inSeconds;
303321
304- // Get GPS coordinates from provider (matching wardrive.js behavior)
305- final coords = _gpsProvider? .call ();
306- final result = await sendHeartbeat (lat: coords? .lat, lon: coords? .lon);
322+ if (secondsUntilHeartbeat <= 0 ) {
323+ // Session is about to expire or already expired - send heartbeat immediately
324+ debugWarn ('[HEARTBEAT] Session expires in ${secondsUntilExpiry }s, sending immediately' );
325+ _sendScheduledHeartbeat ();
326+ return ;
327+ }
307328
308- if (result? ['success' ] == true ) {
309- debugLog ('[API] Heartbeat successful' );
310- _resetHeartbeatTimer (); // Schedule next heartbeat
311- } else {
312- debugWarn ('[API] Heartbeat failed: ${result ?['message' ]}' );
313- _onSessionExpiring? .call ();
314- }
329+ debugLog ('[HEARTBEAT] Scheduling in ${secondsUntilHeartbeat }s (session expires in ${secondsUntilExpiry }s)' );
330+
331+ _heartbeatTimer = Timer (Duration (seconds: secondsUntilHeartbeat), () {
332+ debugLog ('[HEARTBEAT] Timer fired, sending keepalive' );
333+ _sendScheduledHeartbeat ();
315334 });
316335 }
317336
337+ /// Send scheduled heartbeat with GPS coordinates
338+ Future <void > _sendScheduledHeartbeat () async {
339+ // Get GPS coordinates from provider (matching wardrive.js behavior)
340+ final coords = _gpsProvider? .call ();
341+ final result = await sendHeartbeat (lat: coords? .lat, lon: coords? .lon);
342+
343+ if (result? ['success' ] == true ) {
344+ debugLog ('[HEARTBEAT] Heartbeat successful' );
345+ // Next heartbeat will be scheduled when we get new expires_at
346+ } else {
347+ debugWarn ('[HEARTBEAT] Heartbeat failed: ${result ?['message' ]}' );
348+ _onSessionExpiring? .call ();
349+ }
350+ }
351+
318352 /// Clear session data and cancel heartbeat timer
319353 void _clearSession () {
320354 _sessionId = null ;
0 commit comments