diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..21e4a33 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,224 @@ +# MeshCore GOME WarDriver - AI Agent Instructions + +> **Keeping This File Updated**: When you make architectural changes, add new workflows, or modify critical patterns, update this file. Ask the AI: *"Update .github/copilot-instructions.md to reflect the changes I just made"* - it will analyze the modifications and update relevant sections. + +## Project Overview + +Browser-based Progressive Web App for wardriving with MeshCore mesh network devices. Connects via Web Bluetooth to send GPS-tagged pings to a `#wardriving` channel, track repeater echoes, and post coverage data to the MeshMapper API for community mesh mapping. + +**Tech Stack**: Vanilla JavaScript (ES6 modules), Web Bluetooth API, Geolocation API, Tailwind CSS v4 + +**Critical Files**: +- `content/wardrive.js` (4500+ lines) - Main application logic +- `content/mc/` - MeshCore BLE protocol library (Connection classes, Packet parsing, Buffer utilities) +- `index.html` - Single-page UI with embedded Leaflet map +- `docs/` - Comprehensive workflow documentation (CONNECTION_WORKFLOW.md, PING_WORKFLOW.md, etc.) + +## Architecture & Data Flow + +### 1. Connection Architecture +Three-layer connection system: +- **BLE Layer**: `WebBleConnection` (extends `Connection` base class) handles GATT connection, characteristic notifications +- **Protocol Layer**: `Connection.js` (2200+ lines) implements MeshCore companion protocol - packet framing, encryption, channel management, device queries +- **App Layer**: `wardrive.js` orchestrates connect/disconnect workflows with 10-step sequences (see `docs/CONNECTION_WORKFLOW.md`) + +**Connect Sequence**: BLE GATT → Protocol Handshake → Device Info → Time Sync → Capacity Check (API slot acquisition) → Channel Setup → GPS Init → Connected + +### 2. Ping Lifecycle & API Queue System +Two independent data flows merge into a unified API batch queue: + +**TX Flow** (Transmit): +1. User sends ping → `sendPing()` validates GPS/geofence/distance +2. Sends `@[MapperBot][ ]` to `#wardriving` channel via BLE +3. Starts 6-second RX listening window for repeater echoes +4. After window: posts to API queue with type "TX" +5. Triggers 3-second flush timer for real-time map updates + +**RX Flow** (Receive - Passive monitoring): +1. Always-on `handleUnifiedRxLogEvent()` captures ALL incoming packets (no filtering) +2. Validates path length > 0 (must route via repeater, not direct) +3. Buffers RX events per repeater with GPS coordinates +4. Flushes to API queue on 25m movement or 30s timeout, type "RX" + +**API Queue** (`apiQueue.messages[]`): +- Max 50 messages, auto-flush on size/30s timer/TX triggers +- Batch POST to `yow.meshmapper.net/wardriving-api.php` +- See `docs/FLOW_WARDRIVE_API_QUEUE_DIAGRAM.md` for visual flow + +### 3. GPS & Geofencing +- **GPS Watch**: Continuous `navigator.geolocation.watchPosition()` with high accuracy +- **Freshness**: Manual pings use 60s max age, auto pings require fresh acquisition +- **Ottawa Geofence**: 150km radius from Parliament Hill (45.4215, -75.6972) - hard boundary +- **Min Distance Filter**: 25m between pings (prevents spam, separate from 25m RX batch trigger) + +### 4. State Management +Global `state` object tracks: +- `connection`: Active BLE connection instance +- `wardrivingChannel`: Channel object for ping sends +- `txRxAutoRunning` / `autoTimerId` / `nextAutoPingTime`: Auto-ping state +- `lastPingLat/Lon`: For distance validation +- `cooldownEndTime`: 7-second cooldown after each ping +- `sessionId`: UUID for correlating TX/RX events per wardrive session + +**RX Batch Buffer**: `Map` keyed by repeater node ID → `{rxEvents: [], bufferedSince, lastFlushed, flushTimerId}` + +## Critical Developer Workflows + +### Build & Development +```bash +npm install # Install Tailwind CLI +npm run build:css # One-time CSS build +npm run watch:css # Watch mode for development +``` + +**No bundler/compiler** - Open `index.html` directly in browser (Chrome/Chromium required for Web Bluetooth). + +### Debug Logging System +All debug output controlled by `DEBUG_ENABLED` flag (URL param `?debug=true` or hardcoded): +```javascript +debugLog("[TAG] message", ...args); // General info +debugWarn("[TAG] message", ...args); // Warnings +debugError("[TAG] message", ...args); // Errors (also adds to UI Error Log) +``` + +**Required Tags** (see `docs/DEVELOPMENT_REQUIREMENTS.md`): `[BLE]`, `[GPS]`, `[PING]`, `[API QUEUE]`, `[RX BATCH]`, `[UNIFIED RX]`, `[UI]`, etc. NEVER log without a tag. + +### Status Message System +**Two separate status bars** (NEVER mix them): +1. **Connection Status** (`setConnStatus(text, color)`) - ONLY "Connected", "Connecting", "Disconnected", "Disconnecting" +2. **Dynamic Status** (`setDynamicStatus(text, color, immediate)`) - All operational messages, 500ms minimum visibility, blocks connection words + +Use `STATUS_COLORS` constants: `idle`, `success`, `warning`, `error`, `info` + +## Project-Specific Patterns + +### 1. Countdown Timer Pattern +Reusable countdown system for cooldowns, auto-ping intervals, RX listening windows: +```javascript +function createCountdownTimer(getEndTime, getStatusMessage) { + // Returns timer state, updates UI every 500ms + // Handles pause/resume for manual ping interrupts +} +``` +Used by `startAutoCountdown()`, `startRxListeningCountdown()`, cooldown logic. + +### 2. Channel Hash & Decryption +**Pre-computed at startup**: +```javascript +WARDRIVING_CHANNEL_KEY = await deriveChannelKey("#wardriving"); // PBKDF2 SHA-256 +WARDRIVING_CHANNEL_HASH = await computeChannelHash(key); // PSK channel identifier +``` +Used for: +- Repeater echo detection (match `channelHash` in received packets) +- Message decryption (AES-ECB via aes-js library) + +### 3. Wake Lock Management +Auto-ping mode acquires Screen Wake Lock to keep GPS active: +```javascript +await acquireWakeLock(); // On auto-ping start +await releaseWakeLock(); // On stop/disconnect +``` +Handles visibility changes (release on hidden, reacquire on visible). + +### 4. Capacity Check (API Slot Management) +Before connecting, app must acquire a slot from MeshMapper backend: +```javascript +POST /capacitycheck.php { iatacode: "YOW", apikey: "...", apiver: "1.6.0" } +Response: { valid: true, reason: null } or { valid: false, reason: "outofdate" } +``` +Slot released on disconnect. Prevents backend overload. + +## Documentation Requirements (CRITICAL) + +**ALWAYS update docs when modifying workflows**: + +1. **Connection Changes** → Update `docs/CONNECTION_WORKFLOW.md` (steps, states, error handling) +2. **Ping/Auto-Ping Changes** → Update `docs/PING_WORKFLOW.md` (validation, lifecycle, UI impacts) +3. **New Status Messages** → Add to `docs/STATUS_MESSAGES.md` (exact text, trigger, color) +4. **Code Comments** → Use JSDoc (`@param`, `@returns`) for all functions +5. **Architecture/Pattern Changes** → Update `.github/copilot-instructions.md` (this file) to reflect new patterns, data flows, or critical gotchas + +### Status Message Documentation Format +```markdown +#### Message Name +- **Message**: `"Exact text shown"` +- **Color**: Green/Red/Yellow/Blue (success/error/warning/info) +- **When**: Detailed trigger condition +- **Source**: `content/wardrive.js:functionName()` +``` + +## Integration Points & External APIs + +### MeshMapper API (yow.meshmapper.net) +- **Capacity Check**: `capacitycheck.php` - Slot acquisition before connect +- **Wardrive Data**: `wardriving-api.php` - Batch POST TX/RX coverage blocks + - Payload: `[{type:"TX"|"RX", lat, lon, who, power, heard, session_id, iatacode}]` + - Auth: `apikey` in JSON body (NOT query string - see `docs/GEO_AUTH_DESIGN.md`) + +### MeshCore Protocol (content/mc/) +Key methods on `Connection` class: +- `deviceQuery(protoVer)` - Protocol handshake +- `getDeviceName()`, `getPublicKey()`, `getDeviceSettings()` - Device info +- `sendTime()` - Time sync +- `getChannels()`, `createChannel()`, `deleteChannel()` - Channel CRUD +- `sendChannelMsg(channel, text)` - Send text message to channel + +**Packet Structure**: Custom binary protocol with BufferReader/Writer utilities for serialization. + +## Common Pitfalls & Gotchas + +1. **Unified RX Handler accepts ALL packets** - No header filtering at entry point (removed in PR #130). Session log tracking filters headers internally. + +2. **GPS freshness varies by context**: Manual pings tolerate 60s old GPS data, auto pings force fresh acquisition. Check `GPS_WATCH_MAX_AGE_MS` vs `GPS_FRESHNESS_BUFFER_MS`. + +3. **Control locking during ping lifecycle** - `sendPing()` disables all controls until API post completes. Must call `unlockPingControls()` in ALL code paths (success/error). + +4. **Auto-ping pause/resume** - Manual pings during auto mode pause countdown, resume after completion. Handle in `handleManualPingBlockedDuringAutoMode()`. + +5. **Disconnect cleanup order matters**: Flush API queue → Release capacity → Delete channel → Close BLE → Clear timers/GPS/wake locks → Reset state. Out-of-order causes errors. + +6. **Tailwind config paths**: Build scans `index.html` and `content/**/*.{js,html}`. Missing paths = missing styles. + +7. **Status message visibility race** - Use `immediate=true` for countdown updates, `false` for first display (enforces 500ms minimum). + +## Code Style Conventions + +- **No frameworks/bundlers** - Vanilla JS with ES6 modules (`import`/`export`) +- **Functional > Classes** - Most code uses functions + closures (except mc/ library uses classes) +- **State centralization** - Global `state` object, explicit mutations +- **Constants at top** - All config in SCREAMING_SNAKE_CASE (intervals, URLs, thresholds) +- **Async/await** - Preferred over `.then()` chains +- **Error handling** - Wrap BLE/API calls in try-catch, log with `debugError()` + +### AI Prompt Formatting +When generating prompts for AI agents or subagents, always format them in markdown code blocks using four backticks (````markdown) to prevent accidentally closing the code block when the prompt itself contains triple backticks (```): + +````markdown +Example prompt for AI agent: +- Task description here +- Can safely include ```code examples``` without breaking the outer block +```` + +## Key Files Reference + +- `content/wardrive.js:connect()` (line ~2020) - 10-step connection workflow +- `content/wardrive.js:sendPing()` (line ~2211) - Ping validation & send logic +- `content/wardrive.js:handleUnifiedRxLogEvent()` (line ~3100) - RX packet handler +- `content/wardrive.js:flushApiQueue()` (line ~3800) - Batch API POST +- `content/mc/connection/connection.js` - MeshCore protocol implementation +- `docs/FLOW_WARDRIVE_API_QUEUE_DIAGRAM.md` - Visual API queue architecture +- `docs/COVERAGE_TYPES.md` - Coverage block definitions (BIDIR, TX, RX, DEAD, DROP) + +## Testing Approach + +**No automated tests** - Manual testing only: +1. **Syntax check**: `node -c content/wardrive.js` +2. **Desktop testing**: Primary development on Chrome/Chromium desktop + - Use Chrome DevTools Sensors tab for GPS simulation + - Mobile device emulation for responsive UI testing +3. **Debug logging**: Enable via `?debug=true` to trace workflows +4. **Production validation**: App primarily used on mobile (Android Chrome, iOS Bluefy) + - Desktop testing sufficient for most development + - Real device testing with MeshCore companion for final validation + +**Mobile-first app, desktop-tested workflow** - Most development happens on desktop with DevTools, but remember users are on phones with real GPS and BLE constraints. diff --git a/.gitignore b/.gitignore index c055d2b..ac0bca6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ content/config.js # Node modules (if added in the future) node_modules/ +# Package files +package.json +package-lock.json + # Build artifacts dist/ build/ @@ -22,3 +26,8 @@ Thumbs.db # Temporary files tmp/ test-log-ui.html + +# Test data files (local testing only) +test_single_packet.js +packet.json +*_packet.json diff --git a/CHANGES_SUMMARY.md b/CHANGES_SUMMARY.md deleted file mode 100644 index adaef00..0000000 --- a/CHANGES_SUMMARY.md +++ /dev/null @@ -1,228 +0,0 @@ -# wardrive.js Optimization - Changes Summary - -## Overview -This PR implements Phase 1 optimizations for `wardrive.js`, focusing on safe, minimal changes that improve code quality without introducing risk. - -## Objectives Completed ✅ - -### 1. Code Review & Analysis -- ✅ Performed comprehensive review of wardrive.js (4,324 lines) -- ✅ Identified dead code, syntax errors, and optimization opportunities -- ✅ Documented findings in OPTIMIZATION_REPORT.md -- ✅ Risk assessment for potential future optimizations - -### 2. Dead Code Elimination -- ✅ Removed deprecated `handlePassiveRxLogEvent()` function (77 lines) - - Was marked @deprecated and replaced by `handleUnifiedRxLogEvent()` - - No longer called anywhere in the codebase -- ✅ Removed deprecated alias functions (7 lines) - - `startPassiveRxListening()` → replaced with `startUnifiedRxListening()` - - `stopPassiveRxListening()` → replaced with `stopUnifiedRxListening()` -- ✅ Updated all function call references (2 locations) -- ✅ Cleaned up TODO comment on API endpoint - -### 3. Bug Fixes -- ✅ Fixed syntax error at line 4113 - - **Issue**: Missing comma in debugError call - - **Before**: `debugError("[UI] Connection button error:" backtick${e.message}backtick, e);` - - **After**: `debugError("[UI] Connection button error:", backtick${e.message}backtick, e);` - - **Impact**: Prevents potential runtime errors in error logging - -### 4. Code Structure Improvements -- ✅ Extracted magic number to named constant - - Added `previewLength: 20` to `errorLogState` object - - Updated usage in `updateErrorLogSummary()` function - - Improves maintainability and makes configuration explicit - -### 5. File Cleanup -- ✅ Deleted `index-new.html` (237 lines) - - File was not referenced or used anywhere in the application - - Only reference was in tailwind.config.js (now removed) -- ✅ Updated `tailwind.config.js` to remove deleted file reference - -## Files Changed - -### Modified Files -1. **content/wardrive.js** - - Before: 4,324 lines - - After: 4,234 lines - - Reduction: 90 lines (2.1%) - - Changes: - - Removed deprecated functions (84 lines) - - Fixed syntax error (1 line) - - Extracted constant (2 lines) - - Updated function calls (3 lines) - -2. **tailwind.config.js** - - Removed reference to index-new.html - - Impact: Cleaner build configuration - -### New Files -1. **OPTIMIZATION_REPORT.md** (212 lines) - - Comprehensive analysis of codebase - - Identified optimization opportunities - - Risk/benefit assessment - - Performance metrics - - Recommendations for future work - -2. **CHANGES_SUMMARY.md** (this file) - - Summary of all changes made - - Verification and testing details - -### Deleted Files -1. **index-new.html** (237 lines) - - Unused HTML file - - No references in codebase - -## Verification & Testing - -### Syntax Validation ✅ -```bash -node -c content/wardrive.js -# Result: No syntax errors -``` - -### Code Review ✅ -- First review: Identified 2 issues with function call references -- Fixed: Updated calls to removed deprecated functions -- Second review: No issues found - -### Impact Analysis ✅ -- **Breaking Changes**: None -- **Backward Compatibility**: Fully maintained -- **Feature Changes**: None - all features preserved -- **Performance Impact**: Neutral (file size reduction minimal) - -## Detailed Changes - -### Commit 1: Remove index-new.html and deprecated functions -``` -Files changed: 3 -- content/wardrive.js: -93 lines -- index-new.html: deleted (237 lines) -- tailwind.config.js: -1 line -Total: -331 lines -``` - -### Commit 2: Fix syntax error and extract magic number constant -``` -Files changed: 2 -- content/wardrive.js: +6/-5 lines -- OPTIMIZATION_REPORT.md: created (212 lines) -``` - -### Commit 3: Fix function call references -``` -Files changed: 1 -- content/wardrive.js: +3/-3 lines -``` - -## Metrics - -### Before Optimization -- Total lines: 4,324 -- Functions: ~90 -- Debug statements: 434 -- Deprecated code: 91 lines -- Syntax errors: 1 -- Magic numbers: Several - -### After Optimization -- Total lines: 4,234 (-90, -2.1%) -- Functions: ~87 (-3) -- Debug statements: 434 (unchanged) -- Deprecated code: 0 (-91 lines) -- Syntax errors: 0 (-1 fixed) -- Magic numbers: 1 fewer - -## Code Quality Assessment - -### Improvements Made ✅ -- Cleaner codebase with no deprecated code -- Fixed syntax error preventing potential runtime issues -- Better maintainability with named constants -- Comprehensive documentation for future work - -### Strengths Identified ✅ -- Well-structured code with clear separation of concerns -- Comprehensive debug logging with proper tags -- Good error handling and state management -- Proper memory limits preventing unbounded growth -- Efficient batch operations (API queue, RX batching) -- Comprehensive timer cleanup - -### Future Optimization Opportunities (Deferred) -The analysis identified several consolidation opportunities that were **intentionally deferred**: - -1. **Bottom Sheet Toggles** (~50 line savings, medium risk) - - 3 nearly identical functions with ~90% code similarity - - Could be consolidated with generic helper - - **Deferred**: Risk of UI breakage outweighs benefit - -2. **Render Functions** (~70 line savings, high risk) - - 3 similar rendering patterns - - Complex logic with subtle differences - - **Deferred**: Over-abstraction could reduce readability - -3. **CSV Exports** (~30 line savings, low risk) - - 3 similar export functions - - Different column formats - - **Deferred**: Current code is clear and maintainable - -**Rationale**: The codebase is well-maintained and readable. Aggressive consolidation would introduce complexity without meaningful performance gains. No performance issues were identified. - -## Guidelines Compliance ✅ - -All changes strictly follow the development guidelines: -- ✅ Maintained debug logging with proper tags -- ✅ Preserved existing functionality -- ✅ Only removed dead code and fixed bugs -- ✅ No modifications to working code -- ✅ Code quality improvements without breaking changes -- ✅ Minimal, surgical changes as required -- ✅ Comprehensive documentation - -## Testing Recommendations - -Since this is a browser-based PWA with no automated tests: - -### Manual Testing Checklist -1. **Connection Flow** - - [ ] BLE connection establishes successfully - - [ ] Unified RX listening starts after connection - - [ ] Unified RX listening stops on disconnect - -2. **Ping Operations** - - [ ] Manual ping sends successfully - - [ ] Auto ping mode works correctly - - [ ] GPS acquisition functions properly - -3. **UI Components** - - [ ] Session log displays correctly - - [ ] RX log displays correctly - - [ ] Error log displays correctly - - [ ] All log toggles work - - [ ] CSV export functions work - -4. **Error Handling** - - [ ] Error messages display correctly (syntax fix verification) - - [ ] Debug logging works with ?debug=true - - [ ] Error log preview shows correct length - -## Conclusion - -Phase 1 optimization successfully completed with **zero risk** changes: -- ✅ Removed 90 lines of dead code -- ✅ Fixed 1 syntax error -- ✅ Improved code maintainability -- ✅ Created comprehensive documentation -- ✅ Zero breaking changes -- ✅ Full backward compatibility - -The codebase is now cleaner, more maintainable, and free of technical debt while preserving all functionality. Future optimization opportunities have been documented but intentionally deferred based on risk/benefit analysis. - ---- -**Optimization Completed**: December 23, 2025 -**Total Time Saved**: ~2.1% file size reduction -**Risk Level**: Zero (only dead code removed and bugs fixed) -**Compatibility**: 100% maintained diff --git a/README.md b/README.md index 40ae7e1..40be602 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MeshCore GOME WarDriver -[![Version](https://img.shields.io/badge/version-1.6.0-blue.svg)](https://github.com/MrAlders0n/MeshCore-GOME-WarDriver/releases/tag/v1.6.0) +[![Version](https://img.shields.io/badge/version-1.7.0-blue.svg)](https://github.com/MrAlders0n/MeshCore-GOME-WarDriver/releases/tag/v1.7.0) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Platform](https://img.shields.io/badge/platform-Android%20%7C%20iOS-orange.svg)](#platform-support) 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 b63e4be..21527d3 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -64,19 +64,39 @@ const MIN_PAUSE_THRESHOLD_MS = 1000; // Minimum timer value (1 second) const MAX_REASONABLE_TIMER_MS = 5 * 60 * 1000; // Maximum reasonable timer value (5 minutes) to handle clock skew const RX_LOG_LISTEN_WINDOW_MS = 6000; // Listen window for repeater echoes (6 seconds) const CHANNEL_GROUP_TEXT_HEADER = 0x15; // Header byte for Meshtastic GroupText packets (0x15) - used exclusively for Session Log echo detection +const ADVERT_HEADER = 0x11; // Header byte for ADVERT packets (0x11) + +// RX Packet Filter Configuration +const MAX_RX_PATH_LENGTH = 9; // Maximum path length for RX packets (drop if exceeded to filter corrupted packets) +const RX_ALLOWED_CHANNELS = ['#wardriving', '#public', '#testing', '#ottawa']; // Allowed channels for RX wardriving +const RX_PRINTABLE_THRESHOLD = 0.80; // Minimum printable character ratio for GRP_TXT (80%) // Pre-computed channel hash and key for the wardriving channel // These will be computed once at startup and used for message correlation and decryption let WARDRIVING_CHANNEL_HASH = null; let WARDRIVING_CHANNEL_KEY = null; -// Initialize the wardriving channel hash and key at startup +// Pre-computed channel hashes and keys for all allowed RX channels +const RX_CHANNEL_MAP = new Map(); // Map + +// Initialize channel hashes and keys at startup (async function initializeChannelHash() { try { + // Initialize wardriving channel (for TX tracking) WARDRIVING_CHANNEL_KEY = await deriveChannelKey(CHANNEL_NAME); WARDRIVING_CHANNEL_HASH = await computeChannelHash(WARDRIVING_CHANNEL_KEY); debugLog(`[INIT] Wardriving channel hash pre-computed at startup: 0x${WARDRIVING_CHANNEL_HASH.toString(16).padStart(2, '0')}`); debugLog(`[INIT] Wardriving channel key cached for message decryption (${WARDRIVING_CHANNEL_KEY.length} bytes)`); + + // Initialize all allowed RX channels + debugLog(`[INIT] Pre-computing hashes/keys for ${RX_ALLOWED_CHANNELS.length} allowed RX channels...`); + for (const channelName of RX_ALLOWED_CHANNELS) { + const key = await deriveChannelKey(channelName); + const hash = await computeChannelHash(key); + RX_CHANNEL_MAP.set(hash, { name: channelName, key: key }); + debugLog(`[INIT] ${channelName} -> hash=0x${hash.toString(16).padStart(2, '0')}`); + } + debugLog(`[INIT] ✅ All RX channel hashes/keys initialized successfully`); } catch (error) { debugError(`[INIT] CRITICAL: Failed to pre-compute channel hash/key: ${error.message}`); debugError(`[INIT] Repeater echo tracking will be disabled. Please reload the page.`); @@ -141,13 +161,14 @@ 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 rxAutoBtn = $("rxAutoBtn"); 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 txPingsEl = document.getElementById("txPings"); // TX log container const coverageFrameEl = document.getElementById("coverageFrame"); setConnectButton(false); setConnStatus("Disconnected", STATUS_COLORS.error); @@ -156,14 +177,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 +210,7 @@ const errorLogExpandArrow = $("errorLogExpandArrow"); const errorLogCopyBtn = $("errorLogCopyBtn"); // Session log state -const sessionLogState = { +const txLogState = { entries: [], // Array of parsed log entries isExpanded: false, autoScroll: true @@ -198,6 +219,7 @@ const sessionLogState = { // RX log state (passive observations) const rxLogState = { entries: [], // Array of parsed RX log entries + dropCount: 0, // Count of dropped/filtered packets isExpanded: false, autoScroll: true, maxEntries: 100 // Limit to prevent memory issues @@ -217,7 +239,8 @@ const state = { connection: null, channel: null, autoTimerId: null, - running: false, + txRxAutoRunning: false, // TX/RX Auto mode flag (renamed from running) + rxAutoRunning: false, // RX Auto mode flag (passive-only wardriving) wakeLock: null, geoWatchId: null, lastFix: null, // { lat, lon, accM, tsMs } @@ -236,25 +259,28 @@ 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 - repeaterTracking: { - isListening: false, // Whether we're currently listening for echoes + pendingApiPosts: [], // Array of pending background API post promises + 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, // TRUE when unified listener is active (always on when connected) + isWardriving: false, // TRUE when TX/RX Auto OR RX Auto enabled + rxLogHandler: null, // Handler function for RX log events }, - rxBatchBuffer: new Map() // Map + rxBatchBuffer: new Map() // Map }; // API Batch Queue State @@ -410,7 +436,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 }; } @@ -512,11 +538,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(); } } @@ -561,9 +587,16 @@ function startCooldown() { 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; + 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 - permanently disabled (backend API not ready) + rxAutoBtn.disabled = true; } /** @@ -617,16 +650,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"); } @@ -642,14 +672,26 @@ 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"); + // Update TX/RX Auto button + if (state.txRxAutoRunning) { + 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 { - 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 = "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"); } } function buildCoverageEmbedUrl(lat, lon) { @@ -992,7 +1034,13 @@ function startGeoWatch() { }; state.gpsState = "acquired"; updateGpsUi(); - updateDistanceUi(); // Update distance when GPS position changes + updateDistanceUi(); + + // Update distance when GPS position changes + // NEW: Check RX batches for distance trigger when GPS position updates + if (state.rxTracking. isWardriving && state.rxBatchBuffer.size > 0) { + checkAllRxBatchesForDistanceTrigger({ lat: pos.coords. latitude, lon: pos.coords. longitude }); + } }, (err) => { debugError(`[GPS] GPS watch error: ${err.code} - ${err.message}`); @@ -1217,6 +1265,11 @@ function getCurrentPowerSetting() { return checkedPower ? checkedPower.value : ""; } +function getExternalAntennaSetting() { + const checkedAntenna = document.querySelector('input[name="externalAntenna"]:checked'); + return checkedAntenna ? checkedAntenna.value : ""; +} + function buildPayload(lat, lon) { const coordsStr = `${lat.toFixed(5)}, ${lon.toFixed(5)}`; const power = getCurrentPowerSetting(); @@ -1281,7 +1334,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 +1348,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 +1359,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 +1396,40 @@ 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 handleTxLogging or handleRxLogging + * @param {string} heardByte - The "heard" byte (first for TX, last for RX) as hex string + * @returns {Object} Debug data object + */ +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 + }; +} + /** * Post wardrive ping data to MeshMapper API * @param {number} lat - Latitude @@ -1358,6 +1456,7 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { lon, who: getDeviceIdentifier(), power: getCurrentPowerSetting(), + external_antenna: getExternalAntennaSetting(), heard_repeats: heardRepeats, ver: APP_VERSION, test: 0, @@ -1366,7 +1465,31 @@ 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.metadata) { + const heardByte = repeater.repeaterId; // First byte of path + const debugData = buildDebugData(repeater.metadata, heardByte, repeater.repeaterId); + 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", @@ -1429,6 +1552,12 @@ async function postApiInBackground(lat, lon, accuracy, heardRepeats) { debugLog("[API QUEUE] Starting 3-second delay before API POST"); await new Promise(resolve => setTimeout(resolve, 3000)); + // Check if we're still connected before posting (disconnect may have happened during delay) + if (!state.connection || !state.wardriveSessionId) { + debugLog("[API QUEUE] Skipping background API post - disconnected or no session_id"); + return; + } + debugLog("[API QUEUE] 3-second delay complete, posting to API"); try { await postToMeshMapperAPI(lat, lon, heardRepeats); @@ -1499,18 +1628,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() } } @@ -1702,7 +1831,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"); } } @@ -1742,6 +1871,58 @@ 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`); + + // Dump entire raw packet + const rawHex = Array.from(data. raw).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); + debugLog(`[RX PARSE] RAW Packet: ${rawHex}`); + + // Extract header byte from raw[0] + const header = data.raw[0]; + const routeType = header & 0x03; + + // Calculate offset for Path Length based on route type + let pathLengthOffset = 1; + if (routeType === 0x00 || routeType === 0x03) { // TransportFlood or TransportDirect + pathLengthOffset = 5; // Skip 4-byte transport codes + } + + // Extract path length from calculated offset + const pathLength = data.raw[pathLengthOffset]; + + // Path data starts after path length byte + const pathDataOffset = pathLengthOffset + 1; + const pathBytes = Array.from(data.raw.slice(pathDataOffset, pathDataOffset + pathLength)); + + // Derive first and last hops + const firstHop = pathBytes. length > 0 ? pathBytes[0] : null; + const lastHop = pathBytes.length > 0 ? pathBytes[pathLength - 1] : null; + + // Extract encrypted payload after path data + const encryptedPayload = data.raw.slice(pathDataOffset + 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, + header: header, + pathLength: pathLength, + pathBytes: pathBytes, + firstHop: firstHop, + lastHop: lastHop, + snr: data.lastSnr, + rssi: data.lastRssi, + encryptedPayload: encryptedPayload + }; +} + /** * Decrypt GroupText payload and extract message text * Payload structure: [1 byte channel_hash][2 bytes MAC][encrypted data] @@ -1857,13 +2038,184 @@ async function decryptGroupTextPayload(payload, channelKey) { } } +/** + * Check if a string is printable ASCII (basic ASCII only, no extended chars) + * @param {string} str - String to check + * @returns {boolean} True if all characters are printable ASCII (32-126) + */ +function isStrictAscii(str) { + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + if (code < 32 || code > 126) { + return false; + } + } + return true; +} + +/** + * Calculate ratio of printable characters in a string + * @param {string} str - String to analyze + * @returns {number} Ratio of printable chars (0.0 to 1.0) + */ +function getPrintableRatio(str) { + if (str.length === 0) return 0; + let printableCount = 0; + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + // Printable: ASCII 32-126 or common whitespace (9=tab, 10=newline, 13=CR) + if ((code >= 32 && code <= 126) || code === 9 || code === 10 || code === 13) { + printableCount++; + } + } + return printableCount / str.length; +} + +/** + * Parse and validate ADVERT packet name field + * @param {Uint8Array} payload - Encrypted payload from metadata + * @returns {Object} {valid: boolean, name: string, reason: string} + */ +function parseAdvertName(payload) { + try { + // ADVERT structure: [32 bytes pubkey][4 bytes timestamp][64 bytes signature][1 byte flags][name...] + const PUBKEY_SIZE = 32; + const TIMESTAMP_SIZE = 4; + const SIGNATURE_SIZE = 64; + const FLAGS_SIZE = 1; + const NAME_OFFSET = PUBKEY_SIZE + TIMESTAMP_SIZE + SIGNATURE_SIZE + FLAGS_SIZE; + + if (payload.length <= NAME_OFFSET) { + return { valid: false, name: '', reason: 'payload too short for name' }; + } + + const nameBytes = payload.slice(NAME_OFFSET); + const decoder = new TextDecoder('utf-8', { fatal: false }); + const name = decoder.decode(nameBytes).replace(/\0+$/, '').trim(); + + debugLog(`[RX FILTER] ADVERT name extracted: "${name}" (${name.length} chars)`); + + if (name.length === 0) { + return { valid: false, name: '', reason: 'name empty' }; + } + + // Check if name is printable + const printableRatio = getPrintableRatio(name); + debugLog(`[RX FILTER] ADVERT name printable ratio: ${(printableRatio * 100).toFixed(1)}%`); + + if (printableRatio < 0.9) { + return { valid: false, name: name, reason: 'name not printable' }; + } + + // Check strict ASCII (no extended characters) + if (!isStrictAscii(name)) { + return { valid: false, name: name, reason: 'name contains non-ASCII chars' }; + } + + return { valid: true, name: name, reason: 'kept' }; + + } catch (error) { + debugError(`[RX FILTER] Error parsing ADVERT name: ${error.message}`); + return { valid: false, name: '', reason: 'parse error' }; + } +} + +/** + * Validate RX packet for wardriving logging + * @param {Object} metadata - Parsed metadata from parseRxPacketMetadata() + * @returns {Promise} {valid: boolean, reason: string, channelName?: string, plaintext?: string} + */ +async function validateRxPacket(metadata) { + try { + // Log raw packet for debugging + const rawHex = Array.from(metadata.raw).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); + debugLog(`[RX FILTER] ========== VALIDATING PACKET ==========`); + debugLog(`[RX FILTER] Raw packet (${metadata.raw.length} bytes): ${rawHex}`); + debugLog(`[RX FILTER] Header: 0x${metadata.header.toString(16).padStart(2, '0')} | PathLength: ${metadata.pathLength} | SNR: ${metadata.snr}`); + + // VALIDATION 1: Check path length + if (metadata.pathLength > MAX_RX_PATH_LENGTH) { + debugLog(`[RX FILTER] ❌ DROPPED: pathLen>${MAX_RX_PATH_LENGTH} (${metadata.pathLength} hops)`); + return { valid: false, reason: `pathLen>${MAX_RX_PATH_LENGTH}` }; + } + debugLog(`[RX FILTER] ✓ Path length OK (${metadata.pathLength} ≤ ${MAX_RX_PATH_LENGTH})`); + + // VALIDATION 2: Check packet type (only ADVERT and GRP_TXT) + if (metadata.header === CHANNEL_GROUP_TEXT_HEADER) { + debugLog(`[RX FILTER] Packet type: GRP_TXT (0x15)`); + + // GRP_TXT validation + if (metadata.encryptedPayload.length < 3) { + debugLog(`[RX FILTER] ❌ DROPPED: GRP_TXT payload too short (${metadata.encryptedPayload.length} bytes)`); + return { valid: false, reason: 'payload too short' }; + } + + const channelHash = metadata.encryptedPayload[0]; + debugLog(`[RX FILTER] Channel hash: 0x${channelHash.toString(16).padStart(2, '0')}`); + + // Check if channel is in allowed list + const channelInfo = RX_CHANNEL_MAP.get(channelHash); + if (!channelInfo) { + debugLog(`[RX FILTER] ❌ DROPPED: Unknown channel hash 0x${channelHash.toString(16).padStart(2, '0')}`); + return { valid: false, reason: 'unknown channel hash' }; + } + + debugLog(`[RX FILTER] ✓ Channel matched: ${channelInfo.name}`); + + // Decrypt message + const plaintext = await decryptGroupTextPayload(metadata.encryptedPayload, channelInfo.key); + if (!plaintext) { + debugLog(`[RX FILTER] ❌ DROPPED: Decryption failed`); + return { valid: false, reason: 'decrypt failed' }; + } + + debugLog(`[RX FILTER] Decrypted message (${plaintext.length} chars): "${plaintext.substring(0, 60)}${plaintext.length > 60 ? '...' : ''}"}`); + + // Check printable ratio + const printableRatio = getPrintableRatio(plaintext); + debugLog(`[RX FILTER] Printable ratio: ${(printableRatio * 100).toFixed(1)}% (threshold: ${(RX_PRINTABLE_THRESHOLD * 100).toFixed(1)}%)`); + + if (printableRatio < RX_PRINTABLE_THRESHOLD) { + debugLog(`[RX FILTER] ❌ DROPPED: plaintext not printable`); + return { valid: false, reason: 'plaintext not printable' }; + } + + debugLog(`[RX FILTER] ✅ KEPT: GRP_TXT passed all validations`); + return { valid: true, reason: 'kept', channelName: channelInfo.name, plaintext: plaintext }; + + } else if (metadata.header === ADVERT_HEADER) { + debugLog(`[RX FILTER] Packet type: ADVERT (0x11)`); + + // ADVERT validation + const nameResult = parseAdvertName(metadata.encryptedPayload); + + if (!nameResult.valid) { + debugLog(`[RX FILTER] ❌ DROPPED: ${nameResult.reason}`); + return { valid: false, reason: nameResult.reason }; + } + + debugLog(`[RX FILTER] ✅ KEPT: ADVERT passed all validations (name="${nameResult.name}")`); + return { valid: true, reason: 'kept' }; + + } else { + // Unsupported packet type + debugLog(`[RX FILTER] ❌ DROPPED: unsupported ptype (header=0x${metadata.header.toString(16).padStart(2, '0')})`); + return { valid: false, reason: 'unsupported ptype' }; + } + + } catch (error) { + debugError(`[RX FILTER] ❌ Validation error: ${error.message}`); + return { valid: false, reason: 'validation error' }; + } +} + /** * Start listening for repeater echoes via rx_log * Uses the pre-computed WARDRIVING_CHANNEL_HASH for message correlation * @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()}`); @@ -1874,63 +2226,63 @@ 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) } /** * 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) { - 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=${data.lastSnr}, RSSI=${data.lastRssi}`); + 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 (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(`[TX 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(`[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 (packet.payload.length < 3) { - debugLog(`[SESSION LOG] Ignoring: payload too short to contain channel hash`); + if (metadata.encryptedPayload.length < 3) { + debugLog(`[TX LOG] Ignoring: payload too short to contain channel hash`); return false; } - const packetChannelHash = packet.payload[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')}`); + const packetChannelHash = metadata.encryptedPayload[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 @@ -1938,7 +2290,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`); @@ -1973,8 +2325,8 @@ 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) { - debugLog(`[SESSION LOG] Ignoring: no path (direct transmission, not a repeater echo)`); + if (metadata.pathLength === 0) { + debugLog(`[TX LOG] Ignoring: no path (direct transmission, not a repeater echo)`); return false; } @@ -1982,49 +2334,51 @@ 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})`); + 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 (data.lastSnr > existing.snr) { - 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 + if (metadata.snr > existing.snr) { + debugLog(`[PING] Deduplication decision: updating path ${pathHex} with better SNR: ${existing.snr} -> ${metadata.snr}`); + 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 ${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}`); - state.repeaterTracking.repeaters.set(pathHex, { - snr: data.lastSnr, - seenCount: 1 + debugLog(`[PING] Adding new repeater echo: path=${pathHex}, SNR=${metadata.snr}`); + 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; } } @@ -2033,8 +2387,8 @@ async function handleSessionLogTracking(packet, 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 []; } @@ -2043,10 +2397,11 @@ function stopRepeaterTracking() { // No need to unregister handler - unified handler continues running // Just clear the tracking state - // Get the results - const repeaters = Array.from(state.repeaterTracking.repeaters.entries()).map(([id, data]) => ({ + // Get the results with full data (including metadata for debug mode) + const repeaters = Array.from(state.txTracking.repeaters.entries()).map(([id, data]) => ({ repeaterId: id, - snr: data.snr + snr: data.snr, + metadata: data.metadata // Include metadata for debug mode })); // Sort by repeater ID for deterministic output @@ -2055,12 +2410,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; } @@ -2089,107 +2444,137 @@ function formatRepeaterTelemetry(repeaters) { */ async function handleUnifiedRxLogEvent(data) { try { - debugLog(`[UNIFIED RX] Received rx_log entry: SNR=${data.lastSnr}, RSSI=${data.lastRssi}`); + // 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 the packet from raw data (once for both handlers) - const packet = Packet.fromBytes(data.raw); + // Parse metadata ONCE + const metadata = parseRxPacketMetadata(data); - // Log header for debugging (informational for all packet processing) - debugLog(`[UNIFIED RX] Packet header: 0x${packet.header.toString(16).padStart(2, '0')}`); + debugLog(`[UNIFIED RX] Packet received: header=0x${metadata.header.toString(16)}, pathLength=${metadata.pathLength}`); - // 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 - 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 + // 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; } - - 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 (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.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 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 (packet.path.length === 0) { - debugLog(`[PASSIVE RX] Ignoring: no path (direct transmission, not via repeater)`); + if (metadata.pathLength === 0) { + rxLogState.dropCount++; + updateRxLogSummary(); + debugLog(`[RX LOG] 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 repeaterId = lastHopId.toString(16).padStart(2, '0'); - - debugLog(`[PASSIVE RX] Packet heard via last hop: ${repeaterId}, SNR=${data.lastSnr}, path_length=${packet.path.length}`); - - // Get current GPS location + // Get current GPS location (must have GPS before further validation) if (!state.lastFix) { - debugLog(`[PASSIVE RX] No GPS fix available, skipping entry`); + rxLogState.dropCount++; + updateRxLogSummary(); + debugLog(`[RX LOG] No GPS fix available, skipping entry`); + return; + } + + // PACKET FILTER: Validate packet before logging + const validation = await validateRxPacket(metadata); + if (!validation.valid) { + rxLogState.dropCount++; + updateRxLogSummary(); + const rawHex = Array.from(metadata.raw).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); + debugLog(`[RX LOG] ❌ Packet dropped: ${validation.reason}`); + debugLog(`[RX LOG] Dropped packet hex: ${rawHex}`); return; } + // Extract LAST hop from path (the repeater that directly delivered to us) + const lastHopId = metadata.lastHop; + const repeaterId = lastHopId.toString(16).padStart(2, '0'); + + debugLog(`[RX LOG] Packet heard via last hop: ${repeaterId}, SNR=${metadata.snr}, path_length=${metadata.pathLength}`); + debugLog(`[RX LOG] ✅ Packet validated and passed filter`); + const lat = state.lastFix.lat; const lon = state.lastFix.lon; 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)}`); + debugLog(`[RX LOG] ✅ Observation logged: repeater=${repeaterId}, snr=${metadata.snr}, location=${lat.toFixed(5)},${lon.toFixed(5)}`); - // Handle batch tracking for API (parallel batch per repeater) - handlePassiveRxForAPI(repeaterId, data.lastSnr, { lat, lon }); + // Handle tracking for API (best SNR with distance trigger) + handleRxBatching( + repeaterId, + metadata.snr, + metadata.rssi, + metadata.pathLength, + metadata.header, + { lat, lon }, + metadata + ); } catch (error) { - debugError(`[PASSIVE RX] Error processing passive RX: ${error.message}`, error); + debugError(`[RX LOG] Error processing passive RX: ${error.message}`, error); } } /** - * Start unified RX listening - handles both Session Log tracking and passive RX logging + * Start unified RX listening - handles both TX Log tracking and RX logging + * Idempotent: safe to call multiple times */ function startUnifiedRxListening() { - if (state.passiveRxTracking.isListening) { + // 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 listening: no connection`); + debugWarn(`[UNIFIED RX] Cannot start: no connection`); return; } 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`); } @@ -2198,19 +2583,20 @@ 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.isWardriving = false; // Also disable wardriving + state.rxTracking.rxLogHandler = null; debugLog(`[UNIFIED RX] ✅ Unified listening stopped`); } @@ -2223,7 +2609,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; } @@ -2231,161 +2617,192 @@ 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 ---- /** * 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} metadata - Parsed metadata 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 handleRxBatching(repeaterId, snr, rssi, pathLength, header, currentLocation, metadata) { + // 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(), + metadata: metadata // Store full metadata for debug mode + } }; - 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(), + metadata: metadata // Store full metadata for debug mode + }; + } 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 - * @param {string} repeaterId - Repeater ID to flush - * @param {string} trigger - What caused the flush: 'distance' | 'timeout' | 'session_end' + * Check all active RX batches for distance threshold on GPS position update + * Called from GPS watch callback when position changes + * @param {Object} currentLocation - Current GPS location {lat, lon} */ -function flushBatch(repeaterId, trigger) { - debugLog(`[RX BATCH] Flushing batch for repeater ${repeaterId}, trigger=${trigger}`); +function checkAllRxBatchesForDistanceTrigger(currentLocation) { + if (state.rxBatchBuffer.size === 0) { + return; // No active batches to check + } - const batch = state.rxBatchBuffer.get(repeaterId); - if (!batch || batch.samples.length === 0) { - debugLog(`[RX BATCH] No batch to flush for repeater ${repeaterId}`); - return; + debugLog(`[RX BATCH] GPS position updated, checking ${state.rxBatchBuffer. size} active batches for distance trigger`); + + const repeatersToFlush = []; + + // Check each active batch + for (const [repeaterId, buffer] of state.rxBatchBuffer. entries()) { + const distance = calculateHaversineDistance( + currentLocation.lat, + currentLocation.lon, + buffer.firstLocation.lat, + buffer.firstLocation.lng + ); + + 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}, marking for flush`); + repeatersToFlush.push(repeaterId); + } } - // Clear timeout if it exists - if (batch.timeoutId) { - clearTimeout(batch.timeoutId); - debugLog(`[RX BATCH] Cleared timeout for repeater ${repeaterId}`); + // Flush all repeaters that met the distance threshold + for (const repeaterId of repeatersToFlush) { + flushRepeater(repeaterId); } - // 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; + if (repeatersToFlush.length > 0) { + debugLog(`[RX BATCH] Flushed ${repeatersToFlush.length} repeater(s) due to GPS movement`); + } +} + +/** + * Flush a single repeater's batch - post best observation to API + * @param {string} repeaterId - Repeater ID to flush + */ +function flushRepeater(repeaterId) { + debugLog(`[RX BATCH] Flushing repeater ${repeaterId}`); + + const buffer = state.rxBatchBuffer.get(repeaterId); + if (!buffer) { + debugLog(`[RX BATCH] No buffer to flush for repeater ${repeaterId}`); + return; + } - // Build API entry + const best = buffer.bestObservation; + + // 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, + metadata: best.metadata // For 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); + queueRxApiPost(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`); } /** * 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') { - debugLog(`[RX BATCH] Flushing all batches, trigger=${trigger}, active_batches=${state.rxBatchBuffer.size}`); +function flushAllRxBatches(trigger = 'session_end') { + 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) { +function queueRxApiPost(entry) { // Validate session_id exists if (!state.wardriveSessionId) { debugWarn(`[RX BATCH API] Cannot queue: no session_id available`); 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, @@ -2393,6 +2810,7 @@ function queueApiPost(entry) { lon: entry.location.lng, who: getDeviceIdentifier(), power: getCurrentPowerSetting(), + external_antenna: getExternalAntennaSetting(), heard_repeats: heardRepeats, ver: APP_VERSION, test: 0, @@ -2400,9 +2818,23 @@ function queueApiPost(entry) { session_id: state.wardriveSessionId }; + // Add debug data if debug mode is enabled + if (state.debugMode && entry.metadata) { + debugLog(`[RX BATCH API] 🐛 Debug mode active - adding debug_data for RX`); + + // For RX, parsed_heard is the LAST byte of path + 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; + + 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 ---- @@ -2542,67 +2974,67 @@ 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() { - if (!sessionPingsEl) return; +function renderTxLogEntries() { + if (!txPingsEl) return; - debugLog(`[UI] Rendering ${sessionLogState.entries.length} log entries`); - sessionPingsEl.innerHTML = ''; + debugLog(`[UI] Rendering ${txLogState.entries.length} log entries`); + txPingsEl.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'; placeholder.textContent = 'No pings logged yet'; - sessionPingsEl.appendChild(placeholder); + txPingsEl.appendChild(placeholder); debugLog(`[UI] Rendered placeholder (no entries)`); return; } // Render newest first - const entries = [...sessionLogState.entries].reverse(); + const entries = [...txLogState.entries].reverse(); entries.forEach((entry, index) => { const element = createLogEntryElement(entry); - sessionPingsEl.appendChild(element); - debugLog(`[UI] Appended log entry ${index + 1}/${entries.length} to sessionPingsEl`); + txPingsEl.appendChild(element); + debugLog(`[UI] Appended log entry ${index + 1}/${entries.length} to txPingsEl`); }); // 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`); } @@ -2612,23 +3044,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'); @@ -2636,16 +3068,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'); } } @@ -2656,14 +3088,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(); } } @@ -2735,7 +3167,9 @@ function updateRxLogSummary() { if (!rxLogCount || !rxLogLastTime || !rxLogLastRepeater) return; const count = rxLogState.entries.length; - rxLogCount.textContent = count === 1 ? '1 observation' : `${count} observations`; + const dropText = `${rxLogState.dropCount} dropped`; + const obsText = count === 1 ? '1 observation' : `${count} observations`; + rxLogCount.textContent = `${obsText}, ${dropText}`; if (count === 0) { rxLogLastTime.textContent = 'No data'; @@ -3119,11 +3553,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'; } @@ -3131,7 +3565,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 @@ -3149,7 +3583,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; } @@ -3222,12 +3656,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(); @@ -3300,7 +3734,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; } @@ -3387,7 +3821,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(); @@ -3405,7 +3839,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; } @@ -3415,13 +3849,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 ); @@ -3431,9 +3865,9 @@ function updatePingLogWithRepeaters(logData, repeaters) { const updatedEntry = parseLogEntry(logLine); if (updatedEntry) { - sessionLogState.entries[entryIndex] = updatedEntry; - renderLogEntries(); - updateLogSummary(); + txLogState.entries[entryIndex] = updatedEntry; + renderTxLogEntries(); + updateTxLogSummary(); } } @@ -3444,16 +3878,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 })); @@ -3461,8 +3895,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`); } @@ -3483,12 +3917,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); @@ -3498,11 +3932,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 @@ -3527,7 +3961,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(); } @@ -3549,7 +3983,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(); } @@ -3577,7 +4011,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}`); @@ -3597,10 +4031,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() @@ -3620,28 +4054,34 @@ 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); 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) { - 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)"); @@ -3658,12 +4098,21 @@ async function sendPing(manual = false) { const { lat: apiLat, lon: apiLon, accuracy: apiAccuracy } = capturedCoords; debugLog(`[API QUEUE] Backgrounding API post for coordinates: lat=${apiLat.toFixed(5)}, lon=${apiLon.toFixed(5)}, accuracy=${apiAccuracy}m`); - // Post to API in background (async, fire-and-forget with error handling) - postApiInBackground(apiLat, apiLon, apiAccuracy, heardRepeatsStr).catch(error => { + // Post to API in background and track the promise + const apiPromise = postApiInBackground(apiLat, apiLon, apiAccuracy, heardRepeatsStr).catch(error => { debugError(`[API QUEUE] Background API post failed: ${error.message}`, error); // Show error to user only if API fails setDynamicStatus("Error: API post failed", STATUS_COLORS.error); + }).finally(() => { + // Remove from pending list when complete + const index = state.pendingApiPosts.indexOf(apiPromise); + if (index > -1) { + state.pendingApiPosts.splice(index, 1); + } }); + + // Track this promise so disconnect can wait for it + state.pendingApiPosts.push(apiPromise); } else { // This should never happen as coordinates are always captured before ping debugError(`[API QUEUE] CRITICAL: No captured ping coordinates available for API post - this indicates a logic error`); @@ -3687,17 +4136,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)`); - setDynamicStatus(`Wait ${remainingSec}s before toggling auto mode`, STATUS_COLORS.warning); + debugLog(`[TX/RX AUTO] Auto ping stop blocked by cooldown (${remainingSec}s remaining)`); + setDynamicStatus(`Wait ${remainingSec}s before toggling TX/RX Auto`, 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; } @@ -3707,43 +4156,115 @@ function stopAutoPing(stopGps = false) { 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(); + // Only stop GPS watch when disconnecting or page hidden, not during normal stop if (stopGps) { stopGeoWatch(); } - state.running = false; + state.txRxAutoRunning = false; + updateAutoButton(); + updateControlsForCooldown(); // Re-enable RX Auto button + releaseWakeLock(); + debugLog("[TX/RX AUTO] TX/RX Auto stopped"); +} + +/** + * Start RX Auto mode (passive-only wardriving) + */ +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"); +} + +/** + * Stop RX Auto mode + */ +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(); - debugLog("[AUTO] Auto ping stopped"); + + setDynamicStatus("RX Auto stopped", STATUS_COLORS.idle); + debugLog("[RX AUTO] RX Auto mode 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; } @@ -3751,14 +4272,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; } @@ -3767,19 +4288,30 @@ function startAutoPing() { // Clear any previous skip reason state.skipReason = null; + // Defensive check: ensure unified listener is running + if (state.connection && !state.rxTracking.isListening) { + debugWarn("[TX/RX AUTO] Unified listener not active - restarting"); + startUnifiedRxListening(); + } + + // ENABLE RX wardriving + state.rxTracking.isWardriving = true; + debugLog("[TX/RX AUTO] RX wardriving enabled"); + // 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(); + updateControlsForCooldown(); // Disable RX Auto button // 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); } @@ -3860,6 +4392,23 @@ async function connect() { // Start unified RX listening after channel setup startUnifiedRxListening(); + debugLog("[BLE] Unified RX listener started on connect"); + + // CLEAR all logs on connect (new session) + txLogState.entries = []; + renderTxLogEntries(true); + updateTxLogSummary(); + + rxLogState.entries = []; + rxLogState.dropCount = 0; + renderRxLogEntries(true); + updateRxLogSummary(); + + errorLogState.entries = []; + renderErrorLogEntries(true); + updateErrorLogSummary(); + + debugLog("[BLE] All logs cleared on connect (new session)"); // GPS initialization setDynamicStatus("Priming GPS", STATUS_COLORS.info); @@ -3869,6 +4418,10 @@ async function connect() { // Connection complete, show Connected status in connection bar setConnStatus("Connected", STATUS_COLORS.success); setDynamicStatus("Idle"); // Clear dynamic status to em dash + + // Lock wardrive settings after successful connection + lockWardriveSettings(); + debugLog("[BLE] Full connection process completed successfully"); } catch (e) { debugError(`[CHANNEL] Channel setup failed: ${e.message}`, e); @@ -3939,19 +4492,31 @@ 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 - stopAutoPing(true); // Ignore cooldown check on disconnect + + // Unlock wardrive settings after disconnect + unlockWardriveSettings(); + + // Stop auto modes + stopAutoPing(true); // Ignore cooldown check on disconnect, stop GPS + stopRxAuto(); // Stop RX Auto mode + enableControls(false); updateAutoButton(); stopGeoWatch(); stopGpsAgeUpdater(); // Ensure age updater stops - stopRepeaterTracking(); // Stop repeater echo tracking - stopUnifiedRxListening(); // Stop unified RX listening + stopTxTracking(); // Stop TX echo tracking + + // Stop unified RX listening on disconnect + stopUnifiedRxListening(); + debugLog("[BLE] Unified RX listener stopped on disconnect"); // Flush all pending RX batch data before cleanup - flushAllBatches('disconnect'); + flushAllRxBatches('disconnect'); // Clear API queue messages (timers already stopped in cleanupAllTimers) apiQueue.messages = []; @@ -3999,14 +4564,22 @@ async function disconnect() { setConnStatus("Disconnecting", STATUS_COLORS.info); setDynamicStatus("Idle"); // Clear dynamic status - // 1. CRITICAL: Flush API queue FIRST (session_id still valid) + // 1. CRITICAL: Wait for pending background API posts (session_id still valid) + if (state.pendingApiPosts.length > 0) { + debugLog(`[BLE] Waiting for ${state.pendingApiPosts.length} pending background API posts to complete`); + await Promise.allSettled(state.pendingApiPosts); + state.pendingApiPosts = []; + debugLog(`[BLE] All pending background API posts completed`); + } + + // 2. Flush API queue (session_id still valid) if (apiQueue.messages.length > 0) { debugLog(`[BLE] Flushing ${apiQueue.messages.length} queued messages before disconnect`); await flushApiQueue(); } stopFlushTimers(); - // 2. THEN release capacity slot if we have a public key + // 3. THEN release capacity slot if we have a public key if (state.devicePublicKey) { try { debugLog("[BLE] Releasing capacity slot"); @@ -4017,7 +4590,7 @@ async function disconnect() { } } - // 3. Delete the wardriving channel before disconnecting + // 4. Delete the wardriving channel before disconnecting try { if (state.channel && typeof state.connection.deleteChannel === "function") { debugLog(`[BLE] Deleting channel ${CHANNEL_NAME} at index ${state.channel.channelIdx}`); @@ -4058,42 +4631,129 @@ async function disconnect() { document.addEventListener("visibilitychange", async () => { if (document.hidden) { debugLog("[UI] Page visibility changed to hidden"); - if (state.running) { - 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); - } else { + + // 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"); - // On visible again, user can manually re-start Auto. + + // 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 } }); /** - * Update Connect button state based on radio power selection + * Update Connect button state based on radio power and external antenna selection */ function updateConnectButtonState() { const radioPowerSelected = getCurrentPowerSetting() !== ""; + const externalAntennaSelected = getExternalAntennaSetting() !== ""; const isConnected = !!state.connection; if (!isConnected) { - // Only enable Connect if radio power is selected - connectBtn.disabled = !radioPowerSelected; + // Only enable Connect if both settings are selected + connectBtn.disabled = !radioPowerSelected || !externalAntennaSelected; - // Update dynamic status based on power selection - if (!radioPowerSelected) { + // Update dynamic status based on selections + if (!radioPowerSelected && !externalAntennaSelected) { + debugLog("[UI] Radio power and external antenna not selected - showing message in status bar"); + setDynamicStatus("Select radio power and external antenna to connect", STATUS_COLORS.warning); + } else if (!radioPowerSelected) { debugLog("[UI] Radio power not selected - showing message in status bar"); setDynamicStatus("Select radio power to connect", STATUS_COLORS.warning); + } else if (!externalAntennaSelected) { + debugLog("[UI] External antenna not selected - showing message in status bar"); + setDynamicStatus("Select external antenna to connect", STATUS_COLORS.warning); } else { - debugLog("[UI] Radio power selected - clearing message from status bar"); + debugLog("[UI] Radio power and external antenna selected - clearing message from status bar"); setDynamicStatus("Idle"); } } } +/** + * Lock wardrive settings (Radio Power and External Antenna) after connection + */ +function lockWardriveSettings() { + debugLog("[UI] Locking wardrive settings (power and external antenna)"); + + // Lock all radio power inputs and labels + const powerInputs = document.querySelectorAll('input[name="power"]'); + powerInputs.forEach(input => { + input.disabled = true; + const label = input.closest("label"); + if (label) { + label.classList.add("cursor-not-allowed", "pointer-events-none"); + label.style.opacity = "0.5"; + } + }); + + // Lock all external antenna inputs and labels + const antennaInputs = document.querySelectorAll('input[name="externalAntenna"]'); + antennaInputs.forEach(input => { + input.disabled = true; + const label = input.closest("label"); + if (label) { + label.classList.add("cursor-not-allowed", "pointer-events-none"); + label.style.opacity = "0.5"; + } + }); +} + +/** + * Unlock wardrive settings (Radio Power and External Antenna) after disconnect + */ +function unlockWardriveSettings() { + debugLog("[UI] Unlocking wardrive settings (power and external antenna)"); + + // Unlock all radio power inputs and labels + const powerInputs = document.querySelectorAll('input[name="power"]'); + powerInputs.forEach(input => { + input.disabled = false; + const label = input.closest("label"); + if (label) { + label.classList.remove("cursor-not-allowed", "pointer-events-none"); + label.style.opacity = ""; + } + }); + + // Unlock all external antenna inputs and labels + const antennaInputs = document.querySelectorAll('input[name="externalAntenna"]'); + antennaInputs.forEach(input => { + input.disabled = false; + const label = input.closest("label"); + if (label) { + label.classList.remove("cursor-not-allowed", "pointer-events-none"); + label.style.opacity = ""; + } + }); +} + // ---- Bind UI & init ---- export async function onLoad() { debugLog("[INIT] wardrive.js onLoad() called - initializing"); @@ -4101,6 +4761,11 @@ export async function onLoad() { enableControls(false); updateAutoButton(); + // Disable RX Auto button (backend API not ready) + rxAutoBtn.disabled = true; + rxAutoBtn.title = "RX Auto temporarily disabled - backend API not ready"; + debugLog("[INIT] RX Auto button disabled - backend API not ready"); + // Initialize Connect button state based on radio power updateConnectButtonState(); @@ -4116,19 +4781,29 @@ 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 { startAutoPing(); } }); + + // NEW: RX Auto button listener + rxAutoBtn.addEventListener("click", () => { + debugLog("[UI] RX Auto button clicked"); + if (state.rxAutoRunning) { + stopRxAuto(); + } else { + startRxAuto(); + } + }); // Settings panel toggle (for modernized UI) const settingsGearBtn = document.getElementById("settingsGearBtn"); @@ -4174,11 +4849,20 @@ export async function onLoad() { }); }); + // Add event listeners to external antenna options to update Connect button state + const antennaRadios = document.querySelectorAll('input[name="externalAntenna"]'); + antennaRadios.forEach(radio => { + radio.addEventListener("change", () => { + debugLog(`[UI] External antenna changed to: ${getExternalAntennaSetting()}`); + updateConnectButtonState(); + }); + }); + // 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(); }); } @@ -4199,11 +4883,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/docs/CONNECTION_WORKFLOW.md b/docs/CONNECTION_WORKFLOW.md index 3155ad0..875674b 100644 --- a/docs/CONNECTION_WORKFLOW.md +++ b/docs/CONNECTION_WORKFLOW.md @@ -303,7 +303,7 @@ See `content/wardrive.js` for the main `disconnect()` function. - Stops GPS watch - Stops GPS age updater - Stops distance updater - - Stops repeater tracking + - Stops TX tracking - **Stops passive RX listening** (unregisters LogRxData handler) - **Clears API queue messages** (timers already stopped) - Clears all timers (see `cleanupAllTimers()`) @@ -530,7 +530,7 @@ stateDiagram-v2 - **Timer cleanup**: `wardrive.js:cleanupAllTimers()` (lines 427-460) - **Auto-ping stop**: `wardrive.js:stopAutoPing()` (lines 1904-1934) - **GPS watch stop**: `wardrive.js:stopGeoWatch()` (lines 793-803) -- **Repeater stop**: `wardrive.js:stopRepeaterTracking()` (lines 1506-1544) +- **Repeater stop**: `wardrive.js:stopTxTracking()` (lines 1506-1544) - **Channel delete**: `connection.js:deleteChannel()` (lines 1909-1911) ### State Management @@ -653,7 +653,7 @@ The ping/repeat listener flow manages the complete lifecycle of a wardrive ping │ - Listen for repeater echoes │ │ - Show "Listening for heard repeats (Xs)" countdown │ │ - Track all repeaters that forward the ping │ -│ - Update session log in real-time │ +│ - Update TX log in real-time │ └──────────────────────────────┬──────────────────────────────────────┘ │ ▼ @@ -756,7 +756,7 @@ The passive RX log listening feature monitors all incoming packets on the wardri - Extracts **first hop** (first repeater in the path) - Tracks repeaters that first forwarded our message into the mesh - Runs for 7 seconds after each ping -- Results shown in Session Log +- Results shown in TX Log **Passive RX Listening (New):** - Runs continuously in background once connected @@ -801,7 +801,7 @@ The last hop is more relevant for coverage mapping because it represents the rep ### UI Components -**RX Log Section** (below Session Log): +**RX Log Section** (below TX Log): - Header bar showing observation count and last repeater - Expandable/collapsible panel - Scrollable list of observations (newest first) 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 diff --git a/docs/DEVELOPMENT_REQUIREMENTS.md b/docs/DEVELOPMENT_REQUIREMENTS.md index a3a83c7..a873f90 100644 --- a/docs/DEVELOPMENT_REQUIREMENTS.md +++ b/docs/DEVELOPMENT_REQUIREMENTS.md @@ -31,9 +31,10 @@ All debug log messages **MUST** include a descriptive tag in square brackets imm | `[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 | +| `[RX LOG]` | RX logging logic | +| `[RX LOG UI]` | RX UI rendering | +| `[TX LOG]` | TX log tracking | +| `[RX PARSE]` | RX packet metadata parsing | | `[UNIFIED RX]` | Unified RX handler | | `[DECRYPT]` | Message decryption | | `[UI]` | General UI updates (status bar, buttons, etc.) | @@ -42,7 +43,8 @@ All debug log messages **MUST** include a descriptive tag in square brackets imm | `[WAKE LOCK]` | Wake lock acquisition/release | | `[GEOFENCE]` | Geofence and distance validation | | `[CAPACITY]` | Capacity check API calls | -| `[AUTO]` | Auto ping mode operations | +| `[TX/RX AUTO]` | TX/RX Auto mode operations | +| `[RX AUTO]` | RX Auto mode operations | | `[INIT]` | Initialization and setup | | `[ERROR LOG]` | Error log UI operations | diff --git a/docs/FLOW_WARDRIVE_RX_DIAGRAM.md b/docs/FLOW_WARDRIVE_RX_DIAGRAM.md index 35ca26f..3dc4e91 100644 --- a/docs/FLOW_WARDRIVE_RX_DIAGRAM.md +++ b/docs/FLOW_WARDRIVE_RX_DIAGRAM.md @@ -59,7 +59,7 @@ ║ │ │ │ │ │ ║ ║ │ ▼ ▼ │ │ ║ ║ │ ┌─────────┐ ┌──────────────────────────────────────┐ │ ║ -║ │ │ RETURN │ │ PASSIVE RX HANDLER (ALL PACKETS) | | ║ +║ │ │ RETURN │ │ RX LOG HANDLER (ALL PACKETS) | | ║ ║ │ │ (done) │ │ *** NO HEADER OR CHANNEL FILTER *** │ │ ║ ║ │ └─────────┘ └──────────────────────────────────────┘ │ ║ ║ │ │ ║ diff --git a/docs/FLOW_WARDRIVE_TX_DIAGRAM.md b/docs/FLOW_WARDRIVE_TX_DIAGRAM.md index d4f0739..c416590 100644 --- a/docs/FLOW_WARDRIVE_TX_DIAGRAM.md +++ b/docs/FLOW_WARDRIVE_TX_DIAGRAM.md @@ -103,7 +103,7 @@ ║ UI Status: "Listening for heard repeats (6s)" → (5s) → (4s) → ... ║ ║ ║ ║ ┌─────────────────────────────────────────────────────────────────┐ ║ - ║ │ SESSION LOG HANDLER (Echo Detection) │ ║ + ║ │ TX LOG HANDLER (Echo Detection) │ ║ ║ │ *** STRICT VALIDATION (PR #130) *** │ ║ ║ ├─────────────────────────────────────────────────────────────────┤ ║ ║ │ │ ║ @@ -139,7 +139,7 @@ ╠═══════════════════════════════════════════════════════════════════════════╣ ║ ║ ║ ┌────────────────────────────────────────────────────────────────────┐ ║ - ║ │ 1. Update Session Log Entry with Heard Repeaters │ ║ + ║ │ 1. Update TX Log Entry with Heard Repeaters │ ║ ║ │ "..." ──► "22(11.5),4e(8.25),b7(-2.0)" │ ║ ║ └────────────────────────────────────────────────────────────────────┘ ║ ║ │ ║ diff --git a/docs/PING_WORKFLOW.md b/docs/PING_WORKFLOW.md index d5cc084..01526e4 100644 --- a/docs/PING_WORKFLOW.md +++ b/docs/PING_WORKFLOW.md @@ -3,9 +3,9 @@ ## Table of Contents - [Overview](#overview) - [Ping Overview](#ping-overview) - - [Auto Ping Overview](#auto-ping-overview) + - [TX/RX Auto Overview](#auto-ping-overview) - [Manual Ping Workflow](#manual-ping-workflow) -- [Auto Ping Workflow](#auto-ping-workflow) +- [TX/RX Auto Workflow](#auto-ping-workflow) - [Ping Lifecycle](#ping-lifecycle) - [Workflow Diagrams](#workflow-diagrams) - [Code References](#code-references) @@ -20,7 +20,7 @@ - Payload format: `@[MapperBot] , [ [power] ]` - Triggers a 7-second RX listening window for repeater echo detection - Posts ping data to MeshMapper API after the listening window completes -- Logs the ping in the session log with timestamp, coordinates, and repeater data +- Logs the ping in the TX log with timestamp, coordinates, and repeater data **Ping Requirements:** - Active BLE connection to a MeshCore device @@ -30,19 +30,19 @@ - Not in cooldown period (7 seconds after previous ping) - No ping currently in progress -### Auto Ping Overview +### TX/RX Auto Overview -**What "Auto Ping" Does:** +**What "TX/RX Auto" Does:** - Automatically sends pings at configurable intervals (15s/30s/60s) - Acquires a wake lock to keep the screen awake - Displays countdown timer between pings - Skips pings that fail validation (GPS, geofence, distance) without stopping -- Pauses countdown when manual ping is triggered during auto mode +- Pauses countdown when manual ping is triggered during TX/RX TX/RX Auto mode -**Auto Ping State:** -- `state.running`: Boolean indicating if auto mode is active +**TX/RX Auto State:** +- `state.txRxAutoRunning`: Boolean indicating if TX/RX TX/RX Auto mode is active - `state.autoTimerId`: Timer ID for next scheduled ping -- `state.nextAutoPingTime`: Timestamp when next auto ping will fire +- `state.nextAutoPingTime`: Timestamp when next TX/RX Auto will fire - `state.skipReason`: Reason if last ping was skipped (for countdown display) ## Manual Ping Workflow @@ -84,7 +84,7 @@ sendPingBtn.addEventListener("click", () => { - If not in cooldown: proceeds 2. **Handle Auto Mode Interaction** - - If auto mode is running (`state.running === true`): + - If TX/RX TX/RX Auto mode is running (`state.txRxAutoRunning === true`): - Calls `pauseAutoCountdown()` to pause the auto timer - Stores remaining time in `state.pausedAutoTimerRemainingMs` - **Dynamic Status**: `"Sending manual ping"` (blue) @@ -141,9 +141,9 @@ sendPingBtn.addEventListener("click", () => { - **Dynamic Status**: `"Ping sent"` (green) 11. **Start Repeater Tracking** - - Calls `startRepeaterTracking(payload, channelIdx)` + - Calls `startTxTracking(payload, channelIdx)` - Registers `LogRxData` event handler for rx_log entries - - Initializes `state.repeaterTracking` with sent payload and timestamp + - Initializes `state.txTracking` with sent payload and timestamp - **Dynamic Status**: `"Listening (Xs)"` (blue) - countdown display 12. **7-Second Listening Window** @@ -151,10 +151,10 @@ sendPingBtn.addEventListener("click", () => { - Listens for repeater echoes via rx_log events - Each echo validated: header check, channel hash check, payload match - Deduplicates by path, keeps highest SNR value - - Updates session log with live repeater data + - Updates TX log with live repeater data 13. **Finalize Repeaters** - - Calls `stopRepeaterTracking()` after 7s + - Calls `stopTxTracking()` after 7s - Returns array of `{ repeaterId, snr }` objects - Formats as: `"4e(11. 5),77(9.75)"` or `"None"` @@ -177,27 +177,27 @@ sendPingBtn.addEventListener("click", () => { - Calls `unlockPingControls()` - Sets `state.pingInProgress = false` - Updates button disabled states - - **Dynamic Status**: `"—"` (em dash) or countdown if auto mode running + - **Dynamic Status**: `"—"` (em dash) or countdown if TX/RX TX/RX Auto mode running 18. **Resume Auto Countdown (if applicable)** - - If manual ping during auto mode: calls `resumeAutoCountdown()` + - If manual ping during TX/RX TX/RX Auto mode: calls `resumeAutoCountdown()` - Resumes countdown from `state.pausedAutoTimerRemainingMs` -## Auto Ping Workflow +## TX/RX Auto Workflow -### Auto Ping Start Sequence +### TX/RX Auto Start Sequence -1. **User Initiates** → User clicks "Start Auto Ping" button +1. **User Initiates** → User clicks "Start TX/RX Auto" button 2. **Connection Check** → Verify BLE connection exists 3. **Cooldown Check** → Verify not in cooldown period 4. **Timer Cleanup** → Clear any existing auto-ping timer 5. **GPS Watch Start** → Start continuous GPS watching -6. **State Update** → Set `state.running = true` -7. **Button Update** → Change button to "Stop Auto Ping" (amber) +6. **State Update** → Set `state.txRxAutoRunning = true` +7. **Button Update** → Change button to "Stop TX/RX Auto" (amber) 8. **Wake Lock** → Acquire screen wake lock 9. **Initial Ping** → Send first ping immediately -### Detailed Auto Ping Start +### Detailed TX/RX Auto Start See `content/wardrive.js` lines 2462-2501 for `startAutoPing()`. @@ -205,9 +205,9 @@ See `content/wardrive.js` lines 2462-2501 for `startAutoPing()`. ```javascript autoToggleBtn.addEventListener("click", () => { debugLog("Auto toggle button clicked"); - if (state.running) { + if (state.txRxAutoRunning) { stopAutoPing(); - setDynamicStatus("Auto mode stopped", STATUS_COLORS.idle); + setDynamicStatus("TX/RX Auto mode stopped", STATUS_COLORS.idle); } else { startAutoPing(); } @@ -223,7 +223,7 @@ autoToggleBtn.addEventListener("click", () => { 2. **Cooldown Check** - Checks `isInCooldown()` - If in cooldown: - - **Dynamic Status**: `"Wait Xs before toggling auto mode"` (yellow) + - **Dynamic Status**: `"Wait Xs before toggling TX/RX TX/RX Auto mode"` (yellow) - Returns early 3. **Cleanup Existing Timer** @@ -237,7 +237,7 @@ autoToggleBtn.addEventListener("click", () => { - Calls `startGeoWatch()` for continuous GPS updates 6. **Update State** - - Sets `state.running = true` + - Sets `state.txRxAutoRunning = true` - Calls `updateAutoButton()` to change button appearance 7. **Acquire Wake Lock** @@ -247,7 +247,7 @@ autoToggleBtn.addEventListener("click", () => { - Calls `sendPing(false)` immediately - First ping does not wait for interval -### Auto Ping Stop Sequence +### TX/RX Auto Stop Sequence See `content/wardrive. js` lines 2408-2436 for `stopAutoPing()`. @@ -269,20 +269,20 @@ See `content/wardrive. js` lines 2408-2436 for `stopAutoPing()`. - Normal stop keeps GPS watch running 5. **Update State** - - Sets `state.running = false` + - Sets `state.txRxAutoRunning = false` - Calls `updateAutoButton()` to change button appearance 6. **Release Wake Lock** - Calls `releaseWakeLock()` -### Auto Ping Scheduling +### TX/RX Auto Scheduling See `content/wardrive.js` lines 2438-2459 for `scheduleNextAutoPing()`. **After each ping (successful or skipped):** 1. **Check Running State** - - If `state.running === false`: returns early + - If `state.txRxAutoRunning === false`: returns early 2. **Get Interval** - Calls `getSelectedIntervalMs()` (15000, 30000, or 60000) @@ -296,9 +296,9 @@ See `content/wardrive.js` lines 2438-2459 for `scheduleNextAutoPing()`. - Sets `state.autoTimerId = setTimeout(... )` - On timer fire: clears skip reason, calls `sendPing(false)` -### Auto Ping During Manual Ping Interaction +### TX/RX Auto During Manual Ping Interaction -When user sends a manual ping while auto mode is running: +When user sends a manual ping while TX/RX TX/RX Auto mode is running: 1. **Pause Auto Countdown** - `pauseAutoCountdown()` saves remaining time @@ -329,13 +329,13 @@ Locked at: sendPing() validation pass **State Variables:** - `state.pingInProgress`: Boolean flag for active ping operation - `state.cooldownEndTime`: Timestamp when cooldown ends -- Controls: "Send Ping" and "Start/Stop Auto Ping" buttons +- Controls: "Send Ping" and "Start/Stop TX/RX Auto" buttons ### Cooldown Period - **Duration**: 7 seconds (`COOLDOWN_MS = 7000`) - **Starts**: After ping sent to mesh (not after API post) -- **Blocks**: Manual pings and auto mode toggle +- **Blocks**: Manual pings and TX/RX TX/RX Auto mode toggle - **Does NOT block**: Scheduled auto pings (they bypass cooldown) ### Repeater Tracking @@ -415,11 +415,11 @@ sequenceDiagram Device-->>App: Sent App->>UI: "Ping sent" - App->>App: startRepeaterTracking() + App->>App: startTxTracking() Note over App: 7s listening window App->>App: Collect rx_log echoes - App->>App: stopRepeaterTracking() + App->>App: stopTxTracking() App->>API: postToMeshMapperAPI() API-->>App: { allowed: true } @@ -431,13 +431,13 @@ sequenceDiagram end ``` -### Auto Ping State Machine +### TX/RX Auto State Machine ```mermaid stateDiagram-v2 [*] --> Idle - Idle --> Starting: Click "Start Auto Ping" + Idle --> Starting: Click "Start TX/RX Auto" Starting --> Idle: In cooldown Starting --> Idle: Not connected @@ -452,18 +452,18 @@ stateDiagram-v2 Countdown --> Paused: Manual ping clicked Paused --> Countdown: Manual ping complete/blocked - Countdown --> Idle: Click "Stop Auto Ping" + Countdown --> Idle: Click "Stop TX/RX Auto" Countdown --> Idle: Page hidden Listening --> Idle: Disconnect Idle --> [*] ``` -### Auto Ping Countdown Flow +### TX/RX Auto Countdown Flow ```mermaid flowchart TD - A[Ping Complete/Skipped] --> B{Auto mode running?} + A[Ping Complete/Skipped] --> B{TX/RX Auto mode running?} B -->|No| C[End] B -->|Yes| D[scheduleNextAutoPing] D --> E[Start countdown display] @@ -490,7 +490,7 @@ flowchart TD - **Manual button listener**: `wardrive.js` line 2808 - **Auto button listener**: `wardrive.js` line 2812 -### Auto Ping Functions +### TX/RX Auto Functions - **Start function**: `wardrive.js:startAutoPing()` (lines 2462-2501) - **Stop function**: `wardrive.js:stopAutoPing()` (lines 2408-2436) - **Schedule next**: `wardrive.js:scheduleNextAutoPing()` (lines 2438-2459) @@ -503,8 +503,8 @@ flowchart TD - **Cooldown check**: `wardrive.js:isInCooldown()` (lines 445-447) ### Repeater Tracking -- **Start tracking**: `wardrive.js:startRepeaterTracking()` (lines 1508-1567) -- **Stop tracking**: `wardrive.js:stopRepeaterTracking()` (lines 1697-1737) +- **Start tracking**: `wardrive.js:startTxTracking()` (lines 1508-1567) +- **Stop tracking**: `wardrive.js:stopTxTracking()` (lines 1697-1737) - **Handle rx_log**: `wardrive.js:handleRxLogEvent()` (lines 1569-1695) - **Format telemetry**: `wardrive.js:formatRepeaterTelemetry()` (lines 1739-1750) @@ -557,25 +557,25 @@ flowchart TD - Both must be false for controls to be enabled - Cooldown starts at ping send, control lock ends at API complete -### Auto Ping Skipping +### TX/RX Auto Skipping - Auto pings that fail validation are **skipped**, not stopped - Skip reasons: "gps too old", "outside geofence", "too close" - Countdown continues to next interval with skip message displayed -- Auto mode only stops on: user action, page hidden, disconnect +- TX/RX Auto mode only stops on: user action, page hidden, disconnect ### GPS Data Freshness - **Auto pings**: require GPS data within `interval + 5000ms` - **Manual pings**: allow GPS data up to 60 seconds old - If GPS too old during auto: attempts refresh, skips if fails -- GPS watch continues during auto mode, provides fresh data +- GPS watch continues during TX/RX TX/RX Auto mode, provides fresh data ### Page Visibility -- When page becomes hidden during auto mode: - - Auto mode stops immediately +- When page becomes hidden during TX/RX TX/RX Auto mode: + - TX/RX Auto mode stops immediately - GPS watch stops - Wake lock released - - **Dynamic Status**: `"Lost focus, auto mode stopped"` (yellow) -- User must manually restart auto mode when returning + - **Dynamic Status**: `"Lost focus, TX/RX TX/RX Auto mode stopped"` (yellow) +- User must manually restart TX/RX TX/RX Auto mode when returning ### Repeater Tracking Edge Cases - **No repeaters heard**: `heard_repeats = "None"` in API post @@ -603,12 +603,12 @@ MeshCore-GOME-WarDriver implements a comprehensive ping system with both manual 1. **Validation First**: Geofence and distance checks before any mesh operations 2. **Control Locking**: Buttons locked for entire ping lifecycle 3. **Fail-Open on API**: Ping considered sent even if API fails -4. **Graceful Skipping**: Auto mode skips failed pings without stopping +4. **Graceful Skipping**: TX/RX Auto mode skips failed pings without stopping 5. **User Transparency**: Status messages at every step **Manual Ping:** Cooldown Check → GPS → Validations → Lock → Mesh Send → 7s Listen → API Post → Unlock -**Auto Ping:** Start → GPS Watch → Initial Ping → Schedule Next → Countdown → Repeat +**TX/RX Auto:** Start → GPS Watch → Initial Ping → Schedule Next → Countdown → Repeat **Interactions:** - Manual during auto: pause countdown → execute → resume/reschedule @@ -617,4 +617,104 @@ MeshCore-GOME-WarDriver implements a comprehensive ping system with both manual **Debug Mode:** Add `? debug=true` to URL for detailed logging -The workflow prioritizes reliability, validation-first design, and comprehensive user feedback throughout the ping operation. \ No newline at end of file +The workflow prioritizes reliability, validation-first design, and comprehensive user feedback throughout the ping operation. +--- + +## RX Auto Mode + +### 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 | + +### Always-On Unified Listener Architecture + +The unified RX listener operates independently of wardriving modes: + +**Lifecycle:** +- **Starts**: Immediately on connect (after channel setup) +- **Stops**: Only on disconnect +- **Never stops**: During mode changes or page visibility changes + +**Routing Logic:** +1. Listener receives all rx_log events from device +2. Parse packet metadata once with `parseRxPacketMetadata()` +3. If `state.txTracking.isListening` → check for TX echo +4. If `state.rxTracking.isWardriving` → log RX observation +5. If neither → packet ignored (but listener stays active) + +**Defensive Checks:** +- `startUnifiedRxListening()` is idempotent (safe to call multiple times) +- Handler checks `isListening` and reactivates if needed +- Page visibility handler verifies listener active when page visible +- Auto mode start functions ensure listener running + +### Page Visibility +- When page becomes hidden during RX Auto: + - RX Auto mode stops immediately + - Wake lock released + - Unified listener stays active + - **Dynamic Status**: `"Lost focus, RX Auto stopped"` (yellow) +- When page becomes hidden during TX/RX Auto: + - TX/RX Auto mode stops immediately + - GPS watch stops + - Wake lock released + - Unified listener stays active + - **Dynamic Status**: `"Lost focus, TX/RX Auto stopped"` (yellow) +- User must manually restart auto modes when returning + +### Edge Cases +- **Start RX Auto while TX/RX Auto running**: Button disabled (mutual exclusivity) +- **Start TX/RX Auto while RX Auto running**: Button disabled (mutual exclusivity) +- **Disconnect during RX Auto**: Mode stops, listener stops, logs preserved +- **Page hidden during RX Auto**: Mode stops, listener stays on +- **Listener fails**: Defensive checks reactivate listener + +### 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) diff --git a/docs/STATUS_MESSAGES.md b/docs/STATUS_MESSAGES.md index c853f0a..cd44a49 100644 --- a/docs/STATUS_MESSAGES.md +++ b/docs/STATUS_MESSAGES.md @@ -390,24 +390,42 @@ These messages use a hybrid approach: **first display respects 500ms minimum**, #### 7. Auto Mode Messages -##### Auto mode stopped +##### TX/RX Auto stopped - **Message**: `"Auto mode stopped"` - **Color**: Slate (idle) -- **When**: User clicks "Stop Auto Ping" button -- **Source**: `content/wardrive.js:autoToggleBtn click handler` +- **When**: User clicks "Stop TX/RX" button +- **Source**: `content/wardrive.js:txRxAutoBtn click handler` -##### Lost focus, auto mode stopped -- **Message**: `"Lost focus, auto mode stopped"` +##### Lost focus, TX/RX Auto stopped +- **Message**: `"Lost focus, TX/RX Auto stopped"` - **Color**: Amber (warning) -- **When**: Browser tab hidden while auto mode running +- **When**: Browser tab hidden while TX/RX Auto mode running - **Source**: `content/wardrive.js:visibilitychange handler` -##### Wait Xs before toggling auto mode -- **Message**: `"Wait Xs before toggling auto mode"` (X is dynamic countdown) +##### Wait Xs before toggling TX/RX Auto +- **Message**: `"Wait Xs before toggling TX/RX Auto"` (X is dynamic countdown) - **Color**: Amber (warning) -- **When**: User attempts to toggle auto mode during cooldown period +- **When**: User attempts to toggle TX/RX Auto mode during cooldown period - **Source**: `content/wardrive.js:stopAutoPing()`, `startAutoPing()` +##### 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` + #### 8. Error Messages ##### Select radio power to connect diff --git a/index.html b/index.html index 8613b32..ba538f6 100644 --- a/index.html +++ b/index.html @@ -116,6 +116,23 @@

Settings< + +
+ +
+ + +
+
+
@@ -179,13 +196,17 @@

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

Settings<

- -
+ +
-
+
-

Session Log

+

TX Log

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