66// - Acquire wake lock during auto mode to keep screen awake
77
88import { WebBleConnection } from "/content/mc/index.js" ; // your BLE client
9+ import Constants from "/content/mc/constants.js" ;
10+ import Packet from "/content/mc/packet.js" ;
911
1012// ---- Config ----
1113const CHANNEL_NAME = "#wardriving" ; // change to "#wardrive" if needed
@@ -15,6 +17,7 @@ const GPS_FRESHNESS_BUFFER_MS = 5000; // Buffer time for GPS freshness
1517const GPS_ACCURACY_THRESHOLD_M = 100 ; // Maximum acceptable GPS accuracy in meters
1618const MESHMAPPER_DELAY_MS = 7000 ; // Delay MeshMapper API call by 7 seconds
1719const COOLDOWN_MS = 7000 ; // Cooldown period for manual ping and auto toggle
20+ const REPEATER_LISTEN_MS = 7000 ; // Listen for repeater echoes for 7 seconds
1821const STATUS_UPDATE_DELAY_MS = 100 ; // Brief delay to ensure "Ping sent" status is visible
1922const MAP_REFRESH_DELAY_MS = 1000 ; // Delay after API post to ensure backend updated
2023const WARDROVE_KEY = new Uint8Array ( [
@@ -64,7 +67,10 @@ const state = {
6467 autoCountdownTimer : null , // Timer for auto-ping countdown display
6568 nextAutoPingTime : null , // Timestamp when next auto-ping will occur
6669 apiCountdownTimer : null , // Timer for API post countdown display
67- apiPostTime : null // Timestamp when API post will occur
70+ apiPostTime : null , // Timestamp when API post will occur
71+ repeaterListenTimer : null , // Timer for stopping repeater listener
72+ repeaterData : null , // Current repeater collection data {sessionLi, repeaters: Map}
73+ repeaterLogListener : null // LogRxData event listener reference
6874} ;
6975
7076// ---- UI helpers ----
@@ -454,6 +460,113 @@ function buildPayload(lat, lon) {
454460 return `${ PING_PREFIX } ${ coordsStr } ${ suffix } ` ;
455461}
456462
463+ // ---- Repeater Tracking ----
464+ function startRepeaterTracking ( sessionLi ) {
465+ // Clean up any existing tracking
466+ stopRepeaterTracking ( ) ;
467+
468+ // Initialize repeater data collection
469+ state . repeaterData = {
470+ sessionLi : sessionLi ,
471+ repeaters : new Map ( ) // Map of repeaterID -> SNR
472+ } ;
473+
474+ // Create listener for LogRxData events
475+ state . repeaterLogListener = ( logData ) => {
476+ try {
477+ // Check if repeater data collection is still active
478+ if ( ! state . repeaterData ) return ;
479+
480+ // Validate input data
481+ if ( ! logData || typeof logData . lastSnr !== 'number' || ! logData . raw ) return ;
482+
483+ // Parse the packet from raw data
484+ let packet ;
485+ try {
486+ packet = Packet . fromBytes ( logData . raw ) ;
487+ } catch ( parseError ) {
488+ console . warn ( "Failed to parse packet from LogRxData:" , parseError ) ;
489+ return ;
490+ }
491+
492+ // Check if this is a group text message (our ping echo)
493+ // Verify path exists and has at least one byte (repeater ID)
494+ if ( packet . getPayloadType ( ) === Packet . PAYLOAD_TYPE_GRP_TXT &&
495+ packet . path && packet . path . length > 0 ) {
496+ // Extract repeater ID (first byte of path)
497+ const repeaterId = packet . path [ 0 ] ;
498+
499+ // Validate repeater ID is a valid byte value (0-255)
500+ // Note: repeaterId can be 0 (valid byte value), so we check type and range
501+ if ( repeaterId === undefined || typeof repeaterId !== 'number' ||
502+ ! Number . isInteger ( repeaterId ) || repeaterId < 0 || repeaterId > 255 ) {
503+ console . warn ( `Invalid repeater ID: ${ repeaterId } ` ) ;
504+ return ;
505+ }
506+
507+ // SNR ranges from -12 to +12 dB
508+ // Note: logData.lastSnr is already processed (readInt8() / 4) by the connection layer
509+ const snr = Math . round ( logData . lastSnr ) ;
510+
511+ // Store or update repeater data
512+ // Keep the highest SNR value (closest to +12dB) for duplicate repeaters
513+ if ( ! state . repeaterData . repeaters . has ( repeaterId ) ||
514+ state . repeaterData . repeaters . get ( repeaterId ) < snr ) {
515+ state . repeaterData . repeaters . set ( repeaterId , snr ) ;
516+ console . log ( `Repeater detected: ID=${ repeaterId } , SNR=${ snr } dB` ) ;
517+ }
518+ }
519+ } catch ( e ) {
520+ console . warn ( "Failed to parse repeater data:" , e ) ;
521+ }
522+ } ;
523+
524+ // Start listening for LogRxData events
525+ if ( state . connection ) {
526+ state . connection . on ( Constants . PushCodes . LogRxData , state . repeaterLogListener ) ;
527+ }
528+
529+ // Schedule stop after REPEATER_LISTEN_MS
530+ state . repeaterListenTimer = setTimeout ( ( ) => {
531+ stopRepeaterTracking ( ) ;
532+ } , REPEATER_LISTEN_MS ) ;
533+ }
534+
535+ function stopRepeaterTracking ( ) {
536+ // Stop the timer
537+ if ( state . repeaterListenTimer ) {
538+ clearTimeout ( state . repeaterListenTimer ) ;
539+ state . repeaterListenTimer = null ;
540+ }
541+
542+ // Remove the event listener
543+ if ( state . repeaterLogListener && state . connection ) {
544+ state . connection . off ( Constants . PushCodes . LogRxData , state . repeaterLogListener ) ;
545+ state . repeaterLogListener = null ;
546+ }
547+
548+ // Update the session log entry with repeater data
549+ if ( state . repeaterData && state . repeaterData . sessionLi ) {
550+ const repeaters = state . repeaterData . repeaters ;
551+ if ( repeaters . size > 0 ) {
552+ // Format repeater data as [ID1(SNR1),ID2(SNR2),...]
553+ // SNR values range from -12 to +12 dB, e.g., [25(-8),21(-5),14(3)]
554+ const repeaterList = Array . from ( repeaters . entries ( ) )
555+ . sort ( ( a , b ) => a [ 0 ] - b [ 0 ] ) // Sort by repeater ID
556+ . map ( ( [ id , snr ] ) => `${ id } (${ snr } )` )
557+ . join ( ',' ) ;
558+
559+ // Append to the existing text in the session log
560+ const currentText = state . repeaterData . sessionLi . textContent ;
561+ state . repeaterData . sessionLi . textContent = `${ currentText } [${ repeaterList } ]` ;
562+ console . log ( `Session ping updated with ${ repeaters . size } repeater(s)` ) ;
563+ }
564+ }
565+
566+ // Clear repeater data
567+ state . repeaterData = null ;
568+ }
569+
457570// ---- MeshMapper API ----
458571async function postToMeshMapperAPI ( lat , lon ) {
459572 try {
@@ -616,7 +729,8 @@ async function sendPing(manual = false) {
616729 state . meshMapperTimer = null ;
617730 } , MESHMAPPER_DELAY_MS ) ;
618731
619- const nowStr = new Date ( ) . toLocaleString ( ) ;
732+ // Format timestamp as ISO 8601 without milliseconds: YYYY-MM-DDTHH:MM:SSZ
733+ const nowStr = new Date ( ) . toISOString ( ) . split ( '.' ) [ 0 ] + 'Z' ;
620734 if ( lastPingEl ) lastPingEl . textContent = `${ nowStr } — ${ payload } ` ;
621735
622736 // Session log
@@ -627,6 +741,9 @@ async function sendPing(manual = false) {
627741 sessionPingsEl . appendChild ( li ) ;
628742 // Auto-scroll to bottom when a new entry arrives
629743 sessionPingsEl . scrollTop = sessionPingsEl . scrollHeight ;
744+
745+ // Start tracking repeater echoes for this ping
746+ startRepeaterTracking ( li ) ;
630747 }
631748 } catch ( e ) {
632749 console . error ( "Ping failed:" , e ) ;
@@ -762,6 +879,7 @@ async function connect() {
762879 }
763880 stopAutoCountdown ( ) ;
764881 stopApiCountdown ( ) ;
882+ stopRepeaterTracking ( ) ;
765883 state . cooldownEndTime = null ;
766884
767885 state . lastFix = null ;
0 commit comments