From 15a71bd9330ded1e1b426a537454e55db91cc912 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:36:25 +0000 Subject: [PATCH 01/27] Initial plan From 313edf7402fbd80f1a7723e50ccbb1e4e2fa54b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:43:43 +0000 Subject: [PATCH 02/27] Implement RX batching restructure and debug mode support Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 313 ++++++++++++++++++++++++++++++-------------- 1 file changed, 212 insertions(+), 101 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index b63e4be..807e2ec 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -236,6 +236,8 @@ const state = { capturedPingCoords: null, // { lat, lon, accuracy } captured at ping time, used for API post after 7s delay devicePublicKey: null, // Hex string of device's public key (used for capacity check) wardriveSessionId: null, // Session ID from capacity check API (used for all MeshMapper API posts) + debugMode: false, // Whether debug mode is enabled by MeshMapper API + tempTxRepeaterData: null, // Temporary storage for TX repeater debug data disconnectReason: null, // Tracks the reason for disconnection (e.g., "app_down", "capacity_full", "public_key_error", "channel_setup_error", "ble_disconnect_error", "session_id_error", "normal", or API reason codes like "outofdate") channelSetupErrorMessage: null, // Error message from channel setup failure bleDisconnectErrorMessage: null, // Error message from BLE disconnect failure @@ -254,7 +256,7 @@ const state = { rxLogHandler: null, // Handler function for passive rx_log events entries: [] // Array of { repeaterId, snr, lat, lon, timestamp } }, - rxBatchBuffer: new Map() // Map + rxBatchBuffer: new Map() // Map }; // API Batch Queue State @@ -617,16 +619,13 @@ function cleanupAllTimers() { // Clear device public key state.devicePublicKey = null; - // Clear wardrive session ID + // Clear wardrive session ID and debug mode state.wardriveSessionId = null; + state.debugMode = false; + state.tempTxRepeaterData = null; - // Clear RX batch buffer and cancel any pending timeouts + // Clear RX batch buffer (no timeouts to clear anymore) if (state.rxBatchBuffer && state.rxBatchBuffer.size > 0) { - for (const [repeaterId, batch] of state.rxBatchBuffer.entries()) { - if (batch.timeoutId) { - clearTimeout(batch.timeoutId); - } - } state.rxBatchBuffer.clear(); debugLog("[RX BATCH] RX batch buffer cleared"); } @@ -1281,7 +1280,7 @@ async function checkCapacity(reason) { } const data = await response.json(); - debugLog(`[CAPACITY] Capacity check response: allowed=${data.allowed}, session_id=${data.session_id || 'missing'}, reason=${data.reason || 'none'}`); + debugLog(`[CAPACITY] Capacity check response: allowed=${data.allowed}, session_id=${data.session_id || 'missing'}, debug_mode=${data.debug_mode || 'not set'}, reason=${data.reason || 'none'}`); // Handle capacity full vs. allowed cases separately if (data.allowed === false && reason === "connect") { @@ -1295,7 +1294,7 @@ async function checkCapacity(reason) { return false; } - // For connect requests, validate session_id is present when allowed === true + // For connect requests, validate session_id and check debug_mode if (reason === "connect" && data.allowed === true) { if (!data.session_id) { debugError("[CAPACITY] Capacity check returned allowed=true but session_id is missing"); @@ -1306,14 +1305,25 @@ async function checkCapacity(reason) { // Store the session_id for use in MeshMapper API posts state.wardriveSessionId = data.session_id; debugLog(`[CAPACITY] Wardrive session ID received and stored: ${state.wardriveSessionId}`); + + // Check for debug_mode flag (optional field) + if (data.debug_mode === 1) { + state.debugMode = true; + debugLog(`[CAPACITY] 🐛 DEBUG MODE ENABLED by API`); + } else { + state.debugMode = false; + debugLog(`[CAPACITY] Debug mode NOT enabled`); + } } - // For disconnect requests, clear the session_id + // For disconnect requests, clear the session_id and debug mode if (reason === "disconnect") { if (state.wardriveSessionId) { debugLog(`[CAPACITY] Clearing wardrive session ID on disconnect: ${state.wardriveSessionId}`); state.wardriveSessionId = null; } + state.debugMode = false; + debugLog(`[CAPACITY] Debug mode cleared on disconnect`); } return data.allowed === true; @@ -1332,6 +1342,39 @@ async function checkCapacity(reason) { } } +/** + * Convert raw bytes to hex string + * @param {Uint8Array} bytes - Raw bytes + * @returns {string} Hex string representation + */ +function bytesToHex(bytes) { + return Array.from(bytes).map(byte => byte.toString(16).padStart(2, '0').toUpperCase()).join(''); +} + +/** + * Build debug data object for a single packet observation + * @param {Object} rawPacketData - Raw packet data from handleSessionLogTracking or handlePassiveRxLogging + * @param {string} heardByte - The "heard" byte (first for TX, last for RX) as hex string + * @returns {Object} Debug data object + */ +function buildDebugData(rawPacketData, heardByte) { + const { raw, lastSnr, lastRssi, packet } = rawPacketData; + + // Convert path array to hex string (from parsed packet object) + const parsedPathHex = packet.path.map(byte => byte.toString(16).padStart(2, '0').toUpperCase()).join(''); + + return { + raw_packet: bytesToHex(raw), // Send complete raw packet as hex string + raw_snr: lastSnr, + raw_rssi: lastRssi, + parsed_header: packet.header.toString(16).padStart(2, '0').toUpperCase(), + parsed_path_length: packet.path.length, + parsed_path: parsedPathHex, + parsed_payload: bytesToHex(packet.payload), + parsed_heard: heardByte + }; +} + /** * Post wardrive ping data to MeshMapper API * @param {number} lat - Latitude @@ -1366,7 +1409,32 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { WARDRIVE_TYPE: "TX" }; - debugLog(`[API QUEUE] Posting to MeshMapper API: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, who=${payload.who}, power=${payload.power}, heard_repeats=${heardRepeats}, ver=${payload.ver}, iata=${payload.iata}, session_id=${payload.session_id}, WARDRIVE_TYPE=${payload.WARDRIVE_TYPE}`); + // Add debug data if debug mode is enabled and repeater data is available + if (state.debugMode && state.tempTxRepeaterData && state.tempTxRepeaterData.length > 0) { + debugLog(`[API QUEUE] 🐛 Debug mode active - building debug_data array for TX`); + + const debugDataArray = []; + + for (const repeater of state.tempTxRepeaterData) { + if (repeater.rawPacketData) { + const heardByte = repeater.repeaterId; // First byte of path + const debugData = buildDebugData(repeater.rawPacketData, heardByte); + debugData.repeaterId = repeater.repeaterId; // Add repeater ID + debugDataArray.push(debugData); + debugLog(`[API QUEUE] 🐛 Added debug data for TX repeater: ${repeater.repeaterId}`); + } + } + + if (debugDataArray.length > 0) { + payload.debug_data = debugDataArray; + debugLog(`[API QUEUE] 🐛 TX payload includes ${debugDataArray.length} debug_data entries`); + } + + // Clear temp data after use + state.tempTxRepeaterData = null; + } + + debugLog(`[API QUEUE] Posting to MeshMapper API: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, who=${payload.who}, power=${payload.power}, heard_repeats=${heardRepeats}, ver=${payload.ver}, iata=${payload.iata}, session_id=${payload.session_id}, WARDRIVE_TYPE=${payload.WARDRIVE_TYPE}${payload.debug_data ? `, debug_data=${payload.debug_data.length} entries` : ''}`); const response = await fetch(MESHMAPPER_API_URL, { method: "POST", @@ -1997,7 +2065,13 @@ async function handleSessionLogTracking(packet, data) { debugLog(`[PING] Deduplication decision: updating path ${pathHex} with better SNR: ${existing.snr} -> ${data.lastSnr}`); state.repeaterTracking.repeaters.set(pathHex, { snr: data.lastSnr, - seenCount: existing.seenCount + 1 + seenCount: existing.seenCount + 1, + rawPacketData: { // Update with better SNR packet data + raw: data.raw, + lastSnr: data.lastSnr, + lastRssi: data.lastRssi, + packet: packet + } }); // Trigger incremental UI update since SNR changed @@ -2012,7 +2086,13 @@ async function handleSessionLogTracking(packet, data) { debugLog(`[PING] Adding new repeater echo: path=${pathHex}, SNR=${data.lastSnr}`); state.repeaterTracking.repeaters.set(pathHex, { snr: data.lastSnr, - seenCount: 1 + seenCount: 1, + rawPacketData: { // Store for debug mode + raw: data.raw, + lastSnr: data.lastSnr, + lastRssi: data.lastRssi, + packet: packet + } }); // Trigger incremental UI update for the new repeater @@ -2043,10 +2123,11 @@ function stopRepeaterTracking() { // No need to unregister handler - unified handler continues running // Just clear the tracking state - // Get the results + // Get the results with full data (including rawPacketData for debug mode) const repeaters = Array.from(state.repeaterTracking.repeaters.entries()).map(([id, data]) => ({ repeaterId: id, - snr: data.snr + snr: data.snr, + rawPacketData: data.rawPacketData // Include for debug mode })); // Sort by repeater ID for deterministic output @@ -2160,8 +2241,24 @@ async function handlePassiveRxLogging(packet, data) { debugLog(`[PASSIVE RX] ✅ Observation logged: repeater=${repeaterId}, snr=${data.lastSnr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); - // Handle batch tracking for API (parallel batch per repeater) - handlePassiveRxForAPI(repeaterId, data.lastSnr, { lat, lon }); + // Store raw packet data for API handling + const rawPacketData = { + raw: data.raw, + lastSnr: data.lastSnr, + lastRssi: data.lastRssi, + packet: packet + }; + + // Handle tracking for API (best SNR with distance trigger) + handlePassiveRxForAPI( + repeaterId, + data.lastSnr, + data.lastRssi, + packet.path.length, + packet.header, + { lat, lon }, + rawPacketData + ); } catch (error) { debugError(`[PASSIVE RX] Error processing passive RX: ${error.message}`, error); @@ -2238,115 +2335,106 @@ async function postRxLogToMeshMapperAPI(entries) { /** * Handle passive RX event for API batching - * Each repeater is tracked independently with its own batch and timer + * Tracks best SNR observation per repeater with distance-based trigger * @param {string} repeaterId - Repeater ID (hex string) * @param {number} snr - Signal to noise ratio + * @param {number} rssi - Received signal strength indicator + * @param {number} pathLength - Number of hops in the path + * @param {number} header - Packet header byte * @param {Object} currentLocation - Current GPS location {lat, lon} + * @param {Object} rawPacketData - Raw packet data for debug mode */ -function handlePassiveRxForAPI(repeaterId, snr, currentLocation) { - debugLog(`[RX BATCH] Processing RX event: repeater=${repeaterId}, snr=${snr}`); - - // Get or create batch for this repeater - let batch = state.rxBatchBuffer.get(repeaterId); +function handlePassiveRxForAPI(repeaterId, snr, rssi, pathLength, header, currentLocation, rawPacketData) { + // Get or create buffer entry for this repeater + let buffer = state.rxBatchBuffer.get(repeaterId); - if (!batch) { - // First RX from this repeater - create new batch - debugLog(`[RX BATCH] Creating new batch for repeater ${repeaterId}`); - batch = { + if (!buffer) { + // First time hearing this repeater - create new entry + buffer = { firstLocation: { lat: currentLocation.lat, lng: currentLocation.lon }, - firstTimestamp: Date.now(), - samples: [], - timeoutId: null + bestObservation: { + snr, + rssi, + pathLength, + header, + lat: currentLocation.lat, + lon: currentLocation.lon, + timestamp: Date.now(), + rawPacketData + } }; - state.rxBatchBuffer.set(repeaterId, batch); - - // Set timeout for this repeater (independent timer) - batch.timeoutId = setTimeout(() => { - debugLog(`[RX BATCH] Timeout triggered for repeater ${repeaterId} after ${RX_BATCH_TIMEOUT_MS}ms`); - flushBatch(repeaterId, 'timeout'); - }, RX_BATCH_TIMEOUT_MS); - - debugLog(`[RX BATCH] Timeout set for repeater ${repeaterId}: ${RX_BATCH_TIMEOUT_MS}ms`); + state.rxBatchBuffer.set(repeaterId, buffer); + debugLog(`[RX BATCH] First observation for repeater ${repeaterId}: SNR=${snr}`); + } else { + // Already tracking this repeater - check if new SNR is better + if (snr > buffer.bestObservation.snr) { + debugLog(`[RX BATCH] Better SNR for repeater ${repeaterId}: ${buffer.bestObservation.snr} -> ${snr}`); + buffer.bestObservation = { + snr, + rssi, + pathLength, + header, + lat: currentLocation.lat, + lon: currentLocation.lon, + timestamp: Date.now(), + rawPacketData + }; + } else { + debugLog(`[RX BATCH] Ignoring worse SNR for repeater ${repeaterId}: current=${buffer.bestObservation.snr}, new=${snr}`); + } } - // Add sample to batch - const sample = { - snr, - location: { lat: currentLocation.lat, lng: currentLocation.lon }, - timestamp: Date.now() - }; - batch.samples.push(sample); - - debugLog(`[RX BATCH] Sample added to batch for repeater ${repeaterId}: sample_count=${batch.samples.length}`); - - // Check distance trigger (has user moved >= RX_BATCH_DISTANCE_M from first location?) + // Check distance trigger (25m from firstLocation) const distance = calculateHaversineDistance( currentLocation.lat, currentLocation.lon, - batch.firstLocation.lat, - batch.firstLocation.lng + buffer.firstLocation.lat, + buffer.firstLocation.lng ); - debugLog(`[RX BATCH] Distance check for repeater ${repeaterId}: ${distance.toFixed(2)}m from first location (threshold=${RX_BATCH_DISTANCE_M}m)`); + debugLog(`[RX BATCH] Distance check for repeater ${repeaterId}: ${distance.toFixed(2)}m from first observation (threshold=${RX_BATCH_DISTANCE_M}m)`); if (distance >= RX_BATCH_DISTANCE_M) { - debugLog(`[RX BATCH] Distance threshold met for repeater ${repeaterId}, flushing batch`); - flushBatch(repeaterId, 'distance'); + debugLog(`[RX BATCH] Distance threshold met for repeater ${repeaterId}, flushing`); + flushRepeater(repeaterId); } } /** - * Flush a single repeater's batch - aggregate and post to API + * Flush a single repeater's batch - post best observation to API * @param {string} repeaterId - Repeater ID to flush - * @param {string} trigger - What caused the flush: 'distance' | 'timeout' | 'session_end' */ -function flushBatch(repeaterId, trigger) { - debugLog(`[RX BATCH] Flushing batch for repeater ${repeaterId}, trigger=${trigger}`); +function flushRepeater(repeaterId) { + debugLog(`[RX BATCH] Flushing repeater ${repeaterId}`); - const batch = state.rxBatchBuffer.get(repeaterId); - if (!batch || batch.samples.length === 0) { - debugLog(`[RX BATCH] No batch to flush for repeater ${repeaterId}`); + const buffer = state.rxBatchBuffer.get(repeaterId); + if (!buffer) { + debugLog(`[RX BATCH] No buffer to flush for repeater ${repeaterId}`); return; } - // Clear timeout if it exists - if (batch.timeoutId) { - clearTimeout(batch.timeoutId); - debugLog(`[RX BATCH] Cleared timeout for repeater ${repeaterId}`); - } + const best = buffer.bestObservation; - // Calculate aggregations - const snrValues = batch.samples.map(s => s.snr); - const snrAvg = snrValues.reduce((sum, val) => sum + val, 0) / snrValues.length; - const snrMax = Math.max(...snrValues); - const snrMin = Math.min(...snrValues); - const sampleCount = batch.samples.length; - const timestampStart = batch.firstTimestamp; - const timestampEnd = batch.samples[batch.samples.length - 1].timestamp; - - // Build API entry + // Build API entry using BEST observation's location const entry = { repeater_id: repeaterId, - location: batch.firstLocation, - snr_avg: parseFloat(snrAvg.toFixed(3)), - snr_max: parseFloat(snrMax.toFixed(3)), - snr_min: parseFloat(snrMin.toFixed(3)), - sample_count: sampleCount, - timestamp_start: timestampStart, - timestamp_end: timestampEnd, - trigger: trigger + location: { lat: best.lat, lng: best.lon }, // Location of BEST SNR packet + snr: best.snr, + rssi: best.rssi, + pathLength: best.pathLength, + header: best.header, + timestamp: best.timestamp, + rawPacketData: best.rawPacketData // For future debug mode }; - debugLog(`[RX BATCH] Aggregated entry for repeater ${repeaterId}:`, entry); - debugLog(`[RX BATCH] snr_avg=${snrAvg.toFixed(3)}, snr_max=${snrMax.toFixed(3)}, snr_min=${snrMin.toFixed(3)}`); - debugLog(`[RX BATCH] sample_count=${sampleCount}, duration=${((timestampEnd - timestampStart) / 1000).toFixed(1)}s`); + debugLog(`[RX BATCH] Posting repeater ${repeaterId}: snr=${best.snr}, location=${best.lat.toFixed(5)},${best.lon.toFixed(5)}`); // Queue for API posting queueApiPost(entry); - // Remove batch from buffer (cleanup) + // Remove from buffer state.rxBatchBuffer.delete(repeaterId); - debugLog(`[RX BATCH] Batch removed from buffer for repeater ${repeaterId}`); + debugLog(`[RX BATCH] Repeater ${repeaterId} removed from buffer`); } /** @@ -2354,26 +2442,26 @@ function flushBatch(repeaterId, trigger) { * @param {string} trigger - What caused the flush: 'session_end' | 'disconnect' | etc. */ function flushAllBatches(trigger = 'session_end') { - debugLog(`[RX BATCH] Flushing all batches, trigger=${trigger}, active_batches=${state.rxBatchBuffer.size}`); + debugLog(`[RX BATCH] Flushing all repeaters, trigger=${trigger}, active_repeaters=${state.rxBatchBuffer.size}`); if (state.rxBatchBuffer.size === 0) { - debugLog(`[RX BATCH] No batches to flush`); + debugLog(`[RX BATCH] No repeaters to flush`); return; } - // Iterate all repeater batches and flush each one + // Iterate all repeaters and flush each one const repeaterIds = Array.from(state.rxBatchBuffer.keys()); for (const repeaterId of repeaterIds) { - flushBatch(repeaterId, trigger); + flushRepeater(repeaterId); } - debugLog(`[RX BATCH] All batches flushed: ${repeaterIds.length} repeaters`); + debugLog(`[RX BATCH] All repeaters flushed: ${repeaterIds.length} total`); } /** * Queue an entry for API posting * Uses the batch queue system to aggregate RX messages - * @param {Object} entry - The aggregated entry to post + * @param {Object} entry - The entry to post (with best observation data) */ function queueApiPost(entry) { // Validate session_id exists @@ -2382,10 +2470,9 @@ function queueApiPost(entry) { return; } - // Build unified API payload (without WARDRIVE_TYPE yet) - // Format heard_repeats as "repeater_id(snr_avg)" - e.g., "4e(12.0)" + // Format heard_repeats as "repeater_id(snr)" - e.g., "4e(12.0)" // Use absolute value and format with one decimal place - const heardRepeats = `${entry.repeater_id}(${Math.abs(entry.snr_avg).toFixed(1)})`; + const heardRepeats = `${entry.repeater_id}(${Math.abs(entry.snr).toFixed(1)})`; const payload = { key: MESHMAPPER_API_KEY, @@ -2400,9 +2487,25 @@ function queueApiPost(entry) { session_id: state.wardriveSessionId }; + // Add debug data if debug mode is enabled + if (state.debugMode && entry.rawPacketData) { + debugLog(`[RX BATCH API] 🐛 Debug mode active - adding debug_data for RX`); + + const packet = entry.rawPacketData.packet; + + // For RX, parsed_heard is the LAST byte of path + const lastHopId = packet.path[packet.path.length - 1]; + const heardByte = lastHopId.toString(16).padStart(2, '0').toUpperCase(); + + const debugData = buildDebugData(entry.rawPacketData, heardByte); + payload.debug_data = debugData; + + debugLog(`[RX BATCH API] 🐛 RX payload includes debug_data for repeater ${entry.repeater_id}`); + } + // Queue message instead of posting immediately queueApiMessage(payload, "RX"); - debugLog(`[RX BATCH API] RX message queued: repeater=${entry.repeater_id}, snr=${entry.snr_avg.toFixed(1)}, location=${entry.location.lat.toFixed(5)},${entry.location.lng.toFixed(5)}`); + debugLog(`[RX BATCH API] RX message queued: repeater=${entry.repeater_id}, snr=${entry.snr.toFixed(1)}, location=${entry.location.lat.toFixed(5)},${entry.location.lng.toFixed(5)}`); } // ---- Mobile Session Log Bottom Sheet ---- @@ -3630,6 +3733,12 @@ async function sendPing(manual = false) { const heardRepeatsStr = formatRepeaterTelemetry(repeaters); debugLog(`[PING] Formatted heard_repeats for API: "${heardRepeatsStr}"`); + // Store repeater data temporarily for debug mode + if (state.debugMode) { + state.tempTxRepeaterData = repeaters; + debugLog(`[PING] 🐛 Stored ${repeaters.length} repeater(s) data for debug mode`); + } + // Update status and start next timer IMMEDIATELY (before API post) // This is the key change: we don't wait for API to complete if (state.connection) { @@ -3939,6 +4048,8 @@ async function connect() { state.channel = null; state.devicePublicKey = null; // Clear public key state.wardriveSessionId = null; // Clear wardrive session ID + state.debugMode = false; // Clear debug mode + state.tempTxRepeaterData = null; // Clear temp TX data state.disconnectReason = null; // Reset disconnect reason state.channelSetupErrorMessage = null; // Clear error message state.bleDisconnectErrorMessage = null; // Clear error message From 9cb882170439e5971efa6a20c7992cf5e838562c Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Fri, 2 Jan 2026 15:11:25 -0500 Subject: [PATCH 03/27] Implement feature X to enhance user experience and optimize performance --- docs/Change1.md | 1424 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1424 insertions(+) create mode 100644 docs/Change1.md diff --git a/docs/Change1.md b/docs/Change1.md new file mode 100644 index 0000000..4c44605 --- /dev/null +++ b/docs/Change1.md @@ -0,0 +1,1424 @@ +# MeshCore GOME WarDriver - Development Guidelines + +## Overview +This document defines the coding standards and requirements for all changes to the MeshCore GOME WarDriver repository. AI agents and contributors must follow these guidelines for every modification. + +--- + +## Code Style & Standards + +### Debug Logging +- **ALWAYS** include debug console logging for significant operations +- Use the existing debug helper functions: + - `debugLog(message, ...args)` - For general debug information + - `debugWarn(message, ... args)` - For warning conditions + - `debugError(message, ... args)` - For error conditions +- Debug logging is controlled by the `DEBUG_ENABLED` flag (URL parameter `? debug=true`) +- Log at key points: function entry, API calls, state changes, errors, and decision branches + +#### Debug Log Tagging Convention + +All debug log messages **MUST** include a descriptive tag in square brackets immediately after `[DEBUG]` that identifies the subsystem or feature area. This enables easier filtering and understanding of debug output. + +**Format:** `[DEBUG] [TAG] Message here` + +**Required Tags:** + +| Tag | Description | +|-----|-------------| +| `[BLE]` | Bluetooth connection and device communication | +| `[GPS]` | GPS/geolocation operations | +| `[PING]` | Ping sending and validation | +| `[API QUEUE]` | API batch queue operations | +| `[RX BATCH]` | RX batch buffer operations | +| `[PASSIVE RX]` | Passive RX logging logic | +| `[PASSIVE RX UI]` | Passive RX UI rendering | +| `[SESSION LOG]` | Session log tracking | +| `[UNIFIED RX]` | Unified RX handler | +| `[DECRYPT]` | Message decryption | +| `[UI]` | General UI updates (status bar, buttons, etc.) | +| `[CHANNEL]` | Channel setup and management | +| `[TIMER]` | Timer and countdown operations | +| `[WAKE LOCK]` | Wake lock acquisition/release | +| `[GEOFENCE]` | Geofence and distance validation | +| `[CAPACITY]` | Capacity check API calls | +| `[AUTO]` | Auto ping mode operations | +| `[INIT]` | Initialization and setup | +| `[ERROR LOG]` | Error log UI operations | + +**Examples:** +```javascript +// ✅ Correct - includes tag +debugLog("[BLE] Connection established"); +debugLog("[GPS] Fresh position acquired: lat=45.12345, lon=-75.12345"); +debugLog("[PING] Sending ping to channel 2"); + +// ❌ Incorrect - missing tag +debugLog("Connection established"); +debugLog("Fresh position acquired"); +``` + +### Status Messages +- **ALWAYS** update `STATUS_MESSAGES.md` when adding or modifying user-facing status messages +- Use the `setStatus(message, color)` function for all UI status updates +- Use appropriate `STATUS_COLORS` constants: + - `STATUS_COLORS.idle` - Default/waiting state + - `STATUS_COLORS. success` - Successful operations + - `STATUS_COLORS.warning` - Warning conditions + - `STATUS_COLORS.error` - Error states + - `STATUS_COLORS.info` - Informational/in-progress states + +--- + +## Documentation Requirements + +### Code Comments +- Document complex logic with inline comments +- Use JSDoc-style comments for functions: + - `@param` for parameters + - `@returns` for return values + - Brief description of purpose + +### docs/STATUS_MESSAGES.md Updates +When adding new status messages, include: +- The exact status message text +- When it appears (trigger condition) +- The status color used +- Any follow-up actions or states + +### `docs/CONNECTION_WORKFLOW.md` Updates +When **modifying connect or disconnect logic**, you must: +- Read `docs/CONNECTION_WORKFLOW.md` before making the change (to understand current intended behavior). +- Update `docs/CONNECTION_WORKFLOW.md` so it remains accurate after the change: + - Steps/sequence of the workflow + - Any new states, retries, timeouts, or error handling + - Any UI impacts (buttons, indicators, status messages) + +### docs/PING_AUTO_PING_WORKFLOW.md Updates +When **modifying ping or auto-ping logic**, you must: +- Read `docs/PING_AUTO_PING_WORKFLOW.md` before making the change (to understand current intended behavior). +- Update `docs/PING_AUTO_PING_WORKFLOW.md` so it remains accurate after the change: + - Ping flows (manual `sendPing()`, auto-ping lifecycle) + - Validation logic (geofence, distance, cooldown) + - GPS acquisition and payload construction + - Repeater tracking and MeshMapper API posting + - Control locking and cooldown management + - Auto mode behavior (intervals, wake lock, page visibility) + - Any UI impacts (buttons, status messages, countdown displays) + +--- + +### Requested Change + +# Unified Refactor: RX Parsing Architecture, Naming Standardization, and RX Auto Mode + +## Overview +This is a comprehensive refactor covering three major tasks: +1. **Unified RX Parsing Architecture**: Single parsing point for RX packet metadata +2. **Complete Naming Standardization**: TX/RX terminology consistency across entire codebase +3. **RX Auto Mode**: New passive-only wardriving mode with always-on unified listener + +## Repository Context +- **Repository**: MrAlders0n/MeshCore-GOME-WarDriver +- **Branch**: dev +- **Language**: JavaScript (vanilla), HTML, CSS +- **Type**: Progressive Web App (PWA) for Meshtastic wardriving + +--- + +## Task 1: Unified RX Parsing Architecture + +### Objective +Refactor RX packet handling to use a single unified parsing function that extracts header/path metadata once, then routes to TX or RX wardriving handlers. This eliminates duplicate parsing, ensures consistency, and fixes debug data accuracy issues. + +### Current Problems +1. Header and path are parsed separately in `handleSessionLogTracking()` and `handlePassiveRxLogging()` +2. Debug data uses `packet.path` from decrypted packet instead of actual raw path bytes +3. Performance waste - same bytes parsed multiple times per packet +4. Inconsistency risk - two different code paths doing same extraction +5. Debug mode `parsed_path` shows incorrect data (e.g., "0" instead of "4E") + +### Required Changes + +#### 1. Create Unified Metadata Parser + +Create new function `parseRxPacketMetadata(data)` in `content/wardrive.js`: + +**Location**: Add after `computeChannelHash()` function (around line 1743) + +**Implementation**: +- Extract header byte from `data.raw[0]` +- Extract path length from header upper 4 bits: `(header >> 4) & 0x0F` +- Extract raw path bytes as array: `data.raw.slice(1, 1 + pathLength)` +- Derive first hop (for TX repeater ID): `pathBytes[0]` +- Derive last hop (for RX repeater ID): `pathBytes[pathLength - 1]` +- Extract encrypted payload: `data.raw.slice(1 + pathLength)` + +**Return object structure**: +{ + raw: data.raw, // Full raw packet bytes + header: header, // Header byte + pathLength: pathLength, // Number of hops + pathBytes: pathBytes, // Raw path bytes array + firstHop: pathBytes[0], // First hop ID (TX) + lastHop: pathBytes[pathLength-1], // Last hop ID (RX) + snr: data.lastSnr, // SNR value + rssi: data.lastRssi, // RSSI value + encryptedPayload: payload // Rest of packet +} + +**JSDoc**: +/** + * Parse RX packet metadata from raw bytes + * Single source of truth for header/path extraction + * @param {Object} data - LogRxData event data (contains lastSnr, lastRssi, raw) + * @returns {Object} Parsed metadata object + */ + +**Debug logging**: +- Log when parsing starts +- Log extracted values (header, pathLength, firstHop, lastHop) +- Use `[RX PARSE]` debug tag + +#### 2. Refactor TX Handler + +Update `handleSessionLogTracking()` (will also be renamed to `handleTxLogging()` in Task 2): + +**Changes**: +- Accept metadata object as first parameter instead of packet object +- Remove duplicate header/path parsing code +- Use `metadata.header` for header validation +- Use `metadata.firstHop` for repeater ID extraction +- Use `metadata.pathBytes` for debug data +- Store full metadata in repeater tracking for debug mode +- Decrypt payload using `metadata.encryptedPayload` if needed +- Update to work with pre-parsed metadata throughout + +**Signature change**: +// OLD: +async function handleSessionLogTracking(packet, data) + +// NEW (after Task 2 rename): +async function handleTxLogging(metadata, data) + +**Key changes**: +- Replace `packet.header` with `metadata.header` +- Replace `packet.path[0]` with `metadata.firstHop` +- Replace `packet.payload` with `metadata.encryptedPayload` +- Store metadata object (not just SNR) in `state.txTracking.repeaters` for debug mode +- For decryption, use metadata.encryptedPayload + +#### 3. Refactor RX Handler + +Update `handlePassiveRxLogging()` (will also be renamed to `handleRxLogging()` in Task 2): + +**Changes**: +- Accept metadata object as first parameter instead of packet object +- Remove all header/path parsing code (already done in parseRxPacketMetadata) +- Use `metadata.lastHop` for repeater ID extraction +- Use `metadata.pathLength` for path length +- Use `metadata.header` for header value +- Pass metadata to batching function + +**Signature change**: +// OLD: +async function handlePassiveRxLogging(packet, data) + +// NEW (after Task 2 rename): +async function handleRxLogging(metadata, data) + +**Key changes**: +- Replace `packet.path.length` with `metadata.pathLength` +- Replace `packet.path[packet.path.length - 1]` with `metadata.lastHop` +- Replace `packet.header` with `metadata.header` +- Pass metadata to handleRxBatching() instead of building separate rawPacketData object + +#### 4. Fix Debug Data Generation + +Update `buildDebugData()` function: + +**Changes**: +- Accept metadata object as first parameter +- Use `metadata.pathBytes` for `parsed_path` field (NOT packet.path) +- Use `metadata.header` for `parsed_header` field +- Convert `metadata.pathBytes` directly to hex string +- Ensure repeaterId matches first/last byte of pathBytes + +**Signature change**: +// OLD: +function buildDebugData(rawPacketData, heardByte) + +// NEW: +function buildDebugData(metadata, heardByte, repeaterId) + +**Implementation**: +function buildDebugData(metadata, heardByte, repeaterId) { + // Convert path bytes to hex string - these are the ACTUAL bytes used + const parsedPathHex = Array.from(metadata. pathBytes) + .map(byte => byte.toString(16).padStart(2, '0').toUpperCase()) + .join(''); + + return { + raw_packet: bytesToHex(metadata.raw), + raw_snr: metadata.snr, + raw_rssi: metadata.rssi, + parsed_header: metadata.header. toString(16).padStart(2, '0').toUpperCase(), + parsed_path_length: metadata.pathLength, + parsed_path: parsedPathHex, // ACTUAL raw bytes + parsed_payload: bytesToHex(metadata. encryptedPayload), + parsed_heard: heardByte, + repeaterId: repeaterId + }; +} + +#### 5. Update Unified RX Handler + +Update `handleUnifiedRxLogEvent()`: + +**Changes**: +- Call `parseRxPacketMetadata(data)` FIRST before any routing +- Pass metadata to handlers instead of packet +- Remove `Packet.fromBytes()` call from unified handler (moved to individual handlers if needed) +- Keep decrypt logic in TX handler only (TX needs encrypted content) + +**Updated flow**: +async function handleUnifiedRxLogEvent(data) { + try { + // Parse metadata ONCE + const metadata = parseRxPacketMetadata(data); + + debugLog(`[UNIFIED RX] Packet received: header=0x${metadata.header.toString(16)}, pathLength=${metadata.pathLength}`); + + // Route to TX tracking if active + if (state.txTracking.isListening) { + debugLog("[UNIFIED RX] TX tracking active - delegating to TX handler"); + const wasEcho = await handleTxLogging(metadata, data); + if (wasEcho) { + debugLog("[UNIFIED RX] Packet was TX echo, done"); + return; + } + } + + // Route to RX wardriving if active + if (state.rxTracking.isWardriving) { + debugLog("[UNIFIED RX] RX wardriving active - delegating to RX handler"); + await handleRxLogging(metadata, data); + } + } catch (error) { + debugError("[UNIFIED RX] Error processing rx_log entry", error); + } +} + +#### 6. Update Debug Mode Integration + +Update debug data usage in: +- `postToMeshMapperAPI()` (TX debug data) - around line 1409 +- `queueRxApiPost()` (RX debug data) - around line 2378 + +**Changes for TX debug data**: +- Access metadata from stored repeater data: `repeater.metadata` +- Call `buildDebugData(repeater.metadata, heardByte, repeater.repeaterId)` +- For TX: heardByte is the repeaterId (first hop) + +**Changes for RX debug data**: +- Access metadata from batch entry: `entry.metadata` +- Call `buildDebugData(entry.metadata, heardByte, entry.repeater_id)` +- For RX: heardByte is last hop from metadata. pathBytes + +**Example TX integration**: +if (state.debugMode && state.tempTxRepeaterData && state.tempTxRepeaterData.length > 0) { + const debugDataArray = []; + for (const repeater of state.tempTxRepeaterData) { + if (repeater.metadata) { + const heardByte = repeater.repeaterId; + const debugData = buildDebugData(repeater.metadata, heardByte, repeater.repeaterId); + debugDataArray.push(debugData); + } + } + if (debugDataArray.length > 0) { + payload.debug_data = debugDataArray; + } +} + +**Example RX integration**: +if (state.debugMode && entry.metadata) { + const lastHopId = entry.metadata.lastHop; + const heardByte = lastHopId. toString(16).padStart(2, '0').toUpperCase(); + const debugData = buildDebugData(entry.metadata, heardByte, entry.repeater_id); + payload.debug_data = debugData; +} + +#### 7. Update Repeater Tracking Storage + +In `handleTxLogging()` (renamed from handleSessionLogTracking): + +**Store full metadata**: +state.txTracking.repeaters. set(pathHex, { + snr: data.lastSnr, + seenCount: 1, + metadata: metadata // Store full metadata for debug mode +}); + +In `stopTxTracking()` (renamed from stopRepeaterTracking): + +**Return metadata with repeater data**: +const repeaters = Array.from(state.txTracking.repeaters.entries()).map(([id, data]) => ({ + repeaterId: id, + snr: data.snr, + metadata: data.metadata // Include metadata for debug mode +})); + +#### 8. Update RX Batching Storage + +In `handleRxBatching()` (renamed from handlePassiveRxForAPI): + +**Store metadata in buffer**: +buffer = { + firstLocation: { lat: currentLocation.lat, lng: currentLocation.lon }, + bestObservation: { + snr, + rssi, + pathLength, + header, + lat: currentLocation.lat, + lon: currentLocation.lon, + timestamp: Date.now(), + metadata: metadata // Store full metadata for debug mode + } +}; + +In `flushRxBatch()` (renamed from flushBatch): + +**Include metadata in entry**: +const entry = { + repeater_id: repeaterId, + location: { lat: best.lat, lng: best.lon }, + snr: best.snr, + rssi: best.rssi, + pathLength: best.pathLength, + header: best.header, + timestamp: best.timestamp, + metadata: best.metadata // For debug mode +}; + +### Validation Requirements +- Debug data `parsed_path` must show actual raw bytes used for repeater ID determination +- For TX: `parsed_path` first byte must equal `repeaterId` and `parsed_heard` +- For RX: `parsed_path` last byte must equal `repeaterId` used in API post +- No duplicate parsing - single call to parseRxPacketMetadata() per packet +- All existing functionality preserved (TX/RX tracking, API posting, UI updates) +- Debug logging at each step with [RX PARSE] tag + +--- + +## Task 2: Complete Naming Standardization + +### Objective +Standardize all naming conventions across the codebase to use consistent TX/RX terminology, eliminating legacy "session log" and "repeater tracking" names. + +### Naming Convention Rules +- **TX** = Active ping wardriving (send ping, track echoes) +- **RX** = Passive observation wardriving (listen to all packets) +- Use `Tracking` for operational state (lifecycle management) +- Use `Log` for UI state (display/export) +- Use `TxRxAuto` for combined TX + RX auto mode +- Use `RxAuto` for RX-only auto mode + +### Required Changes + +#### 1. State Object Renames + +**File**: `content/wardrive.js` + +**Main operational state (state object)**: + +RENAME state.repeaterTracking TO state.txTracking + - All properties: + - state.txTracking.isListening + - state.txTracking. sentTimestamp + - state.txTracking.sentPayload + - state.txTracking.channelIdx + - state.txTracking.repeaters + - state. txTracking.listenTimeout + - state.txTracking.rxLogHandler + - state.txTracking.currentLogEntry + +RENAME state.passiveRxTracking TO state. rxTracking + - Properties: + - state.rxTracking.isListening + - state.rxTracking.rxLogHandler + - REMOVE state.rxTracking.entries (unused array) + +RENAME state.running TO state.txRxAutoRunning + - This is the flag for TX/RX Auto mode + - Find ALL references throughout codebase + +state.rxAutoRunning - NO CHANGE (already correct) + +**UI state (standalone consts)**: + +RENAME sessionLogState TO txLogState + - All references to sessionLogState + +rxLogState - NO CHANGE +errorLogState - NO CHANGE + +#### 2. Function Renames + +**File**: `content/wardrive.js` + +**Core handlers**: +- RENAME handleSessionLogTracking() TO handleTxLogging() +- RENAME handlePassiveRxLogging() TO handleRxLogging() +- RENAME handlePassiveRxForAPI() TO handleRxBatching() +- handleUnifiedRxLogEvent() - NO CHANGE + +**Lifecycle functions**: +- RENAME startRepeaterTracking() TO startTxTracking() +- RENAME stopRepeaterTracking() TO stopTxTracking() +- startUnifiedRxListening() - NO CHANGE +- stopUnifiedRxListening() - NO CHANGE + +**UI functions**: +- RENAME addLogEntry() TO addTxLogEntry() +- RENAME updateLogSummary() TO updateTxLogSummary() +- RENAME renderLogEntries() TO renderTxLogEntries() +- RENAME toggleBottomSheet() TO toggleTxLogBottomSheet() +- RENAME updateCurrentLogEntryWithLiveRepeaters() TO updateCurrentTxLogEntryWithLiveRepeaters() +- RENAME updatePingLogWithRepeaters() TO updateTxLogWithRepeaters() +- RENAME logPingToUI() TO logTxPingToUI() +- addRxLogEntry() - NO CHANGE +- updateRxLogSummary() - NO CHANGE +- renderRxLogEntries() - NO CHANGE +- toggleRxLogBottomSheet() - NO CHANGE + +**Export functions**: +- RENAME sessionLogToCSV() TO txLogToCSV() +- rxLogToCSV() - NO CHANGE +- errorLogToCSV() - NO CHANGE + +**Batch/API functions**: +- RENAME flushBatch() TO flushRxBatch() +- RENAME flushAllBatches() TO flushAllRxBatches() +- RENAME queueApiPost() TO queueRxApiPost() + +**Helper functions**: +- formatRepeaterTelemetry() - NO CHANGE (generic) + +#### 3. DOM Element Reference Renames + +**File**: `content/wardrive.js` + +RENAME all Session Log DOM references: +- sessionPingsEl TO txPingsEl +- logSummaryBar TO txLogSummaryBar +- logBottomSheet TO txLogBottomSheet +- logScrollContainer TO txLogScrollContainer +- logCount TO txLogCount +- logLastTime TO txLogLastTime +- logLastSnr TO txLogLastSnr +- sessionLogCopyBtn TO txLogCopyBtn + +RENAME button references: +- sendPingBtn TO txPingBtn +- autoToggleBtn TO txRxAutoBtn + +RX Log DOM references - NO CHANGE (already correct): +- rxLogSummaryBar +- rxLogBottomSheet +- rxLogScrollContainer +- rxLogCount +- rxLogLastTime +- rxLogLastRepeater +- rxLogSnrChip +- rxLogEntries +- rxLogExpandArrow +- rxLogCopyBtn + +#### 4. HTML Element ID Renames + +**File**: `index.html` + +UPDATE all Session Log element IDs: +- id="sessionPings" TO id="txPings" +- id="logSummaryBar" TO id="txLogSummaryBar" +- id="logBottomSheet" TO id="txLogBottomSheet" +- id="logScrollContainer" TO id="txLogScrollContainer" +- id="logCount" TO id="txLogCount" +- id="logLastTime" TO id="txLogLastTime" +- id="logLastSnr" TO id="txLogLastSnr" +- id="sessionLogCopyBtn" TO id="txLogCopyBtn" +- id="logExpandArrow" TO id="txLogExpandArrow" + +UPDATE button IDs: +- id="sendPingBtn" TO id="txPingBtn" +- id="autoToggleBtn" TO id="txRxAutoBtn" + +UPDATE user-facing labels: +- H2 heading text: "Session Log" TO "TX Log" +- Button text: "Send Ping" TO "TX Ping" +- Button text: "Start Auto Ping" / "Stop Auto Ping" TO "TX/RX Auto" / "Stop TX/RX" + +#### 5. Debug Log Tag Updates + +**Files**: `content/wardrive.js`, all documentation files + +REPLACE debug tags throughout: +- [SESSION LOG] TO [TX LOG] +- [PASSIVE RX] TO [RX LOG] +- [PASSIVE RX UI] TO [RX LOG UI] +- [AUTO] TO [TX/RX AUTO] (when referring to auto ping mode) + +KEEP unchanged: +- [RX BATCH] (API batching operations) +- [API QUEUE] +- [UNIFIED RX] +- [BLE] +- [GPS] +- [PING] +- etc. + +#### 6. CSS Comments Update + +**File**: `content/style.css` + +UPDATE comment: +/* Session Log - Static Expandable Section */ +TO +/* TX Log - Static Expandable Section */ + +#### 7. Documentation File Updates + +**Files to update**: + +**docs/DEVELOPMENT_REQUIREMENTS.md**: +- Update debug tag table: [SESSION LOG] → [TX LOG] +- Update debug tag table: [AUTO] → [TX/RX AUTO] +- Update debug tag table: [PASSIVE RX] → [RX LOG] +- Update debug tag table: [PASSIVE RX UI] → [RX LOG UI] + +**docs/PING_WORKFLOW.md**: +- Replace "session log" with "TX log" throughout +- Replace "Session Log" with "TX Log" throughout +- Replace "repeater tracking" with "TX tracking" (when referring to TX) +- Replace "auto mode" with "TX/RX Auto mode" +- Replace "Auto Ping" with "TX/RX Auto" +- Update function references to new names +- Update state variable references to new names + +**docs/CONNECTION_WORKFLOW.md**: +- Replace "session log" with "TX log" +- Replace "repeater tracking" with "TX tracking" +- Update function references: stopRepeaterTracking() → stopTxTracking() + +**docs/FLOW_WARDRIVE_TX_DIAGRAM.md**: +- Replace "SESSION LOG HANDLER" with "TX LOG HANDLER" +- Replace "Session Log" with "TX Log" throughout +- Update all function names in diagram + +**docs/FLOW_WARDRIVE_RX_DIAGRAM. md**: +- Replace "PASSIVE RX HANDLER" with "RX LOG HANDLER" +- Update function names in diagram + +**CHANGES_SUMMARY.md**: +- Update historical references (optional, for consistency) + +#### 8. Code Comments and JSDoc Updates + +**File**: `content/wardrive.js` + +UPDATE all inline comments: +- "session log" → "TX log" +- "Session Log" → "TX Log" +- "repeater tracking" (when referring to TX) → "TX tracking" +- "passive RX" (when referring to logging) → "RX logging" +- "auto mode" → "TX/RX Auto mode" +- "Auto Ping" → "TX/RX Auto" + +UPDATE all JSDoc comments: +- Function descriptions mentioning "session log" → "TX log" +- Function descriptions mentioning "repeater" → "TX tracking" or "repeater telemetry" (as appropriate) +- Parameter descriptions +- Return value descriptions + +#### 9. Event Listener Updates + +**File**: `content/wardrive.js` (in onLoad function) + +UPDATE event listeners: +- sendPingBtn. addEventListener → txPingBtn.addEventListener +- autoToggleBtn.addEventListener → txRxAutoBtn.addEventListener +- logSummaryBar.addEventListener → txLogSummaryBar.addEventListener +- sessionLogCopyBtn.addEventListener → txLogCopyBtn.addEventListener + +#### 10. Copy to Clipboard Function Updates + +**File**: `content/wardrive.js` (copyLogToCSV function) + +UPDATE switch statement: +case 'session': + csv = txLogToCSV(); + logTag = '[TX LOG]'; + break; + +### Validation Requirements +- All references to old names must be updated +- No broken references (undefined variables/functions) +- All functionality preserved (no behavior changes) +- Debug logging uses new tags consistently +- Documentation matches code +- UI labels updated for user-facing text +- HTML IDs match JavaScript selectors + +--- + +## Task 3: RX Auto Mode with Always-On Unified Listener + +### Objective +Add a new "RX Auto" button that enables RX-only wardriving (no transmission), while restructuring the unified RX listener to be always active when connected. This enables three distinct modes: TX Ping (manual), TX/RX Auto (current auto behavior), and RX Auto (new passive-only mode). + +### Current Behavior +- Unified RX listener starts when TX/RX Auto button clicked +- Unified RX listener stops when TX/RX Auto button clicked again +- No way to do RX wardriving without TX transmission + +### New Behavior +- Unified RX listener starts IMMEDIATELY on connect and stays on entire connection +- Unified listener NEVER stops except on disconnect +- RX wardriving subscription controlled by flag: `state.rxTracking.isWardriving` +- Three buttons: TX Ping, TX/RX Auto, RX Auto + +### Required Changes + +#### 1. Add New State Properties + +**File**: `content/wardrive.js` + +ADD to state. rxTracking object: +state.rxTracking = { + isListening: true, // TRUE when connected (unified listener) + isWardriving: false, // TRUE when TX/RX Auto OR RX Auto enabled + rxLogHandler: null + // entries removed in Task 2 +}; + +ADD new top-level state property: +state.rxAutoRunning = false; // TRUE when RX Auto mode active + +state.txRxAutoRunning already exists (renamed from state.running in Task 2) + +#### 2. Update Connection Flow + +**File**: `content/wardrive.js` (connect function) + +**Changes in connect() function**: + +MOVE startUnifiedRxListening() to run IMMEDIATELY after channel setup: +async function connect() { + // ... BLE connection ... + // ... Channel setup (ensureChannel) ... + + // START unified RX listener immediately after channel ready + startUnifiedRxListening(); + debugLog("[BLE] Unified RX listener started on connect"); + + // CLEAR all logs on connect (new session) + txLogState.entries = []; + renderTxLogEntries(true); + updateTxLogSummary(); + + rxLogState.entries = []; + renderRxLogEntries(true); + updateRxLogSummary(); + + errorLogState.entries = []; + renderErrorLogEntries(true); + updateErrorLogSummary(); + + debugLog("[BLE] All logs cleared on connect (new session)"); + + // ... GPS initialization ... + // ... Connection complete ... +} + +#### 3. Update Disconnect Flow + +**File**: `content/wardrive.js` (disconnect handler) + +**Changes in disconnected event handler**: + +KEEP stopUnifiedRxListening() on disconnect (this is the ONLY place it should be called): +conn.on("disconnected", () => { + // ... cleanup ... + + stopUnifiedRxListening(); // Stop unified listener on disconnect + debugLog("[BLE] Unified RX listener stopped on disconnect"); + + // DO NOT clear logs on disconnect (preserve for user review) + // Logs are only cleared on connect + + // ... rest of cleanup ... +}); + +#### 4. Make startUnifiedRxListening() Idempotent + +**File**: `content/wardrive.js` + +UPDATE startUnifiedRxListening() to be safe to call multiple times: + +function startUnifiedRxListening() { + // Idempotent: safe to call multiple times + if (state.rxTracking.isListening && state.rxTracking.rxLogHandler) { + debugLog("[UNIFIED RX] Already listening, skipping start"); + return; + } + + if (!state.connection) { + debugWarn("[UNIFIED RX] Cannot start: no connection"); + return; + } + + debugLog("[UNIFIED RX] Starting unified RX listening"); + + const handler = (data) => handleUnifiedRxLogEvent(data); + state.rxTracking.rxLogHandler = handler; + state.connection.on(Constants.PushCodes.LogRxData, handler); + state.rxTracking.isListening = true; + + debugLog("[UNIFIED RX] ✅ Unified listening started successfully"); +} + +#### 5. Add Defensive Check in Unified Handler + +**File**: `content/wardrive.js` + +UPDATE handleUnifiedRxLogEvent() with defensive check: + +async function handleUnifiedRxLogEvent(data) { + try { + // Defensive check: ensure listener is marked as active + if (!state.rxTracking.isListening) { + debugWarn("[UNIFIED RX] Received event but listener marked inactive - reactivating"); + state.rxTracking.isListening = true; + } + + // Parse metadata ONCE (Task 1) + const metadata = parseRxPacketMetadata(data); + + // Route to TX tracking if active (during 7s echo window) + if (state.txTracking.isListening) { + debugLog("[UNIFIED RX] TX tracking active - checking for echo"); + const wasEcho = await handleTxLogging(metadata, data); + if (wasEcho) { + debugLog("[UNIFIED RX] Packet was TX echo, done"); + return; + } + } + + // Route to RX wardriving if active (when TX/RX Auto OR RX Auto enabled) + if (state.rxTracking.isWardriving) { + debugLog("[UNIFIED RX] RX wardriving active - logging observation"); + await handleRxLogging(metadata, data); + } + + // If neither active, packet is received but ignored + // Listener stays on, just not processing for wardriving + + } catch (error) { + debugError("[UNIFIED RX] Error processing rx_log entry", error); + } +} + +#### 6. Update TX/RX Auto Functions + +**File**: `content/wardrive.js` + +UPDATE startAutoPing() function (will be renamed to startTxRxAuto): + +function startAutoPing() { // Function name will stay as is, but references updated + debugLog("[TX/RX AUTO] Starting TX/RX Auto mode"); + + if (!state.connection) { + debugError("[TX/RX AUTO] Cannot start - not connected"); + alert("Connect to a MeshCore device first."); + return; + } + + // Check cooldown + if (isInCooldown()) { + const remainingSec = getRemainingCooldownSeconds(); + debugLog(`[TX/RX AUTO] Start blocked by cooldown (${remainingSec}s remaining)`); + setDynamicStatus(`Wait ${remainingSec}s before toggling TX/RX Auto`, STATUS_COLORS.warning); + return; + } + + // Defensive check: ensure unified listener is running + if (state.connection && !state.rxTracking.isListening) { + debugWarn("[TX/RX AUTO] Unified listener not active - restarting"); + startUnifiedRxListening(); + } + + // Clear any existing auto timer + if (state.autoTimerId) { + debugLog("[TX/RX AUTO] Clearing existing auto timer"); + clearTimeout(state.autoTimerId); + state.autoTimerId = null; + } + stopAutoCountdown(); + + // Clear any previous skip reason + state.skipReason = null; + + // ENABLE RX wardriving + state.rxTracking.isWardriving = true; + debugLog("[TX/RX AUTO] RX wardriving enabled"); + + // Start GPS watch for continuous updates + debugLog("[TX/RX AUTO] Starting GPS watch"); + startGeoWatch(); + + // Set TX/RX Auto mode flag + state.txRxAutoRunning = true; // Renamed from state.running + updateAutoButton(); + updateControlsForCooldown(); // Disable RX Auto button + + // Acquire wake lock + debugLog("[TX/RX AUTO] Acquiring wake lock"); + acquireWakeLock().catch(console.error); + + // Send first ping + debugLog("[TX/RX AUTO] Sending initial auto ping"); + sendPing(false).catch(console.error); +} + +UPDATE stopAutoPing() function: + +function stopAutoPing(stopGps = false) { + debugLog(`[TX/RX AUTO] Stopping TX/RX Auto mode (stopGps=${stopGps})`); + + // Check cooldown (unless stopGps is true for disconnect) + if (!stopGps && isInCooldown()) { + const remainingSec = getRemainingCooldownSeconds(); + debugLog(`[TX/RX AUTO] Stop blocked by cooldown (${remainingSec}s remaining)`); + setDynamicStatus(`Wait ${remainingSec}s before toggling TX/RX Auto`, STATUS_COLORS.warning); + return; + } + + // Clear auto timer + if (state.autoTimerId) { + debugLog("[TX/RX AUTO] Clearing auto timer"); + clearTimeout(state.autoTimerId); + state.autoTimerId = null; + } + stopAutoCountdown(); + + // Clear skip reason and paused timer state + state.skipReason = null; + state.pausedAutoTimerRemainingMs = null; + + // DISABLE RX wardriving + state.rxTracking.isWardriving = false; + debugLog("[TX/RX AUTO] RX wardriving disabled"); + + // DO NOT stop unified listener (stays on) + // REMOVED: stopUnifiedRxListening(); + + // Stop GPS watch if requested + if (stopGps) { + stopGeoWatch(); + } + + // Clear TX/RX Auto mode flag + state.txRxAutoRunning = false; // Renamed from state.running + updateAutoButton(); + updateControlsForCooldown(); // Re-enable RX Auto button + releaseWakeLock(); + + debugLog("[TX/RX AUTO] TX/RX Auto mode stopped"); +} + +#### 7. Add RX Auto Mode Functions + +**File**: `content/wardrive.js` + +ADD new startRxAuto() function: + +function startRxAuto() { + debugLog("[RX AUTO] Starting RX Auto mode"); + + if (!state.connection) { + debugError("[RX AUTO] Cannot start - not connected"); + alert("Connect to a MeshCore device first."); + return; + } + + // Defensive check: ensure unified listener is running + if (state.connection && !state.rxTracking.isListening) { + debugWarn("[RX AUTO] Unified listener not active - restarting"); + startUnifiedRxListening(); + } + + // ENABLE RX wardriving + state.rxTracking.isWardriving = true; + debugLog("[RX AUTO] RX wardriving enabled"); + + // Set RX Auto mode flag + state.rxAutoRunning = true; + updateAutoButton(); + updateControlsForCooldown(); // Disable TX/RX Auto button + + // Acquire wake lock + debugLog("[RX AUTO] Acquiring wake lock"); + acquireWakeLock().catch(console.error); + + setDynamicStatus("RX Auto started", STATUS_COLORS.success); + debugLog("[RX AUTO] RX Auto mode started successfully"); +} + +ADD new stopRxAuto() function: + +function stopRxAuto() { + debugLog("[RX AUTO] Stopping RX Auto mode"); + + if (!state.rxAutoRunning) { + debugLog("[RX AUTO] RX Auto not running, nothing to stop"); + return; + } + + // DISABLE RX wardriving + state.rxTracking.isWardriving = false; + debugLog("[RX AUTO] RX wardriving disabled"); + + // DO NOT stop unified listener (stays on) + // REMOVED: stopUnifiedRxListening(); + + // Clear RX Auto mode flag + state.rxAutoRunning = false; + updateAutoButton(); + updateControlsForCooldown(); // Re-enable TX/RX Auto button + releaseWakeLock(); + + setDynamicStatus("RX Auto stopped", STATUS_COLORS.idle); + debugLog("[RX AUTO] RX Auto mode stopped"); +} + +#### 8. Update Button Control Logic + +**File**: `content/wardrive.js` + +UPDATE updateControlsForCooldown() function: + +function updateControlsForCooldown() { + const connected = !!state.connection; + const inCooldown = isInCooldown(); + + debugLog(`[UI] updateControlsForCooldown: connected=${connected}, inCooldown=${inCooldown}, pingInProgress=${state.pingInProgress}, txRxAutoRunning=${state.txRxAutoRunning}, rxAutoRunning=${state.rxAutoRunning}`); + + // TX Ping button - disabled during cooldown or ping in progress + txPingBtn.disabled = ! connected || inCooldown || state.pingInProgress; + + // TX/RX Auto button - disabled during cooldown, ping in progress, OR when RX Auto running + txRxAutoBtn. disabled = !connected || inCooldown || state.pingInProgress || state.rxAutoRunning; + + // RX Auto button - disabled when TX/RX Auto running (no cooldown restriction for RX-only mode) + rxAutoBtn.disabled = !connected || state.txRxAutoRunning; +} + +UPDATE updateAutoButton() function: + +function updateAutoButton() { + // Update TX/RX Auto button + if (state.txRxAutoRunning) { // Renamed from state.running + txRxAutoBtn.textContent = "Stop TX/RX"; + txRxAutoBtn. classList.remove("bg-indigo-600", "hover:bg-indigo-500"); + txRxAutoBtn.classList.add("bg-amber-600", "hover:bg-amber-500"); + } else { + txRxAutoBtn.textContent = "TX/RX Auto"; + txRxAutoBtn.classList.add("bg-indigo-600", "hover:bg-indigo-500"); + txRxAutoBtn.classList.remove("bg-amber-600", "hover:bg-amber-500"); + } + + // Update RX Auto button + if (state. rxAutoRunning) { + rxAutoBtn.textContent = "Stop RX"; + rxAutoBtn.classList. remove("bg-indigo-600", "hover:bg-indigo-500"); + rxAutoBtn.classList.add("bg-amber-600", "hover:bg-amber-500"); + } else { + rxAutoBtn.textContent = "RX Auto"; + rxAutoBtn.classList.add("bg-indigo-600", "hover:bg-indigo-500"); + rxAutoBtn.classList.remove("bg-amber-600", "hover:bg-amber-500"); + } +} + +#### 9. Update Page Visibility Handler + +**File**: `content/wardrive.js` + +UPDATE page visibility event listener: + +document.addEventListener("visibilitychange", async () => { + if (document.hidden) { + debugLog("[UI] Page visibility changed to hidden"); + + // Stop TX/RX Auto if running + if (state.txRxAutoRunning) { + debugLog("[UI] Stopping TX/RX Auto due to page hidden"); + stopAutoPing(true); // Ignore cooldown, stop GPS + setDynamicStatus("Lost focus, TX/RX Auto stopped", STATUS_COLORS.warning); + } + + // Stop RX Auto if running + if (state.rxAutoRunning) { + debugLog("[UI] Stopping RX Auto due to page hidden"); + stopRxAuto(); + setDynamicStatus("Lost focus, RX Auto stopped", STATUS_COLORS.warning); + } + + // Release wake lock if neither mode running + if (!state.txRxAutoRunning && !state. rxAutoRunning) { + debugLog("[UI] Releasing wake lock due to page hidden"); + releaseWakeLock(); + } + + // DO NOT stop unified listener + + } else { + debugLog("[UI] Page visibility changed to visible"); + + // Defensive check: ensure unified listener is running if connected + if (state.connection && !state.rxTracking.isListening) { + debugWarn("[UI] Page visible but unified listener inactive - restarting"); + startUnifiedRxListening(); + } + + // User must manually restart auto modes + } +}); + +#### 10. Update Disconnect Handler + +**File**: `content/wardrive.js` + +UPDATE disconnected event handler: + +conn.on("disconnected", () => { + debugLog("[BLE] BLE disconnected event fired"); + debugLog(`[BLE] Disconnect reason: ${state.disconnectReason}`); + + // ... set connection/dynamic status ... + + setConnectButton(false); + deviceInfoEl.textContent = "—"; + state.connection = null; + state.channel = null; + state.devicePublicKey = null; + state.wardriveSessionId = null; + state. disconnectReason = null; + state.channelSetupErrorMessage = null; + state.bleDisconnectErrorMessage = null; + + // Stop auto modes + stopAutoPing(true); // Ignore cooldown, stop GPS + stopRxAuto(); // Stop RX Auto + + enableControls(false); + updateAutoButton(); + stopGeoWatch(); + stopGpsAgeUpdater(); + stopTxTracking(); // Renamed from stopRepeaterTracking + + // Stop unified RX listening on disconnect + stopUnifiedRxListening(); + debugLog("[BLE] Unified RX listener stopped on disconnect"); + + // Flush all pending RX batch data + flushAllRxBatches('disconnect'); // Renamed from flushAllBatches + + // Clear API queue + apiQueue. messages = []; + debugLog("[API QUEUE] Queue cleared on disconnect"); + + // Clean up all timers + cleanupAllTimers(); + + // DO NOT clear logs on disconnect (preserve for user review) + // Logs are only cleared on connect + + state.lastFix = null; + state.lastSuccessfulPingLocation = null; + state.gpsState = "idle"; + updateGpsUi(); + updateDistanceUi(); + + debugLog("[BLE] Disconnect cleanup complete"); +}); + +#### 11. Add RX Auto Button to HTML + +**File**: `index.html` + +UPDATE ping controls section: + +
+ + + +
+ +#### 12. Add RX Auto Button Event Listener + +**File**: `content/wardrive.js` (in onLoad function) + +ADD event listener for RX Auto button: + +export async function onLoad() { + // ... existing initialization ... + + // Existing button listeners + connectBtn.addEventListener("click", async () => { /* ... */ }); + txPingBtn.addEventListener("click", () => { /* ... */ }); + txRxAutoBtn.addEventListener("click", () => { /* ... */ }); + + // NEW: RX Auto button listener + rxAutoBtn.addEventListener("click", () => { + debugLog("[UI] RX Auto button clicked"); + if (state.rxAutoRunning) { + stopRxAuto(); + } else { + startRxAuto(); + } + }); + + // ... rest of initialization ... +} + +#### 13. Add RX Auto Debug Tag to Documentation + +**File**: `docs/DEVELOPMENT_REQUIREMENTS.md` + +ADD to debug tag table: + +| Tag | Description | +|-----|-------------| +| `[TX/RX AUTO]` | TX/RX Auto mode operations | +| `[RX AUTO]` | RX Auto mode operations | + +#### 14. Update Status Messages Documentation + +**File**: `docs/STATUS_MESSAGES.md` + +ADD RX Auto status messages: + +##### RX Auto started +- **Message**: "RX Auto started" +- **Color**: Green (success) +- **When**: User clicks "RX Auto" button to start passive RX-only listening +- **Source**: `content/wardrive.js:startRxAuto()` + +##### RX Auto stopped +- **Message**: "RX Auto stopped" +- **Color**: Slate (idle) +- **When**: User clicks "Stop RX" button +- **Source**: `content/wardrive.js:stopRxAuto()` + +##### Lost focus, RX Auto stopped +- **Message**: "Lost focus, RX Auto stopped" +- **Color**: Amber (warning) +- **When**: Browser tab hidden while RX Auto mode running +- **Source**: `content/wardrive.js:visibilitychange handler` + +UPDATE existing status messages: +- "Lost focus, auto mode stopped" → "Lost focus, TX/RX Auto stopped" +- "Auto mode stopped" → "TX/RX Auto stopped" + +#### 15. Update Workflow Documentation + +**File**: `docs/PING_WORKFLOW.md` + +ADD new section "RX Auto Mode Workflow": + +## RX Auto Mode Workflow + +### Overview +RX Auto mode provides passive-only wardriving without transmitting on the mesh network. It listens for all mesh traffic and logs received packets to the RX Log, which are then batched and posted to MeshMapper API. + +### RX Auto Start Sequence +1. User clicks "RX Auto" button +2. Verify BLE connection active +3. Defensive check: ensure unified listener running +4. Set `state.rxTracking.isWardriving = true` +5. Set `state.rxAutoRunning = true` +6. Update button to "Stop RX" (amber) +7. Disable TX/RX Auto button (mutual exclusivity) +8. Acquire wake lock +9. Show "RX Auto started" status (green) + +### RX Auto Stop Sequence +1. User clicks "Stop RX" button +2. Set `state.rxTracking.isWardriving = false` +3. Set `state.rxAutoRunning = false` +4. Update button to "RX Auto" (indigo) +5. Re-enable TX/RX Auto button +6. Release wake lock +7. Show "RX Auto stopped" status (idle) + +### RX Auto Characteristics +- **Zero mesh TX** (no network impact) +- **No GPS requirement** to start +- **No cooldown restrictions** +- **Mutually exclusive** with TX/RX Auto mode +- **Unified listener stays on** (does not stop when mode stops) + +### Behavior Comparison + +| Feature | TX Ping | TX/RX Auto | RX Auto | +|---------|---------|------------|---------| +| Transmits | Yes (once) | Yes (auto) | No | +| TX Echo Tracking | Yes (7s) | Yes (per ping) | No | +| RX Wardriving | No | Yes | Yes | +| Mesh Load | Low | High | None | +| Cooldown | Yes (7s) | Yes (7s) | No | +| GPS Required | Yes | Yes | No | +| Wake Lock | No | Yes | Yes | +| Unified Listener | Always on | Always on | Always on | +| TX Tracking Flag | True (7s) | True (per ping) | False | +| RX Wardriving Flag | False | True | True | + +### Validation Requirements +- Unified listener must start on connect and stay on entire connection +- Unified listener only stops on disconnect +- RX wardriving flag controls whether packets are logged +- TX/RX Auto and RX Auto are mutually exclusive +- All logs cleared on connect, preserved on disconnect +- Defensive checks ensure listener stays active +- startUnifiedRxListening() is idempotent (safe to call multiple times) + +--- + +## Development Guidelines Compliance + +### Debug Logging +- **ALWAYS** include debug logging for significant operations +- Use proper debug tags: + - `[RX PARSE]` for metadata parsing + - `[TX LOG]` for TX logging operations (renamed from [SESSION LOG]) + - `[RX LOG]` for RX logging operations (renamed from [PASSIVE RX]) + - `[TX/RX AUTO]` for TX/RX Auto mode (renamed from [AUTO]) + - `[RX AUTO]` for RX Auto mode (new) + - `[UNIFIED RX]` for unified listener operations +- Log at key points: function entry, state changes, routing decisions, errors + +### Status Messages +- Update `STATUS_MESSAGES.md` with all new status messages +- Use `setDynamicStatus()` for all UI status updates +- Use appropriate `STATUS_COLORS` constants + +### Documentation Updates +When modifying connection, disconnect, or ping logic: +- Read relevant workflow docs before making changes +- Update workflow docs to remain accurate after changes +- Document new modes, states, behaviors +- Update function references, state variables, button labels + +### Code Comments +- Document complex logic with inline comments +- Use JSDoc-style comments for new functions +- Update existing JSDoc when function signatures change +- Explain defensive checks and idempotent patterns + +--- + +## Testing Recommendations + +Since this is a browser-based PWA with no automated tests, perform thorough manual testing: + +### Connection Testing +- [ ] Connect to device - unified listener starts immediately +- [ ] Check debug log confirms listener started +- [ ] Verify all logs cleared on connect +- [ ] Disconnect - listener stops +- [ ] Reconnect - listener restarts + +### TX Ping Testing +- [ ] Single TX Ping works +- [ ] TX log shows echoes +- [ ] Debug data shows correct parsed_path +- [ ] RX wardriving stays OFF during TX Ping + +### TX/RX Auto Testing +- [ ] Start TX/RX Auto - both TX and RX wardriving active +- [ ] TX pings send automatically +- [ ] RX observations logged continuously +- [ ] RX Auto button disabled during TX/RX Auto +- [ ] Stop TX/RX Auto - both modes stop, listener stays on +- [ ] Unified listener still receiving events + +### RX Auto Testing +- [ ] Start RX Auto - only RX wardriving active +- [ ] No TX transmissions +- [ ] RX observations logged continuously +- [ ] TX/RX Auto button disabled during RX Auto +- [ ] Stop RX Auto - RX wardriving stops, listener stays on +- [ ] Unified listener still receiving events + +### Mutual Exclusivity Testing +- [ ] Cannot start TX/RX Auto when RX Auto running +- [ ] Cannot start RX Auto when TX/RX Auto running +- [ ] Buttons properly disabled/enabled + +### Edge Case Testing +- [ ] Switch browser tab away - modes stop, listener stays on +- [ ] Switch browser tab back - listener still active +- [ ] Disconnect during TX/RX Auto - clean shutdown +- [ ] Disconnect during RX Auto - clean shutdown +- [ ] Multiple connect/disconnect cycles - no memory leaks + +### Debug Mode Testing (with `? debug=true`) +- [ ] TX debug data shows correct parsed_path (actual raw bytes) +- [ ] RX debug data shows correct parsed_path (actual raw bytes) +- [ ] parsed_path matches repeaterId for TX (first hop) +- [ ] parsed_path matches repeaterId for RX (last hop) + +### Log Clearing Testing +- [ ] All logs clear on connect +- [ ] All logs preserved on disconnect +- [ ] User can review RX data after disconnecting + +--- + +## Summary + +This comprehensive refactor accomplishes three major improvements: + +1. **Unified RX Parsing**: Single parsing point eliminates duplication, improves performance, and fixes debug data accuracy +2. **Naming Standardization**: Consistent TX/RX terminology throughout codebase improves maintainability and clarity +3. **RX Auto Mode**: New passive-only wardriving mode with always-on unified listener architecture + +**Key architectural changes**: +- Unified RX listener always on when connected (never stops for mode changes) +- RX wardriving controlled by subscription flag (not listener lifecycle) +- Three distinct modes: TX Ping (manual), TX/RX Auto (active + passive), RX Auto (passive only) +- Defensive checks ensure listener stays active across edge cases +- Single metadata parsing eliminates duplication and inconsistency + +**User-facing improvements**: +- Clear TX/RX button labels +- New RX Auto mode for zero-impact wardriving +- Consistent log naming (TX Log, RX Log) +- Logs preserved on disconnect for review + +**Developer improvements**: +- Consistent naming conventions +- Single source of truth for packet parsing +- Idempotent functions prevent double-initialization +- Comprehensive debug logging +- Well-documented behavior \ No newline at end of file From 82faf15e862e18eea0f8247b016f8e29b0038a2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:11:44 +0000 Subject: [PATCH 04/27] Initial plan From 37d0afb97bcd3c843d863d5bf5f557107fafc631 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:18:51 +0000 Subject: [PATCH 05/27] Task 1: Implement unified RX parsing architecture - Add parseRxPacketMetadata() function to parse header/path once - Update buildDebugData() to use metadata.pathBytes for accurate debug data - Refactor handleSessionLogTracking() to use metadata instead of packet - Refactor handlePassiveRxLogging() to use metadata instead of packet - Update handleUnifiedRxLogEvent() to call parsing first before routing - Update repeater tracking storage to store full metadata - Update RX batching storage to store full metadata - Update TX/RX debug data integration in API posting functions Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/wardrive.js | 216 ++++++++++++++++++++++++-------------------- 1 file changed, 116 insertions(+), 100 deletions(-) diff --git a/content/wardrive.js b/content/wardrive.js index 807e2ec..1a1f913 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -1357,21 +1357,22 @@ function bytesToHex(bytes) { * @param {string} heardByte - The "heard" byte (first for TX, last for RX) as hex string * @returns {Object} Debug data object */ -function buildDebugData(rawPacketData, heardByte) { - const { raw, lastSnr, lastRssi, packet } = rawPacketData; - - // Convert path array to hex string (from parsed packet object) - const parsedPathHex = packet.path.map(byte => byte.toString(16).padStart(2, '0').toUpperCase()).join(''); +function buildDebugData(metadata, heardByte, repeaterId) { + // Convert path bytes to hex string - these are the ACTUAL bytes used + const parsedPathHex = Array.from(metadata.pathBytes) + .map(byte => byte.toString(16).padStart(2, '0').toUpperCase()) + .join(''); return { - raw_packet: bytesToHex(raw), // Send complete raw packet as hex string - raw_snr: lastSnr, - raw_rssi: lastRssi, - parsed_header: packet.header.toString(16).padStart(2, '0').toUpperCase(), - parsed_path_length: packet.path.length, - parsed_path: parsedPathHex, - parsed_payload: bytesToHex(packet.payload), - parsed_heard: heardByte + raw_packet: bytesToHex(metadata.raw), + raw_snr: metadata.snr, + raw_rssi: metadata.rssi, + parsed_header: metadata.header.toString(16).padStart(2, '0').toUpperCase(), + parsed_path_length: metadata.pathLength, + parsed_path: parsedPathHex, // ACTUAL raw bytes + parsed_payload: bytesToHex(metadata.encryptedPayload), + parsed_heard: heardByte, + repeaterId: repeaterId }; } @@ -1416,10 +1417,9 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { const debugDataArray = []; for (const repeater of state.tempTxRepeaterData) { - if (repeater.rawPacketData) { + if (repeater.metadata) { const heardByte = repeater.repeaterId; // First byte of path - const debugData = buildDebugData(repeater.rawPacketData, heardByte); - debugData.repeaterId = repeater.repeaterId; // Add repeater ID + const debugData = buildDebugData(repeater.metadata, heardByte, repeater.repeaterId); debugDataArray.push(debugData); debugLog(`[API QUEUE] 🐛 Added debug data for TX repeater: ${repeater.repeaterId}`); } @@ -1810,6 +1810,48 @@ async function computeChannelHash(channelSecret) { return hashArray[0]; } +/** + * Parse RX packet metadata from raw bytes + * Single source of truth for header/path extraction + * @param {Object} data - LogRxData event data (contains lastSnr, lastRssi, raw) + * @returns {Object} Parsed metadata object + */ +function parseRxPacketMetadata(data) { + debugLog(`[RX PARSE] Starting metadata parsing`); + + // Extract header byte from raw[0] + const header = data.raw[0]; + + // Extract path length from header upper 4 bits: (header >> 4) & 0x0F + const pathLength = (header >> 4) & 0x0F; + + // Extract raw path bytes as array: raw.slice(1, 1 + pathLength) + const pathBytes = Array.from(data.raw.slice(1, 1 + pathLength)); + + // Derive first hop (for TX repeater ID): pathBytes[0] + const firstHop = pathBytes.length > 0 ? pathBytes[0] : null; + + // Derive last hop (for RX repeater ID): pathBytes[pathLength - 1] + const lastHop = pathBytes.length > 0 ? pathBytes[pathLength - 1] : null; + + // Extract encrypted payload: raw.slice(1 + pathLength) + const encryptedPayload = data.raw.slice(1 + pathLength); + + debugLog(`[RX PARSE] Parsed metadata: header=0x${header.toString(16).padStart(2, '0')}, pathLength=${pathLength}, firstHop=${firstHop ? '0x' + firstHop.toString(16).padStart(2, '0') : 'null'}, lastHop=${lastHop ? '0x' + lastHop.toString(16).padStart(2, '0') : 'null'}`); + + return { + raw: data.raw, // Full raw packet bytes + header: header, // Header byte + pathLength: pathLength, // Number of hops + pathBytes: pathBytes, // Raw path bytes array + firstHop: firstHop, // First hop ID (TX) + lastHop: lastHop, // Last hop ID (RX) + snr: data.lastSnr, // SNR value + rssi: data.lastRssi, // RSSI value + encryptedPayload: encryptedPayload // Rest of packet + }; +} + /** * Decrypt GroupText payload and extract message text * Payload structure: [1 byte channel_hash][2 bytes MAC][encrypted data] @@ -1963,34 +2005,34 @@ function startRepeaterTracking(payload, channelIdx) { /** * Handle Session Log tracking for repeater echoes * Called by unified RX handler when tracking is active - * @param {Object} packet - Parsed packet from Packet.fromBytes + * @param {Object} metadata - Parsed metadata from parseRxPacketMetadata() * @param {Object} data - The LogRxData event data (contains lastSnr, lastRssi, raw) * @returns {boolean} True if packet was an echo and tracked, false otherwise */ -async function handleSessionLogTracking(packet, data) { +async function handleSessionLogTracking(metadata, data) { const originalPayload = state.repeaterTracking.sentPayload; const channelIdx = state.repeaterTracking.channelIdx; const expectedChannelHash = WARDRIVING_CHANNEL_HASH; try { - debugLog(`[SESSION LOG] Processing rx_log entry: SNR=${data.lastSnr}, RSSI=${data.lastRssi}`); + debugLog(`[SESSION LOG] Processing rx_log entry: SNR=${metadata.snr}, RSSI=${metadata.rssi}`); // VALIDATION STEP 1: Header validation for echo detection // Only GroupText packets (CHANNEL_GROUP_TEXT_HEADER) can be echoes of our channel messages - if (packet.header !== CHANNEL_GROUP_TEXT_HEADER) { - debugLog(`[SESSION LOG] Ignoring: header validation failed (header=0x${packet.header.toString(16).padStart(2, '0')})`); + if (metadata.header !== CHANNEL_GROUP_TEXT_HEADER) { + debugLog(`[SESSION LOG] Ignoring: header validation failed (header=0x${metadata.header.toString(16).padStart(2, '0')})`); return false; } - debugLog(`[SESSION LOG] Header validation passed: 0x${packet.header.toString(16).padStart(2, '0')}`); + debugLog(`[SESSION LOG] Header validation passed: 0x${metadata.header.toString(16).padStart(2, '0')}`); // VALIDATION STEP 2: Validate this message is for our channel by comparing channel hash // Channel message payload structure: [1 byte channel_hash][2 bytes MAC][encrypted message] - if (packet.payload.length < 3) { + if (metadata.encryptedPayload.length < 3) { debugLog(`[SESSION LOG] Ignoring: payload too short to contain channel hash`); return false; } - const packetChannelHash = packet.payload[0]; + const packetChannelHash = metadata.encryptedPayload[0]; debugLog(`[SESSION LOG] Message correlation check: packet_channel_hash=0x${packetChannelHash.toString(16).padStart(2, '0')}, expected=0x${expectedChannelHash.toString(16).padStart(2, '0')}`); if (packetChannelHash !== expectedChannelHash) { @@ -2006,7 +2048,7 @@ async function handleSessionLogTracking(packet, data) { if (WARDRIVING_CHANNEL_KEY) { debugLog(`[MESSAGE_CORRELATION] Channel key available, attempting decryption...`); - const decryptedMessage = await decryptGroupTextPayload(packet.payload, WARDRIVING_CHANNEL_KEY); + const decryptedMessage = await decryptGroupTextPayload(metadata.encryptedPayload, WARDRIVING_CHANNEL_KEY); if (decryptedMessage === null) { debugLog(`[MESSAGE_CORRELATION] ❌ REJECT: Failed to decrypt message`); @@ -2041,7 +2083,7 @@ async function handleSessionLogTracking(packet, data) { // VALIDATION STEP 4: Check path length (repeater echo vs direct transmission) // For channel messages, the path contains repeater hops // Each hop in the path is 1 byte (repeater ID) - if (packet.path.length === 0) { + if (metadata.pathLength === 0) { debugLog(`[SESSION LOG] Ignoring: no path (direct transmission, not a repeater echo)`); return false; } @@ -2050,49 +2092,39 @@ async function handleSessionLogTracking(packet, data) { // The path may contain multiple hops (e.g., [0x22, 0xd0, 0x5d, 0x46, 0x8b]) // but we only care about the first repeater that echoed our message // Example: path [0x22, 0xd0, 0x5d] becomes "22" (only first hop) - const firstHopId = packet.path[0]; + const firstHopId = metadata.firstHop; const pathHex = firstHopId.toString(16).padStart(2, '0'); - debugLog(`[PING] Repeater echo accepted: first_hop=${pathHex}, SNR=${data.lastSnr}, full_path_length=${packet.path.length}`); + debugLog(`[PING] Repeater echo accepted: first_hop=${pathHex}, SNR=${metadata.snr}, full_path_length=${metadata.pathLength}`); // Check if we already have this path if (state.repeaterTracking.repeaters.has(pathHex)) { const existing = state.repeaterTracking.repeaters.get(pathHex); - debugLog(`[PING] Deduplication: path ${pathHex} already seen (existing SNR=${existing.snr}, new SNR=${data.lastSnr})`); + debugLog(`[PING] Deduplication: path ${pathHex} already seen (existing SNR=${existing.snr}, new SNR=${metadata.snr})`); // Keep the best (highest) SNR - if (data.lastSnr > existing.snr) { - debugLog(`[PING] Deduplication decision: updating path ${pathHex} with better SNR: ${existing.snr} -> ${data.lastSnr}`); + if (metadata.snr > existing.snr) { + debugLog(`[PING] Deduplication decision: updating path ${pathHex} with better SNR: ${existing.snr} -> ${metadata.snr}`); state.repeaterTracking.repeaters.set(pathHex, { - snr: data.lastSnr, + snr: metadata.snr, seenCount: existing.seenCount + 1, - rawPacketData: { // Update with better SNR packet data - raw: data.raw, - lastSnr: data.lastSnr, - lastRssi: data.lastRssi, - packet: packet - } + metadata: metadata // Store full metadata for debug mode }); // Trigger incremental UI update since SNR changed updateCurrentLogEntryWithLiveRepeaters(); } else { - debugLog(`[PING] Deduplication decision: keeping existing SNR for path ${pathHex} (existing ${existing.snr} >= new ${data.lastSnr})`); + debugLog(`[PING] Deduplication decision: keeping existing SNR for path ${pathHex} (existing ${existing.snr} >= new ${metadata.snr})`); // Still increment seen count existing.seenCount++; } } else { // New path - debugLog(`[PING] Adding new repeater echo: path=${pathHex}, SNR=${data.lastSnr}`); + debugLog(`[PING] Adding new repeater echo: path=${pathHex}, SNR=${metadata.snr}`); state.repeaterTracking.repeaters.set(pathHex, { - snr: data.lastSnr, + snr: metadata.snr, seenCount: 1, - rawPacketData: { // Store for debug mode - raw: data.raw, - lastSnr: data.lastSnr, - lastRssi: data.lastRssi, - packet: packet - } + metadata: metadata // Store full metadata for debug mode }); // Trigger incremental UI update for the new repeater @@ -2123,11 +2155,11 @@ function stopRepeaterTracking() { // No need to unregister handler - unified handler continues running // Just clear the tracking state - // Get the results with full data (including rawPacketData for debug mode) + // Get the results with full data (including metadata for debug mode) const repeaters = Array.from(state.repeaterTracking.repeaters.entries()).map(([id, data]) => ({ repeaterId: id, snr: data.snr, - rawPacketData: data.rawPacketData // Include for debug mode + metadata: data.metadata // Include metadata for debug mode })); // Sort by repeater ID for deterministic output @@ -2170,44 +2202,38 @@ function formatRepeaterTelemetry(repeaters) { */ async function handleUnifiedRxLogEvent(data) { try { - debugLog(`[UNIFIED RX] Received rx_log entry: SNR=${data.lastSnr}, RSSI=${data.lastRssi}`); + // Parse metadata ONCE + const metadata = parseRxPacketMetadata(data); - // Parse the packet from raw data (once for both handlers) - const packet = Packet.fromBytes(data.raw); + debugLog(`[UNIFIED RX] Packet received: header=0x${metadata.header.toString(16)}, pathLength=${metadata.pathLength}`); - // Log header for debugging (informational for all packet processing) - debugLog(`[UNIFIED RX] Packet header: 0x${packet.header.toString(16).padStart(2, '0')}`); - - // DELEGATION: If Session Log is actively tracking, delegate to it first - // Session Log requires header validation (CHANNEL_GROUP_TEXT_HEADER) and will handle validation internally + // Route to TX tracking if active if (state.repeaterTracking.isListening) { - debugLog(`[UNIFIED RX] Session Log is tracking - delegating to Session Log handler`); - const wasTracked = await handleSessionLogTracking(packet, data); - - if (wasTracked) { - debugLog(`[UNIFIED RX] Packet was an echo and tracked by Session Log`); - return; // Echo handled, done + debugLog("[UNIFIED RX] TX tracking active - delegating to TX handler"); + const wasEcho = await handleSessionLogTracking(metadata, data); + if (wasEcho) { + debugLog("[UNIFIED RX] Packet was TX echo, done"); + return; } - - debugLog(`[UNIFIED RX] Packet was not an echo, continuing to Passive RX processing`); } - // DELEGATION: Handle passive RX logging for all other cases - // Passive RX accepts any packet regardless of header type - await handlePassiveRxLogging(packet, data); - + // Route to RX wardriving if active + if (state.passiveRxTracking.isListening) { + debugLog("[UNIFIED RX] RX wardriving active - delegating to RX handler"); + await handlePassiveRxLogging(metadata, data); + } } catch (error) { - debugError(`[UNIFIED RX] Error processing rx_log entry: ${error.message}`, error); + debugError("[UNIFIED RX] Error processing rx_log entry", error); } } /** * Handle passive RX logging - monitors all incoming packets not handled by Session Log * Extracts the LAST hop from the path (direct repeater) and records observation - * @param {Object} packet - Parsed packet from Packet.fromBytes + * @param {Object} metadata - Parsed metadata from parseRxPacketMetadata() * @param {Object} data - The LogRxData event data (contains lastSnr, lastRssi, raw) */ -async function handlePassiveRxLogging(packet, data) { +async function handlePassiveRxLogging(metadata, data) { try { debugLog(`[PASSIVE RX] Processing packet for passive logging`); @@ -2215,16 +2241,16 @@ async function handlePassiveRxLogging(packet, data) { // A packet's path array contains the sequence of repeater IDs that forwarded the message. // Packets with no path are direct transmissions (node-to-node) and don't provide // information about repeater coverage, so we skip them for RX wardriving purposes. - if (packet.path.length === 0) { + if (metadata.pathLength === 0) { debugLog(`[PASSIVE RX] Ignoring: no path (direct transmission, not via repeater)`); return; } // Extract LAST hop from path (the repeater that directly delivered to us) - const lastHopId = packet.path[packet.path.length - 1]; + const lastHopId = metadata.lastHop; const repeaterId = lastHopId.toString(16).padStart(2, '0'); - debugLog(`[PASSIVE RX] Packet heard via last hop: ${repeaterId}, SNR=${data.lastSnr}, path_length=${packet.path.length}`); + debugLog(`[PASSIVE RX] Packet heard via last hop: ${repeaterId}, SNR=${metadata.snr}, path_length=${metadata.pathLength}`); // Get current GPS location if (!state.lastFix) { @@ -2237,27 +2263,19 @@ async function handlePassiveRxLogging(packet, data) { const timestamp = new Date().toISOString(); // Add entry to RX log (including RSSI, path length, and header for CSV export) - addRxLogEntry(repeaterId, data.lastSnr, data.lastRssi, packet.path.length, packet.header, lat, lon, timestamp); + addRxLogEntry(repeaterId, metadata.snr, metadata.rssi, metadata.pathLength, metadata.header, lat, lon, timestamp); - debugLog(`[PASSIVE RX] ✅ Observation logged: repeater=${repeaterId}, snr=${data.lastSnr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); - - // Store raw packet data for API handling - const rawPacketData = { - raw: data.raw, - lastSnr: data.lastSnr, - lastRssi: data.lastRssi, - packet: packet - }; + debugLog(`[PASSIVE RX] ✅ Observation logged: repeater=${repeaterId}, snr=${metadata.snr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); // Handle tracking for API (best SNR with distance trigger) handlePassiveRxForAPI( repeaterId, - data.lastSnr, - data.lastRssi, - packet.path.length, - packet.header, + metadata.snr, + metadata.rssi, + metadata.pathLength, + metadata.header, { lat, lon }, - rawPacketData + metadata ); } catch (error) { @@ -2342,9 +2360,9 @@ async function postRxLogToMeshMapperAPI(entries) { * @param {number} pathLength - Number of hops in the path * @param {number} header - Packet header byte * @param {Object} currentLocation - Current GPS location {lat, lon} - * @param {Object} rawPacketData - Raw packet data for debug mode + * @param {Object} metadata - Parsed metadata for debug mode */ -function handlePassiveRxForAPI(repeaterId, snr, rssi, pathLength, header, currentLocation, rawPacketData) { +function handlePassiveRxForAPI(repeaterId, snr, rssi, pathLength, header, currentLocation, metadata) { // Get or create buffer entry for this repeater let buffer = state.rxBatchBuffer.get(repeaterId); @@ -2360,7 +2378,7 @@ function handlePassiveRxForAPI(repeaterId, snr, rssi, pathLength, header, curren lat: currentLocation.lat, lon: currentLocation.lon, timestamp: Date.now(), - rawPacketData + metadata: metadata // Store full metadata for debug mode } }; state.rxBatchBuffer.set(repeaterId, buffer); @@ -2377,7 +2395,7 @@ function handlePassiveRxForAPI(repeaterId, snr, rssi, pathLength, header, curren lat: currentLocation.lat, lon: currentLocation.lon, timestamp: Date.now(), - rawPacketData + metadata: metadata // Store full metadata for debug mode }; } else { debugLog(`[RX BATCH] Ignoring worse SNR for repeater ${repeaterId}: current=${buffer.bestObservation.snr}, new=${snr}`); @@ -2424,7 +2442,7 @@ function flushRepeater(repeaterId) { pathLength: best.pathLength, header: best.header, timestamp: best.timestamp, - rawPacketData: best.rawPacketData // For future debug mode + metadata: best.metadata // For debug mode }; debugLog(`[RX BATCH] Posting repeater ${repeaterId}: snr=${best.snr}, location=${best.lat.toFixed(5)},${best.lon.toFixed(5)}`); @@ -2488,16 +2506,14 @@ function queueApiPost(entry) { }; // Add debug data if debug mode is enabled - if (state.debugMode && entry.rawPacketData) { + if (state.debugMode && entry.metadata) { debugLog(`[RX BATCH API] 🐛 Debug mode active - adding debug_data for RX`); - const packet = entry.rawPacketData.packet; - // For RX, parsed_heard is the LAST byte of path - const lastHopId = packet.path[packet.path.length - 1]; + const lastHopId = entry.metadata.lastHop; const heardByte = lastHopId.toString(16).padStart(2, '0').toUpperCase(); - const debugData = buildDebugData(entry.rawPacketData, heardByte); + const debugData = buildDebugData(entry.metadata, heardByte, entry.repeater_id); payload.debug_data = debugData; debugLog(`[RX BATCH API] 🐛 RX payload includes debug_data for repeater ${entry.repeater_id}`); From 8729388e73c94e27ad8a105828b0bebbed6c1bbd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:23:32 +0000 Subject: [PATCH 06/27] Task 2: Complete naming standardization to TX/RX terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename state.repeaterTracking → state.txTracking - Rename state.passiveRxTracking → state.rxTracking - Rename state.running → state.txRxAutoRunning - Rename sessionLogState → txLogState - Rename all handler functions (handleSessionLogTracking → handleTxLogging, etc.) - Rename all UI functions (addLogEntry → addTxLogEntry, etc.) - Rename all batch functions (flushBatch → flushRxBatch, etc.) - Rename DOM element references (sendPingBtn → txPingBtn, autoToggleBtn → txRxAutoBtn, etc.) - Update HTML element IDs and button labels (Session Log → TX Log, Send Ping → TX Ping, etc.) - Update debug log tags ([SESSION LOG] → [TX LOG], [PASSIVE RX] → [RX LOG], [AUTO] → [TX/RX AUTO]) - Update CSS comments (Session Log → TX Log) - Update copy to clipboard function tags Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com> --- content/style.css | 6 +- content/wardrive.js | 423 ++++++++++++++++++++++---------------------- index.html | 32 ++-- 3 files changed, 230 insertions(+), 231 deletions(-) diff --git a/content/style.css b/content/style.css index 882c4be..d0209fd 100644 --- a/content/style.css +++ b/content/style.css @@ -179,12 +179,12 @@ body, white-space: nowrap; } -/* Session Log - Static Expandable Section */ -#logBottomSheet.open { +/* TX Log - Static Expandable Section */ +#txLogBottomSheet.open { display: block !important; } -#logExpandArrow.expanded { +#txLogExpandArrow.expanded { transform: rotate(180deg); } diff --git a/content/wardrive.js b/content/wardrive.js index 1a1f913..5abe28e 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -141,13 +141,13 @@ const statusEl = $("status"); const deviceInfoEl = $("deviceInfo"); const channelInfoEl = $("channelInfo"); const connectBtn = $("connectBtn"); -const sendPingBtn = $("sendPingBtn"); -const autoToggleBtn = $("autoToggleBtn"); +const txPingBtn = $("txPingBtn"); +const txRxAutoBtn = $("txRxAutoBtn"); const lastPingEl = $("lastPing"); const gpsInfoEl = document.getElementById("gpsInfo"); const gpsAccEl = document.getElementById("gpsAcc"); const distanceInfoEl = document.getElementById("distanceInfo"); // Distance from last ping -const sessionPingsEl = document.getElementById("sessionPings"); // optional +const sessionPingsEl = document.getElementById("sessionPings"); // optional - TODO: rename to txPings const coverageFrameEl = document.getElementById("coverageFrame"); setConnectButton(false); setConnStatus("Disconnected", STATUS_COLORS.error); @@ -156,14 +156,14 @@ setConnStatus("Disconnected", STATUS_COLORS.error); const intervalSelect = $("intervalSelect"); // 15 / 30 / 60 seconds const powerSelect = $("powerSelect"); // "", "0.3w", "0.6w", "1.0w" -// Session Log selectors -const logSummaryBar = $("logSummaryBar"); -const logBottomSheet = $("logBottomSheet"); -const logScrollContainer = $("logScrollContainer"); -const logCount = $("logCount"); -const logLastTime = $("logLastTime"); -const logLastSnr = $("logLastSnr"); -const sessionLogCopyBtn = $("sessionLogCopyBtn"); +// TX Log selectors +const txLogSummaryBar = $("txLogSummaryBar"); +const txLogBottomSheet = $("txLogBottomSheet"); +const txLogScrollContainer = $("txLogScrollContainer"); +const txLogCount = $("txLogCount"); +const txLogLastTime = $("txLogLastTime"); +const txLogLastSnr = $("txLogLastSnr"); +const txLogCopyBtn = $("txLogCopyBtn"); // RX Log selectors const rxLogSummaryBar = $("rxLogSummaryBar"); @@ -189,7 +189,7 @@ const errorLogExpandArrow = $("errorLogExpandArrow"); const errorLogCopyBtn = $("errorLogCopyBtn"); // Session log state -const sessionLogState = { +const txLogState = { entries: [], // Array of parsed log entries isExpanded: false, autoScroll: true @@ -217,7 +217,7 @@ const state = { connection: null, channel: null, autoTimerId: null, - running: false, + txRxAutoRunning: false, // TX/RX Auto mode flag (renamed from running) wakeLock: null, geoWatchId: null, lastFix: null, // { lat, lon, accM, tsMs } @@ -241,20 +241,19 @@ const state = { disconnectReason: null, // Tracks the reason for disconnection (e.g., "app_down", "capacity_full", "public_key_error", "channel_setup_error", "ble_disconnect_error", "session_id_error", "normal", or API reason codes like "outofdate") channelSetupErrorMessage: null, // Error message from channel setup failure bleDisconnectErrorMessage: null, // Error message from BLE disconnect failure - repeaterTracking: { - isListening: false, // Whether we're currently listening for echoes + txTracking: { + isListening: false, // Whether we're currently listening for TX echoes sentTimestamp: null, // Timestamp when the ping was sent sentPayload: null, // The payload text that was sent channelIdx: null, // Channel index for reference - repeaters: new Map(), // Map + repeaters: new Map(), // Map listenTimeout: null, // Timeout handle for 7-second window rxLogHandler: null, // Handler function for rx_log events currentLogEntry: null, // Current log entry being updated (for incremental UI updates) }, - passiveRxTracking: { - isListening: false, // Whether we're currently listening passively - rxLogHandler: null, // Handler function for passive rx_log events - entries: [] // Array of { repeaterId, snr, lat, lon, timestamp } + rxTracking: { + isListening: false, // Whether we're currently listening to unified RX + rxLogHandler: null, // Handler function for RX log events }, rxBatchBuffer: new Map() // Map }; @@ -412,7 +411,7 @@ function createCountdownTimer(getEndTime, getStatusMessage) { const autoCountdownTimer = createCountdownTimer( () => state.nextAutoPingTime, (remainingSec) => { - if (!state.running) return null; + if (!state.txRxAutoRunning) return null; if (remainingSec === 0) { return { message: "Sending auto ping", color: STATUS_COLORS.info }; } @@ -514,11 +513,11 @@ function resumeAutoCountdown() { * @returns {void} */ function handleManualPingBlockedDuringAutoMode() { - if (state.running) { - debugLog("[AUTO] Manual ping blocked during auto mode - resuming auto countdown"); + if (state.txRxAutoRunning) { + debugLog("[TX/RX AUTO] Manual ping blocked during auto mode - resuming auto countdown"); const resumed = resumeAutoCountdown(); if (!resumed) { - debugLog("[AUTO] No paused countdown to resume, scheduling new auto ping"); + debugLog("[TX/RX AUTO] No paused countdown to resume, scheduling new auto ping"); scheduleNextAutoPing(); } } @@ -564,8 +563,8 @@ function updateControlsForCooldown() { const connected = !!state.connection; const inCooldown = isInCooldown(); debugLog(`[UI] updateControlsForCooldown: connected=${connected}, inCooldown=${inCooldown}, pingInProgress=${state.pingInProgress}`); - sendPingBtn.disabled = !connected || inCooldown || state.pingInProgress; - autoToggleBtn.disabled = !connected || inCooldown || state.pingInProgress; + txPingBtn.disabled = !connected || inCooldown || state.pingInProgress; + txRxAutoBtn.disabled = !connected || inCooldown || state.pingInProgress; } /** @@ -641,14 +640,14 @@ function enableControls(connected) { // No need to show/hide the controls anymore } function updateAutoButton() { - if (state.running) { - autoToggleBtn.textContent = "Stop Auto Ping"; - autoToggleBtn.classList.remove("bg-indigo-600","hover:bg-indigo-500"); - autoToggleBtn.classList.add("bg-amber-600","hover:bg-amber-500"); + if (state.txRxAutoRunning) { + txRxAutoBtn.textContent = "Stop Auto Ping"; + txRxAutoBtn.classList.remove("bg-indigo-600","hover:bg-indigo-500"); + txRxAutoBtn.classList.add("bg-amber-600","hover:bg-amber-500"); } else { - autoToggleBtn.textContent = "Start Auto Ping"; - autoToggleBtn.classList.add("bg-indigo-600","hover:bg-indigo-500"); - autoToggleBtn.classList.remove("bg-amber-600","hover:bg-amber-500"); + txRxAutoBtn.textContent = "Start Auto Ping"; + txRxAutoBtn.classList.add("bg-indigo-600","hover:bg-indigo-500"); + txRxAutoBtn.classList.remove("bg-amber-600","hover:bg-amber-500"); } } function buildCoverageEmbedUrl(lat, lon) { @@ -1353,7 +1352,7 @@ function bytesToHex(bytes) { /** * Build debug data object for a single packet observation - * @param {Object} rawPacketData - Raw packet data from handleSessionLogTracking or handlePassiveRxLogging + * @param {Object} rawPacketData - Raw packet data from handleTxLogging or handleRxLogging * @param {string} heardByte - The "heard" byte (first for TX, last for RX) as hex string * @returns {Object} Debug data object */ @@ -1567,18 +1566,18 @@ async function postApiAndRefreshMap(lat, lon, accuracy, heardRepeats) { // Update status based on current mode if (state.connection) { - if (state.running) { + if (state.txRxAutoRunning) { // Check if we should resume a paused auto countdown (manual ping during auto mode) const resumed = resumeAutoCountdown(); if (!resumed) { // No paused timer to resume, schedule new auto ping (this was an auto ping) - debugLog("[AUTO] Scheduling next auto ping"); + debugLog("[TX/RX AUTO] Scheduling next auto ping"); scheduleNextAutoPing(); } else { - debugLog("[AUTO] Resumed auto countdown after manual ping"); + debugLog("[TX/RX AUTO] Resumed auto countdown after manual ping"); } } else { - debugLog("[AUTO] Setting dynamic status to show queue size"); + debugLog("[TX/RX AUTO] Setting dynamic status to show queue size"); // Status already set by queueApiMessage() } } @@ -1770,7 +1769,7 @@ async function flushApiQueue() { } else { debugLog(`[API QUEUE] Batch post successful: ${txCount} TX, ${rxCount} RX`); // Clear status after successful post - if (state.connection && !state.running) { + if (state.connection && !state.txRxAutoRunning) { setDynamicStatus("Idle"); } } @@ -1973,7 +1972,7 @@ async function decryptGroupTextPayload(payload, channelKey) { * @param {string} payload - The ping payload that was sent * @param {number} channelIdx - The channel index where the ping was sent */ -function startRepeaterTracking(payload, channelIdx) { +function startTxTracking(payload, channelIdx) { debugLog(`[PING] Starting repeater echo tracking for ping: "${payload}" on channel ${channelIdx}`); debugLog(`[PING] 7-second rx_log listening window opened at ${new Date().toISOString()}`); @@ -1984,21 +1983,21 @@ function startRepeaterTracking(payload, channelIdx) { } // Clear any existing tracking state - stopRepeaterTracking(); + stopTxTracking(); debugLog(`[PING] Using pre-computed channel hash for correlation: 0x${WARDRIVING_CHANNEL_HASH.toString(16).padStart(2, '0')}`); // Initialize tracking state - state.repeaterTracking.isListening = true; - state.repeaterTracking.sentTimestamp = Date.now(); - state.repeaterTracking.sentPayload = payload; - state.repeaterTracking.channelIdx = channelIdx; - state.repeaterTracking.repeaters.clear(); + state.txTracking.isListening = true; + state.txTracking.sentTimestamp = Date.now(); + state.txTracking.sentPayload = payload; + state.txTracking.channelIdx = channelIdx; + state.txTracking.repeaters.clear(); - debugLog(`[SESSION LOG] Session Log tracking activated - unified handler will delegate echoes to Session Log`); + debugLog(`[TX LOG] Session Log tracking activated - unified handler will delegate echoes to Session Log`); // Note: The unified RX handler (started at connect) will automatically delegate to - // handleSessionLogTracking() when isListening = true. No separate handler needed. + // handleTxLogging() when isListening = true. No separate handler needed. // The 7-second timeout to stop listening is managed by the caller (sendPing function) } @@ -2009,38 +2008,38 @@ function startRepeaterTracking(payload, channelIdx) { * @param {Object} data - The LogRxData event data (contains lastSnr, lastRssi, raw) * @returns {boolean} True if packet was an echo and tracked, false otherwise */ -async function handleSessionLogTracking(metadata, data) { - const originalPayload = state.repeaterTracking.sentPayload; - const channelIdx = state.repeaterTracking.channelIdx; +async function handleTxLogging(metadata, data) { + const originalPayload = state.txTracking.sentPayload; + const channelIdx = state.txTracking.channelIdx; const expectedChannelHash = WARDRIVING_CHANNEL_HASH; try { - debugLog(`[SESSION LOG] Processing rx_log entry: SNR=${metadata.snr}, RSSI=${metadata.rssi}`); + debugLog(`[TX LOG] Processing rx_log entry: SNR=${metadata.snr}, RSSI=${metadata.rssi}`); // VALIDATION STEP 1: Header validation for echo detection // Only GroupText packets (CHANNEL_GROUP_TEXT_HEADER) can be echoes of our channel messages if (metadata.header !== CHANNEL_GROUP_TEXT_HEADER) { - debugLog(`[SESSION LOG] Ignoring: header validation failed (header=0x${metadata.header.toString(16).padStart(2, '0')})`); + debugLog(`[TX LOG] Ignoring: header validation failed (header=0x${metadata.header.toString(16).padStart(2, '0')})`); return false; } - debugLog(`[SESSION LOG] Header validation passed: 0x${metadata.header.toString(16).padStart(2, '0')}`); + debugLog(`[TX LOG] Header validation passed: 0x${metadata.header.toString(16).padStart(2, '0')}`); // VALIDATION STEP 2: Validate this message is for our channel by comparing channel hash // Channel message payload structure: [1 byte channel_hash][2 bytes MAC][encrypted message] if (metadata.encryptedPayload.length < 3) { - debugLog(`[SESSION LOG] Ignoring: payload too short to contain channel hash`); + debugLog(`[TX LOG] Ignoring: payload too short to contain channel hash`); return false; } const packetChannelHash = metadata.encryptedPayload[0]; - debugLog(`[SESSION LOG] Message correlation check: packet_channel_hash=0x${packetChannelHash.toString(16).padStart(2, '0')}, expected=0x${expectedChannelHash.toString(16).padStart(2, '0')}`); + debugLog(`[TX LOG] Message correlation check: packet_channel_hash=0x${packetChannelHash.toString(16).padStart(2, '0')}, expected=0x${expectedChannelHash.toString(16).padStart(2, '0')}`); if (packetChannelHash !== expectedChannelHash) { - debugLog(`[SESSION LOG] Ignoring: channel hash mismatch (packet=0x${packetChannelHash.toString(16).padStart(2, '0')}, expected=0x${expectedChannelHash.toString(16).padStart(2, '0')})`); + debugLog(`[TX LOG] Ignoring: channel hash mismatch (packet=0x${packetChannelHash.toString(16).padStart(2, '0')}, expected=0x${expectedChannelHash.toString(16).padStart(2, '0')})`); return false; } - debugLog(`[SESSION LOG] Channel hash match confirmed - this is a message on our channel`); + debugLog(`[TX LOG] Channel hash match confirmed - this is a message on our channel`); // VALIDATION STEP 3: Decrypt and verify message content matches what we sent // This ensures we're tracking echoes of OUR specific ping, not other messages on the channel @@ -2084,7 +2083,7 @@ async function handleSessionLogTracking(metadata, data) { // For channel messages, the path contains repeater hops // Each hop in the path is 1 byte (repeater ID) if (metadata.pathLength === 0) { - debugLog(`[SESSION LOG] Ignoring: no path (direct transmission, not a repeater echo)`); + debugLog(`[TX LOG] Ignoring: no path (direct transmission, not a repeater echo)`); return false; } @@ -2098,21 +2097,21 @@ async function handleSessionLogTracking(metadata, data) { debugLog(`[PING] Repeater echo accepted: first_hop=${pathHex}, SNR=${metadata.snr}, full_path_length=${metadata.pathLength}`); // Check if we already have this path - if (state.repeaterTracking.repeaters.has(pathHex)) { - const existing = state.repeaterTracking.repeaters.get(pathHex); + if (state.txTracking.repeaters.has(pathHex)) { + const existing = state.txTracking.repeaters.get(pathHex); debugLog(`[PING] Deduplication: path ${pathHex} already seen (existing SNR=${existing.snr}, new SNR=${metadata.snr})`); // Keep the best (highest) SNR if (metadata.snr > existing.snr) { debugLog(`[PING] Deduplication decision: updating path ${pathHex} with better SNR: ${existing.snr} -> ${metadata.snr}`); - state.repeaterTracking.repeaters.set(pathHex, { + state.txTracking.repeaters.set(pathHex, { snr: metadata.snr, seenCount: existing.seenCount + 1, metadata: metadata // Store full metadata for debug mode }); // Trigger incremental UI update since SNR changed - updateCurrentLogEntryWithLiveRepeaters(); + updateCurrentTxLogEntryWithLiveRepeaters(); } else { debugLog(`[PING] Deduplication decision: keeping existing SNR for path ${pathHex} (existing ${existing.snr} >= new ${metadata.snr})`); // Still increment seen count @@ -2121,22 +2120,22 @@ async function handleSessionLogTracking(metadata, data) { } else { // New path debugLog(`[PING] Adding new repeater echo: path=${pathHex}, SNR=${metadata.snr}`); - state.repeaterTracking.repeaters.set(pathHex, { + state.txTracking.repeaters.set(pathHex, { snr: metadata.snr, seenCount: 1, metadata: metadata // Store full metadata for debug mode }); // Trigger incremental UI update for the new repeater - updateCurrentLogEntryWithLiveRepeaters(); + updateCurrentTxLogEntryWithLiveRepeaters(); } // Successfully tracked this echo - debugLog(`[SESSION LOG] ✅ Echo tracked successfully`); + debugLog(`[TX LOG] ✅ Echo tracked successfully`); return true; } catch (error) { - debugError(`[SESSION LOG] Error processing rx_log entry: ${error.message}`, error); + debugError(`[TX LOG] Error processing rx_log entry: ${error.message}`, error); return false; } } @@ -2145,8 +2144,8 @@ async function handleSessionLogTracking(metadata, data) { * Stop listening for repeater echoes and return the results * @returns {Array<{repeaterId: string, snr: number}>} Array of repeater telemetry */ -function stopRepeaterTracking() { - if (!state.repeaterTracking.isListening) { +function stopTxTracking() { + if (!state.txTracking.isListening) { return []; } @@ -2156,7 +2155,7 @@ function stopRepeaterTracking() { // Just clear the tracking state // Get the results with full data (including metadata for debug mode) - const repeaters = Array.from(state.repeaterTracking.repeaters.entries()).map(([id, data]) => ({ + const repeaters = Array.from(state.txTracking.repeaters.entries()).map(([id, data]) => ({ repeaterId: id, snr: data.snr, metadata: data.metadata // Include metadata for debug mode @@ -2168,12 +2167,12 @@ function stopRepeaterTracking() { debugLog(`[PING] Final aggregated repeater list: ${repeaters.length > 0 ? repeaters.map(r => `${r.repeaterId}(${r.snr}dB)`).join(', ') : 'none'}`); // Reset state - state.repeaterTracking.isListening = false; - state.repeaterTracking.sentTimestamp = null; - state.repeaterTracking.sentPayload = null; - state.repeaterTracking.repeaters.clear(); - state.repeaterTracking.rxLogHandler = null; // Kept for compatibility - state.repeaterTracking.currentLogEntry = null; + state.txTracking.isListening = false; + state.txTracking.sentTimestamp = null; + state.txTracking.sentPayload = null; + state.txTracking.repeaters.clear(); + state.txTracking.rxLogHandler = null; // Kept for compatibility + state.txTracking.currentLogEntry = null; return repeaters; } @@ -2208,9 +2207,9 @@ async function handleUnifiedRxLogEvent(data) { debugLog(`[UNIFIED RX] Packet received: header=0x${metadata.header.toString(16)}, pathLength=${metadata.pathLength}`); // Route to TX tracking if active - if (state.repeaterTracking.isListening) { + if (state.txTracking.isListening) { debugLog("[UNIFIED RX] TX tracking active - delegating to TX handler"); - const wasEcho = await handleSessionLogTracking(metadata, data); + const wasEcho = await handleTxLogging(metadata, data); if (wasEcho) { debugLog("[UNIFIED RX] Packet was TX echo, done"); return; @@ -2218,9 +2217,9 @@ async function handleUnifiedRxLogEvent(data) { } // Route to RX wardriving if active - if (state.passiveRxTracking.isListening) { + if (state.rxTracking.isListening) { debugLog("[UNIFIED RX] RX wardriving active - delegating to RX handler"); - await handlePassiveRxLogging(metadata, data); + await handleRxLogging(metadata, data); } } catch (error) { debugError("[UNIFIED RX] Error processing rx_log entry", error); @@ -2233,16 +2232,16 @@ async function handleUnifiedRxLogEvent(data) { * @param {Object} metadata - Parsed metadata from parseRxPacketMetadata() * @param {Object} data - The LogRxData event data (contains lastSnr, lastRssi, raw) */ -async function handlePassiveRxLogging(metadata, data) { +async function handleRxLogging(metadata, data) { try { - debugLog(`[PASSIVE RX] Processing packet for passive logging`); + debugLog(`[RX LOG] Processing packet for passive logging`); // VALIDATION: Check path length (need at least one hop) // A packet's path array contains the sequence of repeater IDs that forwarded the message. // Packets with no path are direct transmissions (node-to-node) and don't provide // information about repeater coverage, so we skip them for RX wardriving purposes. if (metadata.pathLength === 0) { - debugLog(`[PASSIVE RX] Ignoring: no path (direct transmission, not via repeater)`); + debugLog(`[RX LOG] Ignoring: no path (direct transmission, not via repeater)`); return; } @@ -2250,11 +2249,11 @@ async function handlePassiveRxLogging(metadata, data) { const lastHopId = metadata.lastHop; const repeaterId = lastHopId.toString(16).padStart(2, '0'); - debugLog(`[PASSIVE RX] Packet heard via last hop: ${repeaterId}, SNR=${metadata.snr}, path_length=${metadata.pathLength}`); + debugLog(`[RX LOG] Packet heard via last hop: ${repeaterId}, SNR=${metadata.snr}, path_length=${metadata.pathLength}`); // Get current GPS location if (!state.lastFix) { - debugLog(`[PASSIVE RX] No GPS fix available, skipping entry`); + debugLog(`[RX LOG] No GPS fix available, skipping entry`); return; } @@ -2265,10 +2264,10 @@ async function handlePassiveRxLogging(metadata, data) { // Add entry to RX log (including RSSI, path length, and header for CSV export) addRxLogEntry(repeaterId, metadata.snr, metadata.rssi, metadata.pathLength, metadata.header, lat, lon, timestamp); - debugLog(`[PASSIVE RX] ✅ Observation logged: repeater=${repeaterId}, snr=${metadata.snr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); + debugLog(`[RX LOG] ✅ Observation logged: repeater=${repeaterId}, snr=${metadata.snr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); // Handle tracking for API (best SNR with distance trigger) - handlePassiveRxForAPI( + handleRxBatching( repeaterId, metadata.snr, metadata.rssi, @@ -2279,7 +2278,7 @@ async function handlePassiveRxLogging(metadata, data) { ); } catch (error) { - debugError(`[PASSIVE RX] Error processing passive RX: ${error.message}`, error); + debugError(`[RX LOG] Error processing passive RX: ${error.message}`, error); } } @@ -2289,7 +2288,7 @@ async function handlePassiveRxLogging(metadata, data) { * Start unified RX listening - handles both Session Log tracking and passive RX logging */ function startUnifiedRxListening() { - if (state.passiveRxTracking.isListening) { + if (state.rxTracking.isListening) { debugLog(`[UNIFIED RX] Already listening, skipping start`); return; } @@ -2302,9 +2301,9 @@ function startUnifiedRxListening() { debugLog(`[UNIFIED RX] Starting unified RX listening`); const handler = (data) => handleUnifiedRxLogEvent(data); - state.passiveRxTracking.rxLogHandler = handler; + state.rxTracking.rxLogHandler = handler; state.connection.on(Constants.PushCodes.LogRxData, handler); - state.passiveRxTracking.isListening = true; + state.rxTracking.isListening = true; debugLog(`[UNIFIED RX] ✅ Unified listening started successfully`); } @@ -2313,19 +2312,19 @@ function startUnifiedRxListening() { * Stop unified RX listening */ function stopUnifiedRxListening() { - if (!state.passiveRxTracking.isListening) { + if (!state.rxTracking.isListening) { return; } debugLog(`[UNIFIED RX] Stopping unified RX listening`); - if (state.connection && state.passiveRxTracking.rxLogHandler) { - state.connection.off(Constants.PushCodes.LogRxData, state.passiveRxTracking.rxLogHandler); + if (state.connection && state.rxTracking.rxLogHandler) { + state.connection.off(Constants.PushCodes.LogRxData, state.rxTracking.rxLogHandler); debugLog(`[UNIFIED RX] Unregistered LogRxData event handler`); } - state.passiveRxTracking.isListening = false; - state.passiveRxTracking.rxLogHandler = null; + state.rxTracking.isListening = false; + state.rxTracking.rxLogHandler = null; debugLog(`[UNIFIED RX] ✅ Unified listening stopped`); } @@ -2338,7 +2337,7 @@ function stopUnifiedRxListening() { */ async function postRxLogToMeshMapperAPI(entries) { if (!MESHMAPPER_RX_LOG_API_URL) { - debugLog('[PASSIVE RX] RX Log API posting not configured yet'); + debugLog('[RX LOG] RX Log API posting not configured yet'); return; } @@ -2346,7 +2345,7 @@ async function postRxLogToMeshMapperAPI(entries) { // - Batch post accumulated RX log entries // - Include session_id from state.wardriveSessionId // - Format: { observations: [{ repeaterId, snr, lat, lon, timestamp }] } - debugLog(`[PASSIVE RX] Would post ${entries.length} RX log entries to API (not implemented yet)`); + debugLog(`[RX LOG] Would post ${entries.length} RX log entries to API (not implemented yet)`); } // ---- Passive RX Batch API Integration ---- @@ -2362,7 +2361,7 @@ async function postRxLogToMeshMapperAPI(entries) { * @param {Object} currentLocation - Current GPS location {lat, lon} * @param {Object} metadata - Parsed metadata for debug mode */ -function handlePassiveRxForAPI(repeaterId, snr, rssi, pathLength, header, currentLocation, metadata) { +function handleRxBatching(repeaterId, snr, rssi, pathLength, header, currentLocation, metadata) { // Get or create buffer entry for this repeater let buffer = state.rxBatchBuffer.get(repeaterId); @@ -2448,7 +2447,7 @@ function flushRepeater(repeaterId) { debugLog(`[RX BATCH] Posting repeater ${repeaterId}: snr=${best.snr}, location=${best.lat.toFixed(5)},${best.lon.toFixed(5)}`); // Queue for API posting - queueApiPost(entry); + queueRxApiPost(entry); // Remove from buffer state.rxBatchBuffer.delete(repeaterId); @@ -2459,7 +2458,7 @@ function flushRepeater(repeaterId) { * Flush all active batches (called on session end, disconnect, etc.) * @param {string} trigger - What caused the flush: 'session_end' | 'disconnect' | etc. */ -function flushAllBatches(trigger = 'session_end') { +function flushAllRxBatches(trigger = 'session_end') { debugLog(`[RX BATCH] Flushing all repeaters, trigger=${trigger}, active_repeaters=${state.rxBatchBuffer.size}`); if (state.rxBatchBuffer.size === 0) { @@ -2481,7 +2480,7 @@ function flushAllBatches(trigger = 'session_end') { * Uses the batch queue system to aggregate RX messages * @param {Object} entry - The entry to post (with best observation data) */ -function queueApiPost(entry) { +function queueRxApiPost(entry) { // Validate session_id exists if (!state.wardriveSessionId) { debugWarn(`[RX BATCH API] Cannot queue: no session_id available`); @@ -2661,46 +2660,46 @@ function createLogEntryElement(entry) { /** * Update summary bar with latest log data */ -function updateLogSummary() { - if (!logCount || !logLastTime || !logLastSnr) return; +function updateTxLogSummary() { + if (!txLogCount || !txLogLastTime || !txLogLastSnr) return; - const count = sessionLogState.entries.length; - logCount.textContent = count === 1 ? '1 ping' : `${count} pings`; + const count = txLogState.entries.length; + txLogCount.textContent = count === 1 ? '1 ping' : `${count} pings`; if (count === 0) { - logLastTime.textContent = 'No data'; - logLastSnr.textContent = '—'; - debugLog('[SESSION LOG] Session log summary updated: no entries'); + txLogLastTime.textContent = 'No data'; + txLogLastSnr.textContent = '—'; + debugLog('[TX LOG] Session log summary updated: no entries'); return; } - const lastEntry = sessionLogState.entries[count - 1]; + const lastEntry = txLogState.entries[count - 1]; const date = new Date(lastEntry.timestamp); - logLastTime.textContent = date.toLocaleTimeString(); + txLogLastTime.textContent = date.toLocaleTimeString(); // Count total heard repeats in the latest ping const heardCount = lastEntry.events.length; - debugLog(`[SESSION LOG] Session log summary updated: ${count} total pings, latest ping heard ${heardCount} repeats`); + debugLog(`[TX LOG] Session log summary updated: ${count} total pings, latest ping heard ${heardCount} repeats`); if (heardCount > 0) { - logLastSnr.textContent = heardCount === 1 ? '1 Repeat' : `${heardCount} Repeats`; - logLastSnr.className = 'text-xs font-mono text-slate-300'; + txLogLastSnr.textContent = heardCount === 1 ? '1 Repeat' : `${heardCount} Repeats`; + txLogLastSnr.className = 'text-xs font-mono text-slate-300'; } else { - logLastSnr.textContent = '0 Repeats'; - logLastSnr.className = 'text-xs font-mono text-slate-500'; + txLogLastSnr.textContent = '0 Repeats'; + txLogLastSnr.className = 'text-xs font-mono text-slate-500'; } } /** * Render all log entries to the session log */ -function renderLogEntries() { +function renderTxLogEntries() { if (!sessionPingsEl) return; - debugLog(`[UI] Rendering ${sessionLogState.entries.length} log entries`); + debugLog(`[UI] Rendering ${txLogState.entries.length} log entries`); sessionPingsEl.innerHTML = ''; - if (sessionLogState.entries.length === 0) { + if (txLogState.entries.length === 0) { // Show placeholder when no entries const placeholder = document.createElement('div'); placeholder.className = 'text-xs text-slate-500 italic text-center py-4'; @@ -2711,7 +2710,7 @@ function renderLogEntries() { } // Render newest first - const entries = [...sessionLogState.entries].reverse(); + const entries = [...txLogState.entries].reverse(); entries.forEach((entry, index) => { const element = createLogEntryElement(entry); @@ -2720,8 +2719,8 @@ function renderLogEntries() { }); // Auto-scroll to top (newest) - if (sessionLogState.autoScroll && logScrollContainer) { - logScrollContainer.scrollTop = 0; + if (txLogState.autoScroll && txLogScrollContainer) { + txLogScrollContainer.scrollTop = 0; debugLog(`[UI] Auto-scrolled to top of log container`); } @@ -2731,23 +2730,23 @@ function renderLogEntries() { /** * Toggle session log expanded/collapsed */ -function toggleBottomSheet() { - sessionLogState.isExpanded = !sessionLogState.isExpanded; +function toggleTxLogBottomSheet() { + txLogState.isExpanded = !txLogState.isExpanded; - if (logBottomSheet) { - if (sessionLogState.isExpanded) { - logBottomSheet.classList.add('open'); - logBottomSheet.classList.remove('hidden'); + if (txLogBottomSheet) { + if (txLogState.isExpanded) { + txLogBottomSheet.classList.add('open'); + txLogBottomSheet.classList.remove('hidden'); } else { - logBottomSheet.classList.remove('open'); - logBottomSheet.classList.add('hidden'); + txLogBottomSheet.classList.remove('open'); + txLogBottomSheet.classList.add('hidden'); } } // Toggle arrow rotation - const logExpandArrow = document.getElementById('logExpandArrow'); + const logExpandArrow = document.getElementById('txLogExpandArrow'); if (logExpandArrow) { - if (sessionLogState.isExpanded) { + if (txLogState.isExpanded) { logExpandArrow.classList.add('expanded'); } else { logExpandArrow.classList.remove('expanded'); @@ -2755,16 +2754,16 @@ function toggleBottomSheet() { } // Toggle copy button and status visibility - if (sessionLogState.isExpanded) { + if (txLogState.isExpanded) { // Hide status elements, show copy button - if (logLastSnr) logLastSnr.classList.add('hidden'); - if (sessionLogCopyBtn) sessionLogCopyBtn.classList.remove('hidden'); - debugLog('[SESSION LOG] Expanded - showing copy button, hiding status'); + if (txLogLastSnr) txLogLastSnr.classList.add('hidden'); + if (txLogCopyBtn) txLogCopyBtn.classList.remove('hidden'); + debugLog('[TX LOG] Expanded - showing copy button, hiding status'); } else { // Show status elements, hide copy button - if (logLastSnr) logLastSnr.classList.remove('hidden'); - if (sessionLogCopyBtn) sessionLogCopyBtn.classList.add('hidden'); - debugLog('[SESSION LOG] Collapsed - hiding copy button, showing status'); + if (txLogLastSnr) txLogLastSnr.classList.remove('hidden'); + if (txLogCopyBtn) txLogCopyBtn.classList.add('hidden'); + debugLog('[TX LOG] Collapsed - hiding copy button, showing status'); } } @@ -2775,14 +2774,14 @@ function toggleBottomSheet() { * @param {string} lon - Longitude * @param {string} eventsStr - Events string (e.g., "4e(12),b7(0)" or "None") */ -function addLogEntry(timestamp, lat, lon, eventsStr) { +function addTxLogEntry(timestamp, lat, lon, eventsStr) { const logLine = `${timestamp} | ${lat},${lon} | ${eventsStr}`; const entry = parseLogEntry(logLine); if (entry) { - sessionLogState.entries.push(entry); - renderLogEntries(); - updateLogSummary(); + txLogState.entries.push(entry); + renderTxLogEntries(); + updateTxLogSummary(); } } @@ -3238,11 +3237,11 @@ function addErrorLogEntry(message, source = null) { * Columns: Timestamp,Latitude,Longitude,Repeater1_ID,Repeater1_SNR,Repeater2_ID,Repeater2_SNR,... * @returns {string} CSV formatted string */ -function sessionLogToCSV() { - debugLog('[SESSION LOG] Converting session log to CSV format'); +function txLogToCSV() { + debugLog('[TX LOG] Converting session log to CSV format'); - if (sessionLogState.entries.length === 0) { - debugWarn('[SESSION LOG] No session log entries to export'); + if (txLogState.entries.length === 0) { + debugWarn('[TX LOG] No session log entries to export'); return 'Timestamp,Latitude,Longitude,Repeats\n'; } @@ -3250,7 +3249,7 @@ function sessionLogToCSV() { const header = 'Timestamp,Latitude,Longitude,Repeats\n'; // Build CSV rows - const rows = sessionLogState.entries.map(entry => { + const rows = txLogState.entries.map(entry => { let row = `${entry.timestamp},${entry.lat},${entry.lon}`; // Combine all repeater data into single Repeats column @@ -3268,7 +3267,7 @@ function sessionLogToCSV() { }); const csv = header + rows.join('\n'); - debugLog(`[SESSION LOG] CSV export complete: ${sessionLogState.entries.length} entries`); + debugLog(`[TX LOG] CSV export complete: ${txLogState.entries.length} entries`); return csv; } @@ -3341,12 +3340,12 @@ async function copyLogToCSV(logType, button) { switch (logType) { case 'session': - csv = sessionLogToCSV(); - logTag = '[SESSION LOG]'; + csv = txLogToCSV(); + logTag = '[TX LOG]'; break; case 'rx': csv = rxLogToCSV(); - logTag = '[PASSIVE RX UI]'; + logTag = '[RX LOG UI]'; break; case 'error': csv = errorLogToCSV(); @@ -3419,7 +3418,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { if (isAutoMode) { // Auto mode: validate GPS freshness before sending if (!state.lastFix) { - debugWarn("[AUTO] Auto ping skipped: no GPS fix available yet"); + debugWarn("[TX/RX AUTO] Auto ping skipped: no GPS fix available yet"); setDynamicStatus("Waiting for GPS fix", STATUS_COLORS.warning); return null; } @@ -3506,7 +3505,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { * @param {number} lon - Longitude * @returns {Object|null} The log entry object for later updates, or null */ -function logPingToUI(payload, lat, lon) { +function logTxPingToUI(payload, lat, lon) { // Use ISO format for data storage but user-friendly format for display const now = new Date(); const isoStr = now.toISOString(); @@ -3524,7 +3523,7 @@ function logPingToUI(payload, lat, lon) { }; // Add to session log (this will handle both mobile and desktop) - addLogEntry(logData.timestamp, logData.lat, logData.lon, logData.eventsStr); + addTxLogEntry(logData.timestamp, logData.lat, logData.lon, logData.eventsStr); return logData; } @@ -3534,13 +3533,13 @@ function logPingToUI(payload, lat, lon) { * @param {Object|null} logData - The log data object to update * @param {Array<{repeaterId: string, snr: number}>} repeaters - Array of repeater telemetry */ -function updatePingLogWithRepeaters(logData, repeaters) { +function updateTxLogWithRepeaters(logData, repeaters) { if (!logData) return; const repeaterStr = formatRepeaterTelemetry(repeaters); - // Find and update the entry in sessionLogState - const entryIndex = sessionLogState.entries.findIndex( + // Find and update the entry in txLogState + const entryIndex = txLogState.entries.findIndex( e => e.timestamp === logData.timestamp && e.lat === logData.lat && e.lon === logData.lon ); @@ -3550,9 +3549,9 @@ function updatePingLogWithRepeaters(logData, repeaters) { const updatedEntry = parseLogEntry(logLine); if (updatedEntry) { - sessionLogState.entries[entryIndex] = updatedEntry; - renderLogEntries(); - updateLogSummary(); + txLogState.entries[entryIndex] = updatedEntry; + renderTxLogEntries(); + updateTxLogSummary(); } } @@ -3563,16 +3562,16 @@ function updatePingLogWithRepeaters(logData, repeaters) { * Incrementally update the current ping log entry as repeaters are detected * This provides real-time updates during the RX listening window */ -function updateCurrentLogEntryWithLiveRepeaters() { +function updateCurrentTxLogEntryWithLiveRepeaters() { // Only update if we're actively listening and have a current log entry - if (!state.repeaterTracking.isListening || !state.repeaterTracking.currentLogEntry) { + if (!state.txTracking.isListening || !state.txTracking.currentLogEntry) { return; } - const logData = state.repeaterTracking.currentLogEntry; + const logData = state.txTracking.currentLogEntry; // Convert current repeaters Map to array format - const repeaters = Array.from(state.repeaterTracking.repeaters.entries()).map(([id, data]) => ({ + const repeaters = Array.from(state.txTracking.repeaters.entries()).map(([id, data]) => ({ repeaterId: id, snr: data.snr })); @@ -3580,8 +3579,8 @@ function updateCurrentLogEntryWithLiveRepeaters() { // Sort by repeater ID for deterministic output repeaters.sort((a, b) => a.repeaterId.localeCompare(b.repeaterId)); - // Reuse the existing updatePingLogWithRepeaters function - updatePingLogWithRepeaters(logData, repeaters); + // Reuse the existing updateTxLogWithRepeaters function + updateTxLogWithRepeaters(logData, repeaters); debugLog(`[PING] Incrementally updated ping log entry: ${repeaters.length} repeater(s) detected so far`); } @@ -3602,12 +3601,12 @@ async function sendPing(manual = false) { } // Handle countdown timers based on ping type - if (manual && state.running) { + if (manual && state.txRxAutoRunning) { // Manual ping during auto mode: pause the auto countdown debugLog("[PING] Manual ping during auto mode - pausing auto countdown"); pauseAutoCountdown(); setDynamicStatus("Sending manual ping", STATUS_COLORS.info); - } else if (!manual && state.running) { + } else if (!manual && state.txRxAutoRunning) { // Auto ping: stop the countdown timer to avoid status conflicts stopAutoCountdown(); setDynamicStatus("Sending auto ping", STATUS_COLORS.info); @@ -3617,11 +3616,11 @@ async function sendPing(manual = false) { } // Get GPS coordinates - const coords = await getGpsCoordinatesForPing(!manual && state.running); + const coords = await getGpsCoordinatesForPing(!manual && state.txRxAutoRunning); if (!coords) { // GPS not available, message already shown // For auto mode, schedule next attempt - if (!manual && state.running) { + if (!manual && state.txRxAutoRunning) { scheduleNextAutoPing(); } // For manual ping during auto mode, resume the paused countdown @@ -3646,7 +3645,7 @@ async function sendPing(manual = false) { setDynamicStatus("Ping skipped, outside of geofenced region", STATUS_COLORS.warning); // If auto mode is running, resume the paused countdown handleManualPingBlockedDuringAutoMode(); - } else if (state.running) { + } else if (state.txRxAutoRunning) { // Auto ping: schedule next ping and show countdown with skip message scheduleNextAutoPing(); } @@ -3668,7 +3667,7 @@ async function sendPing(manual = false) { setDynamicStatus("Ping skipped, too close to last ping", STATUS_COLORS.warning); // If auto mode is running, resume the paused countdown handleManualPingBlockedDuringAutoMode(); - } else if (state.running) { + } else if (state.txRxAutoRunning) { // Auto ping: schedule next ping and show countdown with skip message scheduleNextAutoPing(); } @@ -3696,7 +3695,7 @@ async function sendPing(manual = false) { // Start repeater echo tracking BEFORE sending the ping debugLog(`[PING] Channel ping transmission: timestamp=${new Date().toISOString()}, channel=${ch.channelIdx}, payload="${payload}"`); - startRepeaterTracking(payload, ch.channelIdx); + startTxTracking(payload, ch.channelIdx); await state.connection.sendChannelTextMessage(ch.channelIdx, payload); debugLog(`[PING] Ping sent successfully to channel ${ch.channelIdx}`); @@ -3716,10 +3715,10 @@ async function sendPing(manual = false) { setDynamicStatus("Ping sent", STATUS_COLORS.success); // Create UI log entry with placeholder for repeater data - const logEntry = logPingToUI(payload, lat, lon); + const logEntry = logTxPingToUI(payload, lat, lon); // Store log entry in repeater tracking state for incremental updates - state.repeaterTracking.currentLogEntry = logEntry; + state.txTracking.currentLogEntry = logEntry; // Start RX listening countdown // The minimum 500ms visibility of "Ping sent" is enforced by setStatus() @@ -3739,11 +3738,11 @@ async function sendPing(manual = false) { stopRxListeningCountdown(); // Stop repeater tracking and get final results - const repeaters = stopRepeaterTracking(); + const repeaters = stopTxTracking(); debugLog(`[PING] Finalized heard repeats: ${repeaters.length} unique paths detected`); // Update UI log with repeater data - updatePingLogWithRepeaters(logEntry, repeaters); + updateTxLogWithRepeaters(logEntry, repeaters); // Format repeater data for API const heardRepeatsStr = formatRepeaterTelemetry(repeaters); @@ -3758,15 +3757,15 @@ async function sendPing(manual = false) { // Update status and start next timer IMMEDIATELY (before API post) // This is the key change: we don't wait for API to complete if (state.connection) { - if (state.running) { + if (state.txRxAutoRunning) { // Check if we should resume a paused auto countdown (manual ping during auto mode) const resumed = resumeAutoCountdown(); if (!resumed) { // No paused timer to resume, schedule new auto ping (this was an auto ping) - debugLog("[AUTO] Scheduling next auto ping immediately after RX window"); + debugLog("[TX/RX AUTO] Scheduling next auto ping immediately after RX window"); scheduleNextAutoPing(); } else { - debugLog("[AUTO] Resumed auto countdown after manual ping"); + debugLog("[TX/RX AUTO] Resumed auto countdown after manual ping"); } } else { debugLog("[UI] Setting dynamic status to Idle (manual mode)"); @@ -3812,17 +3811,17 @@ async function sendPing(manual = false) { // ---- Auto mode ---- function stopAutoPing(stopGps = false) { - debugLog(`[AUTO] stopAutoPing called (stopGps=${stopGps})`); + debugLog(`[TX/RX AUTO] stopAutoPing called (stopGps=${stopGps})`); // Check if we're in cooldown before stopping (unless stopGps is true for disconnect) if (!stopGps && isInCooldown()) { const remainingSec = getRemainingCooldownSeconds(); - debugLog(`[AUTO] Auto ping stop blocked by cooldown (${remainingSec}s remaining)`); + debugLog(`[TX/RX AUTO] Auto ping stop blocked by cooldown (${remainingSec}s remaining)`); setDynamicStatus(`Wait ${remainingSec}s before toggling auto mode`, STATUS_COLORS.warning); return; } if (state.autoTimerId) { - debugLog("[AUTO] Clearing auto ping timer"); + debugLog("[TX/RX AUTO] Clearing auto ping timer"); clearTimeout(state.autoTimerId); state.autoTimerId = null; } @@ -3837,38 +3836,38 @@ function stopAutoPing(stopGps = false) { stopGeoWatch(); } - state.running = false; + state.txRxAutoRunning = false; updateAutoButton(); releaseWakeLock(); - debugLog("[AUTO] Auto ping stopped"); + debugLog("[TX/RX AUTO] Auto ping stopped"); } function scheduleNextAutoPing() { - if (!state.running) { - debugLog("[AUTO] Not scheduling next auto ping - auto mode not running"); + if (!state.txRxAutoRunning) { + debugLog("[TX/RX AUTO] Not scheduling next auto ping - auto mode not running"); return; } const intervalMs = getSelectedIntervalMs(); - debugLog(`[AUTO] Scheduling next auto ping in ${intervalMs}ms`); + debugLog(`[TX/RX AUTO] Scheduling next auto ping in ${intervalMs}ms`); // Start countdown immediately (skipReason may be set if ping was skipped) startAutoCountdown(intervalMs); // Schedule the next ping state.autoTimerId = setTimeout(() => { - if (state.running) { + if (state.txRxAutoRunning) { // Clear skip reason before next attempt state.skipReason = null; - debugLog("[AUTO] Auto ping timer fired, sending ping"); + debugLog("[TX/RX AUTO] Auto ping timer fired, sending ping"); sendPing(false).catch(console.error); } }, intervalMs); } function startAutoPing() { - debugLog("[AUTO] startAutoPing called"); + debugLog("[TX/RX AUTO] startAutoPing called"); if (!state.connection) { - debugError("[AUTO] Cannot start auto ping - not connected"); + debugError("[TX/RX AUTO] Cannot start auto ping - not connected"); alert("Connect to a MeshCore device first."); return; } @@ -3876,14 +3875,14 @@ function startAutoPing() { // Check if we're in cooldown if (isInCooldown()) { const remainingSec = getRemainingCooldownSeconds(); - debugLog(`[AUTO] Auto ping start blocked by cooldown (${remainingSec}s remaining)`); + debugLog(`[TX/RX AUTO] Auto ping start blocked by cooldown (${remainingSec}s remaining)`); setDynamicStatus(`Wait ${remainingSec}s before toggling auto mode`, STATUS_COLORS.warning); return; } // Clean up any existing auto-ping timer (but keep GPS watch running) if (state.autoTimerId) { - debugLog("[AUTO] Clearing existing auto ping timer"); + debugLog("[TX/RX AUTO] Clearing existing auto ping timer"); clearTimeout(state.autoTimerId); state.autoTimerId = null; } @@ -3893,18 +3892,18 @@ function startAutoPing() { state.skipReason = null; // Start GPS watch for continuous updates - debugLog("[AUTO] Starting GPS watch for auto mode"); + debugLog("[TX/RX AUTO] Starting GPS watch for auto mode"); startGeoWatch(); - state.running = true; + state.txRxAutoRunning = true; updateAutoButton(); // Acquire wake lock for auto mode - debugLog("[AUTO] Acquiring wake lock for auto mode"); + debugLog("[TX/RX AUTO] Acquiring wake lock for auto mode"); acquireWakeLock().catch(console.error); // Send first ping immediately - debugLog("[AUTO] Sending initial auto ping"); + debugLog("[TX/RX AUTO] Sending initial auto ping"); sendPing(false).catch(console.error); } @@ -4074,11 +4073,11 @@ async function connect() { updateAutoButton(); stopGeoWatch(); stopGpsAgeUpdater(); // Ensure age updater stops - stopRepeaterTracking(); // Stop repeater echo tracking + stopTxTracking(); // Stop repeater echo tracking stopUnifiedRxListening(); // Stop unified RX listening // Flush all pending RX batch data before cleanup - flushAllBatches('disconnect'); + flushAllRxBatches('disconnect'); // Clear API queue messages (timers already stopped in cleanupAllTimers) apiQueue.messages = []; @@ -4185,7 +4184,7 @@ async function disconnect() { document.addEventListener("visibilitychange", async () => { if (document.hidden) { debugLog("[UI] Page visibility changed to hidden"); - if (state.running) { + if (state.txRxAutoRunning) { debugLog("[UI] Stopping auto ping due to page hidden"); stopAutoPing(true); // Ignore cooldown check when page is hidden setDynamicStatus("Lost focus, auto mode stopped", STATUS_COLORS.warning); @@ -4243,13 +4242,13 @@ export async function onLoad() { setDynamicStatus(e.message || "Connection failed", STATUS_COLORS.error); } }); - sendPingBtn.addEventListener("click", () => { + txPingBtn.addEventListener("click", () => { debugLog("[UI] Manual ping button clicked"); sendPing(true).catch(console.error); }); - autoToggleBtn.addEventListener("click", () => { + txRxAutoBtn.addEventListener("click", () => { debugLog("[UI] Auto toggle button clicked"); - if (state.running) { + if (state.txRxAutoRunning) { stopAutoPing(); setDynamicStatus("Auto mode stopped", STATUS_COLORS.idle); } else { @@ -4302,10 +4301,10 @@ export async function onLoad() { }); // Session Log event listener - if (logSummaryBar) { - logSummaryBar.addEventListener("click", () => { + if (txLogSummaryBar) { + txLogSummaryBar.addEventListener("click", () => { debugLog("[UI] Log summary bar clicked - toggling session log"); - toggleBottomSheet(); + toggleTxLogBottomSheet(); }); } @@ -4326,11 +4325,11 @@ export async function onLoad() { } // Copy button event listeners - if (sessionLogCopyBtn) { - sessionLogCopyBtn.addEventListener("click", (e) => { + if (txLogCopyBtn) { + txLogCopyBtn.addEventListener("click", (e) => { e.stopPropagation(); // Prevent triggering the summary bar toggle - debugLog("[SESSION LOG] Copy button clicked"); - copyLogToCSV('session', sessionLogCopyBtn); + debugLog("[TX LOG] Copy button clicked"); + copyLogToCSV('session', txLogCopyBtn); }); } diff --git a/index.html b/index.html index 8613b32..64659c6 100644 --- a/index.html +++ b/index.html @@ -179,13 +179,13 @@

Settings<
- -
@@ -196,32 +196,32 @@

Settings<

- -
+ +
-
+
-

Session Log

+

TX Log

| - 0 pings - + 0 pings +
- — - - +
-