@@ -73,6 +73,11 @@ const OTTAWA_GEOFENCE_RADIUS_M = 150000; // 150 km in meters
7373// Distance-Based Ping Filtering
7474const MIN_PING_DISTANCE_M = 25 ; // Minimum distance (25m) between pings
7575
76+ // Passive RX Log Batch Configuration
77+ const RX_BATCH_DISTANCE_M = 25 ; // Distance trigger for flushing batch (separate from MIN_PING_DISTANCE_M for independent tuning)
78+ const RX_BATCH_TIMEOUT_MS = 30000 ; // Max hold time per repeater (30 sec)
79+ const RX_BATCH_MIN_WAIT_MS = 2000 ; // Min wait to collect burst RX events
80+
7681// MeshMapper API Configuration
7782const MESHMAPPER_API_URL = "https://yow.meshmapper.net/wardriving-api.php" ;
7883const MESHMAPPER_CAPACITY_CHECK_URL = "https://yow.meshmapper.net/capacitycheck.php" ;
@@ -194,7 +199,8 @@ const state = {
194199 isListening : false , // Whether we're currently listening passively
195200 rxLogHandler : null , // Handler function for passive rx_log events
196201 entries : [ ] // Array of { repeaterId, snr, lat, lon, timestamp }
197- }
202+ } ,
203+ rxBatchBuffer : new Map ( ) // Map<repeaterId, {firstLocation, firstTimestamp, samples, timeoutId}>
198204} ;
199205
200206// Status message management with minimum visibility duration
@@ -548,6 +554,17 @@ function cleanupAllTimers() {
548554
549555 // Clear wardrive session ID
550556 state . wardriveSessionId = null ;
557+
558+ // Clear RX batch buffer and cancel any pending timeouts
559+ if ( state . rxBatchBuffer && state . rxBatchBuffer . size > 0 ) {
560+ for ( const [ repeaterId , batch ] of state . rxBatchBuffer . entries ( ) ) {
561+ if ( batch . timeoutId ) {
562+ clearTimeout ( batch . timeoutId ) ;
563+ }
564+ }
565+ state . rxBatchBuffer . clear ( ) ;
566+ debugLog ( "RX batch buffer cleared" ) ;
567+ }
551568}
552569
553570function enableControls ( connected ) {
@@ -1861,6 +1878,9 @@ async function handlePassiveRxLogging(packet, data) {
18611878
18621879 debugLog ( `[PASSIVE RX] ✅ Observation logged: repeater=${ repeaterId } , snr=${ data . lastSnr } , location=${ lat . toFixed ( 5 ) } ,${ lon . toFixed ( 5 ) } ` ) ;
18631880
1881+ // Handle batch tracking for API (parallel batch per repeater)
1882+ handlePassiveRxForAPI ( repeaterId , data . lastSnr , { lat, lon } ) ;
1883+
18641884 } catch ( error ) {
18651885 debugError ( `[PASSIVE RX] Error processing passive RX: ${ error . message } ` , error ) ;
18661886 }
@@ -2023,6 +2043,196 @@ async function postRxLogToMeshMapperAPI(entries) {
20232043 debugLog ( `[PASSIVE RX] Would post ${ entries . length } RX log entries to API (not implemented yet)` ) ;
20242044}
20252045
2046+ // ---- Passive RX Batch API Integration ----
2047+
2048+ /**
2049+ * Handle passive RX event for API batching
2050+ * Each repeater is tracked independently with its own batch and timer
2051+ * @param {string } repeaterId - Repeater ID (hex string)
2052+ * @param {number } snr - Signal to noise ratio
2053+ * @param {Object } currentLocation - Current GPS location {lat, lon}
2054+ */
2055+ function handlePassiveRxForAPI ( repeaterId , snr , currentLocation ) {
2056+ debugLog ( `[RX BATCH] Processing RX event: repeater=${ repeaterId } , snr=${ snr } ` ) ;
2057+
2058+ // Get or create batch for this repeater
2059+ let batch = state . rxBatchBuffer . get ( repeaterId ) ;
2060+
2061+ if ( ! batch ) {
2062+ // First RX from this repeater - create new batch
2063+ debugLog ( `[RX BATCH] Creating new batch for repeater ${ repeaterId } ` ) ;
2064+ batch = {
2065+ firstLocation : { lat : currentLocation . lat , lng : currentLocation . lon } ,
2066+ firstTimestamp : Date . now ( ) ,
2067+ samples : [ ] ,
2068+ timeoutId : null
2069+ } ;
2070+ state . rxBatchBuffer . set ( repeaterId , batch ) ;
2071+
2072+ // Set timeout for this repeater (independent timer)
2073+ batch . timeoutId = setTimeout ( ( ) => {
2074+ debugLog ( `[RX BATCH] Timeout triggered for repeater ${ repeaterId } after ${ RX_BATCH_TIMEOUT_MS } ms` ) ;
2075+ flushBatch ( repeaterId , 'timeout' ) ;
2076+ } , RX_BATCH_TIMEOUT_MS ) ;
2077+
2078+ debugLog ( `[RX BATCH] Timeout set for repeater ${ repeaterId } : ${ RX_BATCH_TIMEOUT_MS } ms` ) ;
2079+ }
2080+
2081+ // Add sample to batch
2082+ const sample = {
2083+ snr,
2084+ location : { lat : currentLocation . lat , lng : currentLocation . lon } ,
2085+ timestamp : Date . now ( )
2086+ } ;
2087+ batch . samples . push ( sample ) ;
2088+
2089+ debugLog ( `[RX BATCH] Sample added to batch for repeater ${ repeaterId } : sample_count=${ batch . samples . length } ` ) ;
2090+
2091+ // Check distance trigger (has user moved >= RX_BATCH_DISTANCE_M from first location?)
2092+ const distance = calculateHaversineDistance (
2093+ currentLocation . lat ,
2094+ currentLocation . lon ,
2095+ batch . firstLocation . lat ,
2096+ batch . firstLocation . lng
2097+ ) ;
2098+
2099+ debugLog ( `[RX BATCH] Distance check for repeater ${ repeaterId } : ${ distance . toFixed ( 2 ) } m from first location (threshold=${ RX_BATCH_DISTANCE_M } m)` ) ;
2100+
2101+ if ( distance >= RX_BATCH_DISTANCE_M ) {
2102+ debugLog ( `[RX BATCH] Distance threshold met for repeater ${ repeaterId } , flushing batch` ) ;
2103+ flushBatch ( repeaterId , 'distance' ) ;
2104+ }
2105+ }
2106+
2107+ /**
2108+ * Flush a single repeater's batch - aggregate and post to API
2109+ * @param {string } repeaterId - Repeater ID to flush
2110+ * @param {string } trigger - What caused the flush: 'distance' | 'timeout' | 'session_end'
2111+ */
2112+ function flushBatch ( repeaterId , trigger ) {
2113+ debugLog ( `[RX BATCH] Flushing batch for repeater ${ repeaterId } , trigger=${ trigger } ` ) ;
2114+
2115+ const batch = state . rxBatchBuffer . get ( repeaterId ) ;
2116+ if ( ! batch || batch . samples . length === 0 ) {
2117+ debugLog ( `[RX BATCH] No batch to flush for repeater ${ repeaterId } ` ) ;
2118+ return ;
2119+ }
2120+
2121+ // Clear timeout if it exists
2122+ if ( batch . timeoutId ) {
2123+ clearTimeout ( batch . timeoutId ) ;
2124+ debugLog ( `[RX BATCH] Cleared timeout for repeater ${ repeaterId } ` ) ;
2125+ }
2126+
2127+ // Calculate aggregations
2128+ const snrValues = batch . samples . map ( s => s . snr ) ;
2129+ const snrAvg = snrValues . reduce ( ( sum , val ) => sum + val , 0 ) / snrValues . length ;
2130+ const snrMax = Math . max ( ...snrValues ) ;
2131+ const snrMin = Math . min ( ...snrValues ) ;
2132+ const sampleCount = batch . samples . length ;
2133+ const timestampStart = batch . firstTimestamp ;
2134+ const timestampEnd = batch . samples [ batch . samples . length - 1 ] . timestamp ;
2135+
2136+ // Build API entry
2137+ const entry = {
2138+ repeater_id : repeaterId ,
2139+ location : batch . firstLocation ,
2140+ snr_avg : parseFloat ( snrAvg . toFixed ( 3 ) ) ,
2141+ snr_max : parseFloat ( snrMax . toFixed ( 3 ) ) ,
2142+ snr_min : parseFloat ( snrMin . toFixed ( 3 ) ) ,
2143+ sample_count : sampleCount ,
2144+ timestamp_start : timestampStart ,
2145+ timestamp_end : timestampEnd ,
2146+ trigger : trigger
2147+ } ;
2148+
2149+ debugLog ( `[RX BATCH] Aggregated entry for repeater ${ repeaterId } :` , entry ) ;
2150+ debugLog ( `[RX BATCH] snr_avg=${ snrAvg . toFixed ( 3 ) } , snr_max=${ snrMax . toFixed ( 3 ) } , snr_min=${ snrMin . toFixed ( 3 ) } ` ) ;
2151+ debugLog ( `[RX BATCH] sample_count=${ sampleCount } , duration=${ ( ( timestampEnd - timestampStart ) / 1000 ) . toFixed ( 1 ) } s` ) ;
2152+
2153+ // Queue for API posting
2154+ queueApiPost ( entry ) ;
2155+
2156+ // Remove batch from buffer (cleanup)
2157+ state . rxBatchBuffer . delete ( repeaterId ) ;
2158+ debugLog ( `[RX BATCH] Batch removed from buffer for repeater ${ repeaterId } ` ) ;
2159+ }
2160+
2161+ /**
2162+ * Flush all active batches (called on session end, disconnect, etc.)
2163+ * @param {string } trigger - What caused the flush: 'session_end' | 'disconnect' | etc.
2164+ */
2165+ function flushAllBatches ( trigger = 'session_end' ) {
2166+ debugLog ( `[RX BATCH] Flushing all batches, trigger=${ trigger } , active_batches=${ state . rxBatchBuffer . size } ` ) ;
2167+
2168+ if ( state . rxBatchBuffer . size === 0 ) {
2169+ debugLog ( `[RX BATCH] No batches to flush` ) ;
2170+ return ;
2171+ }
2172+
2173+ // Iterate all repeater batches and flush each one
2174+ const repeaterIds = Array . from ( state . rxBatchBuffer . keys ( ) ) ;
2175+ for ( const repeaterId of repeaterIds ) {
2176+ flushBatch ( repeaterId , trigger ) ;
2177+ }
2178+
2179+ debugLog ( `[RX BATCH] All batches flushed: ${ repeaterIds . length } repeaters` ) ;
2180+ }
2181+
2182+ /**
2183+ * Queue an entry for API posting
2184+ * For now: console.log in debug mode
2185+ * Future: Actual HTTP POST to MESHMAPPER_RX_LOG_API_URL
2186+ * @param {Object } entry - The aggregated entry to post
2187+ */
2188+ function queueApiPost ( entry ) {
2189+ // Validate session_id exists
2190+ if ( ! state . wardriveSessionId ) {
2191+ debugWarn ( `[RX BATCH API] Cannot post: no session_id available` ) ;
2192+ return ;
2193+ }
2194+
2195+ // Build API payload
2196+ const payload = {
2197+ session_id : state . wardriveSessionId ,
2198+ entries : [ entry ]
2199+ } ;
2200+
2201+ // DEBUG MODE: Console log the payload
2202+ debugLog ( `[RX BATCH API] ===== API PAYLOAD (DEBUG MODE) =====` ) ;
2203+ console . log ( '[RX BATCH API] Would POST to:' , MESHMAPPER_RX_LOG_API_URL || '(URL not configured)' ) ;
2204+ console . log ( '[RX BATCH API] Payload:' , JSON . stringify ( payload , null , 2 ) ) ;
2205+ debugLog ( `[RX BATCH API] =====================================` ) ;
2206+
2207+ // PRODUCTION CODE (commented out until ready)
2208+ /*
2209+ if (!MESHMAPPER_RX_LOG_API_URL) {
2210+ debugWarn('[RX BATCH API] API URL not configured, skipping POST');
2211+ return;
2212+ }
2213+
2214+ // Post to API
2215+ fetch(MESHMAPPER_RX_LOG_API_URL, {
2216+ method: 'POST',
2217+ headers: { 'Content-Type': 'application/json' },
2218+ body: JSON.stringify(payload)
2219+ })
2220+ .then(response => {
2221+ if (!response.ok) {
2222+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
2223+ }
2224+ return response.json();
2225+ })
2226+ .then(data => {
2227+ debugLog(`[RX BATCH API] ✅ Successfully posted entry for repeater ${entry.repeater_id}`);
2228+ debugLog(`[RX BATCH API] Response:`, data);
2229+ })
2230+ .catch(error => {
2231+ debugError(`[RX BATCH API] ❌ Failed to post entry for repeater ${entry.repeater_id}: ${error.message}`);
2232+ });
2233+ */
2234+ }
2235+
20262236// ---- Mobile Session Log Bottom Sheet ----
20272237
20282238/**
@@ -3126,6 +3336,9 @@ async function connect() {
31263336 stopRepeaterTracking ( ) ; // Stop repeater echo tracking
31273337 stopPassiveRxListening ( ) ; // Stop passive RX listening
31283338
3339+ // Flush all pending RX batch data before cleanup
3340+ flushAllBatches ( 'disconnect' ) ;
3341+
31293342 // Clean up all timers
31303343 cleanupAllTimers ( ) ;
31313344
0 commit comments