From ab033b35d32984412ffc5f7e4a0e86d786c313bb Mon Sep 17 00:00:00 2001 From: Smittix Date: Tue, 10 Mar 2026 22:49:03 +0000 Subject: [PATCH] feat: WiFi Locate mode, mobile nav groups, v2.24.0 Add WiFi Locate mode for locating access points by BSSID with real-time signal meter, distance estimation, RSSI history chart, and audio proximity tones. Includes hand-off from WiFi detail drawer, environment presets (Free Space/Outdoor/Indoor), and signal-lost detection. Also includes: - Mobile navigation reorganized into labeled groups (SIG/TRK/SPC/WIFI/INTEL/SYS) - flask-limiter made optional with graceful degradation - Fix radiosonde setup missing semver Python dependency - Documentation updates (FEATURES, USAGE, UI_GUIDE, GitHub Pages site) Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 12 + Dockerfile | 2 +- README.md | 1 + app.py | 31 +- config.py | 12 +- docs/FEATURES.md | 28 ++ docs/UI_GUIDE.md | 5 +- docs/USAGE.md | 33 ++ docs/index.html | 5 + pyproject.toml | 2 +- setup.sh | 6 +- static/css/modes/wifi_locate.css | 385 +++++++++++++++ static/css/responsive.css | 51 ++ static/js/core/cheat-sheets.js | 1 + static/js/modes/wifi_locate.js | 556 ++++++++++++++++++++++ templates/index.html | 63 ++- templates/partials/help-modal.html | 12 + templates/partials/modes/wifi_locate.html | 66 +++ templates/partials/nav.html | 110 +++-- 19 files changed, 1328 insertions(+), 53 deletions(-) create mode 100644 static/css/modes/wifi_locate.css create mode 100644 static/js/modes/wifi_locate.js create mode 100644 templates/partials/modes/wifi_locate.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d34f32..53784ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to iNTERCEPT will be documented in this file. +## [2.24.0] - 2026-03-10 + +### Added +- **WiFi Locate Mode** - Locate WiFi access points by BSSID with real-time signal meter, distance estimation, RSSI chart, and audio proximity tones. Hand-off from WiFi detail drawer, environment presets (Free Space/Outdoor/Indoor), and signal-lost detection. + +### Changed +- Mobile navigation bar reorganized into labeled groups (SIG, TRK, SPC, WIFI, INTEL, SYS) for better usability +- flask-limiter made optional — rate limiting degrades gracefully if package is missing + +### Fixed +- Radiosonde setup missing `semver` Python dependency — `setup.sh` now explicitly installs it alongside `requirements.txt` + ## [2.23.0] - 2026-02-27 ### Added diff --git a/Dockerfile b/Dockerfile index 3776650..9bd9406 100644 --- a/Dockerfile +++ b/Dockerfile @@ -204,7 +204,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && cd /tmp \ && git clone --depth 1 https://github.com/projecthorus/radiosonde_auto_rx.git \ && cd radiosonde_auto_rx/auto_rx \ - && pip install --no-cache-dir -r requirements.txt \ + && pip install --no-cache-dir -r requirements.txt semver \ && bash build.sh \ && mkdir -p /opt/radiosonde_auto_rx/auto_rx \ && cp -r . /opt/radiosonde_auto_rx/auto_rx/ \ diff --git a/README.md b/README.md index 352f1c0..8d7e30d 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Support the developer of this open-source project - **WiFi Scanning** - Monitor mode reconnaissance via aircrack-ng - **Bluetooth Scanning** - Device discovery and tracker detection (with Ubertooth support) - **BT Locate** - SAR Bluetooth device location with GPS-tagged signal trail mapping and proximity alerts +- **WiFi Locate** - Locate WiFi access points by BSSID with real-time signal meter, distance estimation, and proximity audio - **GPS** - Real-time GPS position tracking with live map, speed, altitude, and satellite info - **TSCM** - Counter-surveillance with RF baseline comparison and threat detection - **Meshtastic** - LoRa mesh network integration diff --git a/app.py b/app.py index f87ff79..b1593a0 100644 --- a/app.py +++ b/app.py @@ -42,8 +42,12 @@ from utils.constants import ( QUEUE_MAX_SIZE, ) import logging -from flask_limiter import Limiter -from flask_limiter.util import get_remote_address +try: + from flask_limiter import Limiter + from flask_limiter.util import get_remote_address + _has_limiter = True +except ImportError: + _has_limiter = False # Track application start time for uptime calculation import time as _time _app_start_time = _time.time() @@ -54,11 +58,24 @@ app = Flask(__name__) app.secret_key = "signals_intelligence_secret" # Required for flash messages # Set up rate limiting -limiter = Limiter( - key_func=get_remote_address, - app=app, - storage_uri="memory://", -) +if _has_limiter: + limiter = Limiter( + key_func=get_remote_address, + app=app, + storage_uri="memory://", + ) +else: + logging.getLogger('intercept').warning( + "flask-limiter not installed – rate limiting disabled. " + "Install with: pip install flask-limiter" + ) + class _NoopLimiter: + """Stub so @limiter.limit() decorators are silently ignored.""" + def limit(self, *a, **kw): + def decorator(f): + return f + return decorator + limiter = _NoopLimiter() # Disable Werkzeug debugger PIN (not needed for local development tool) os.environ['WERKZEUG_DEBUG_PIN'] = 'off' diff --git a/config.py b/config.py index f8bc76f..e226a34 100644 --- a/config.py +++ b/config.py @@ -7,10 +7,20 @@ import os import sys # Application version -VERSION = "2.23.0" +VERSION = "2.24.0" # Changelog - latest release notes (shown on welcome screen) CHANGELOG = [ + { + "version": "2.24.0", + "date": "March 2026", + "highlights": [ + "WiFi Locate mode — locate access points by BSSID with real-time signal meter, distance estimation, RSSI chart, and audio proximity tones", + "Mobile navigation reorganized into labeled groups for better usability", + "flask-limiter made optional for graceful degradation", + "Radiosonde setup fix — missing semver dependency", + ] + }, { "version": "2.23.0", "date": "February 2026", diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 2696701..11a5ec3 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -276,6 +276,34 @@ Search and rescue Bluetooth device location with GPS-tagged signal trail mapping - Bluetooth adapter (built-in or USB) - GPS receiver (optional, falls back to manual coordinates) +## WiFi Locate + +Locate a WiFi access point by BSSID using real-time signal strength tracking. + +### Core Features +- **Target by BSSID** - Enter any MAC address or hand off from the WiFi scanner +- **Real-time signal meter** - Large dBm display with color-coded strength (good/medium/weak) +- **20-segment signal bar** - Visual proximity indicator with red/yellow/green segments +- **RSSI history chart** - Canvas sparkline showing signal trend over time +- **Distance estimation** - Log-distance path loss model with configurable environment presets +- **Audio proximity alerts** - Web Audio API tones that increase in pitch and frequency as signal strengthens +- **Signal lost detection** - 30-second timeout with visual overlay when target disappears +- **Hand-off from WiFi mode** - One-click transfer from WiFi detail drawer to WiFi Locate +- **Stats tracking** - Current, min, max, and average RSSI across session + +### Environment Presets +- **Open Field** (n=2.0) - Free space path loss +- **Outdoor** (n=2.8) - Typical outdoor environment (default) +- **Indoor** (n=3.5) - Indoor with walls and obstacles + +### Mode Transition +- WiFi scan is preserved when switching between WiFi and WiFi Locate modes +- Deep scan auto-starts if not already running + +### Requirements +- WiFi adapter capable of monitor mode +- aircrack-ng suite for deep scanning + ## GPS Mode Real-time GPS position tracking with live map visualization. diff --git a/docs/UI_GUIDE.md b/docs/UI_GUIDE.md index 05ddbfb..0aab760 100644 --- a/docs/UI_GUIDE.md +++ b/docs/UI_GUIDE.md @@ -212,15 +212,16 @@ Extended base for full-screen dashboards (maps, visualizations). | `websdr` | WebSDR | | `subghz` | Sub-GHz analyzer | | `bt_locate` | BT Locate | +| `wifi_locate` | WiFi Locate | | `analytics` | Analytics dashboard | | `spaceweather` | Space weather | -### Navigation Groups +### Navigation Groups The navigation is organized into groups: - **Signals**: Pager, 433MHz, Meters, Listening Post, SubGHz - **Tracking**: Aircraft, Vessels, APRS, GPS - **Space**: Satellite, ISS SSTV, Weather Sat, HF SSTV, Space Weather -- **Wireless**: WiFi, Bluetooth, BT Locate, Meshtastic +- **Wireless**: WiFi, Bluetooth, BT Locate, WiFi Locate, Meshtastic - **Intel**: TSCM, Analytics, Spy Stations, WebSDR --- diff --git a/docs/USAGE.md b/docs/USAGE.md index 1e58515..3d677f3 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -377,6 +377,39 @@ Digital Selective Calling monitoring runs alongside AIS: - The RSSI chart shows signal trend over time — use it to determine if you're getting closer - Clear the trail when starting a new search area +## WiFi Locate Mode + +1. **Set Target** - Enter a BSSID (MAC address) in AA:BB:CC:DD:EE:FF format, or hand off from WiFi mode +2. **Choose Environment** - Select the RF environment preset: + - **Open Field** (n=2.0) - Best for open areas with line-of-sight + - **Outdoor** (n=2.8) - Default, works well in most outdoor settings + - **Indoor** (n=3.5) - For buildings with walls and obstacles +3. **Start Locate** - Click "Start Locate" to begin tracking +4. **Monitor Signal** - The HUD shows: + - Large dBm reading with color coding (green/yellow/red) + - 20-segment signal bar for quick visual reference + - Estimated distance based on path loss model + - RSSI history chart for trend analysis + - Current/min/max/average statistics +5. **Follow the Signal** - Move towards stronger signal (higher RSSI / closer distance) +6. **Audio Alerts** - Enable audio for proximity tones that speed up as signal strengthens + +### Hand-off from WiFi Mode + +1. Open WiFi scanning mode and start a deep scan +2. Click any network to open the detail drawer +3. Click the "Locate" button in the drawer header +4. WiFi Locate opens with the BSSID and SSID pre-filled +5. Click "Start Locate" to begin tracking + +### Tips + +- Deep scan is required for continuous RSSI updates — WiFi Locate auto-starts it if needed +- The WiFi scan is preserved when switching between WiFi and WiFi Locate modes +- Signal lost overlay appears after 30 seconds without an update from the target +- The distance estimate is approximate — environment preset significantly affects accuracy +- Indoor environments with walls attenuate signal more than open field + ## GPS Mode 1. **Start GPS** - Click "Start" to connect to gpsd and begin position tracking diff --git a/docs/index.html b/docs/index.html index 5e80eae..5fcdd00 100644 --- a/docs/index.html +++ b/docs/index.html @@ -192,6 +192,11 @@

BT Locate

SAR Bluetooth device location with GPS-tagged signal trail mapping, IRK-based RPA resolution, and proximity audio alerts.

+
+
+

WiFi Locate

+

Locate WiFi access points by BSSID with real-time signal meter, distance estimation, RSSI chart, and audio proximity tones.

+

TSCM

diff --git a/pyproject.toml b/pyproject.toml index 55c4ecc..3494700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "intercept" -version = "2.23.0" +version = "2.24.0" description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth" readme = "README.md" requires-python = ">=3.9" diff --git a/setup.sh b/setup.sh index fd0c557..f32a0cd 100755 --- a/setup.sh +++ b/setup.sh @@ -1060,13 +1060,13 @@ install_radiosonde_auto_rx() { info "Installing Python dependencies..." cd "$tmp_dir/radiosonde_auto_rx/auto_rx" if [ -x "$project_dir/venv/bin/pip" ]; then - "$project_dir/venv/bin/pip" install --quiet -r requirements.txt || { + "$project_dir/venv/bin/pip" install --quiet -r requirements.txt semver || { warn "Failed to install radiosonde_auto_rx Python dependencies" exit 1 } else - pip3 install --quiet --break-system-packages -r requirements.txt 2>/dev/null \ - || pip3 install --quiet -r requirements.txt || { + pip3 install --quiet --break-system-packages -r requirements.txt semver 2>/dev/null \ + || pip3 install --quiet -r requirements.txt semver || { warn "Failed to install radiosonde_auto_rx Python dependencies" exit 1 } diff --git a/static/css/modes/wifi_locate.css b/static/css/modes/wifi_locate.css new file mode 100644 index 0000000..ebe7823 --- /dev/null +++ b/static/css/modes/wifi_locate.css @@ -0,0 +1,385 @@ +/* WiFi Locate Mode Styles */ + +/* Environment preset grid */ +.wfl-env-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 6px; +} + +.wfl-env-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 8px 4px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + color: var(--text-secondary); +} + +.wfl-env-btn:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.2); +} + +.wfl-env-btn.active { + background: rgba(0, 255, 136, 0.1); + border-color: var(--accent-green, #00ff88); + color: var(--text-primary); +} + +.wfl-env-icon { + font-size: 18px; + line-height: 1; +} + +.wfl-env-label { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; +} + +.wfl-env-n { + font-size: 9px; + font-family: var(--font-mono); + color: var(--text-dim); +} + +/* ============================================ + VISUALS CONTAINER + ============================================ */ + +.wfl-visuals-container { + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; + min-height: 0; + overflow: hidden; + padding: 8px; +} + +/* ============================================ + PROXIMITY HUD + ============================================ */ + +.wfl-hud { + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; + padding: 16px; + position: relative; + background: rgba(0, 0, 0, 0.5); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + overflow: hidden; +} + +.wfl-hud-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-bottom: 10px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.wfl-hud-target { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + min-width: 0; +} + +.wfl-target-ssid { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.wfl-target-bssid { + font-size: 11px; + font-family: var(--font-mono); + color: var(--text-dim); +} + +.wfl-hud-audio-toggle { + display: flex; + align-items: center; + gap: 5px; + font-size: 11px; + color: var(--text-secondary); + cursor: pointer; + white-space: nowrap; +} + +.wfl-hud-audio-toggle input[type="checkbox"] { + margin: 0; +} + +.wfl-hud-stop-btn { + padding: 5px 12px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #ff3366; + background: rgba(255, 51, 102, 0.1); + border: 1px solid rgba(255, 51, 102, 0.3); + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.wfl-hud-stop-btn:hover { + background: rgba(255, 51, 102, 0.2); + border-color: #ff3366; +} + +/* ============================================ + RSSI DISPLAY — big dBm number + ============================================ */ + +.wfl-rssi-display { + font-size: 64px; + font-weight: 800; + font-family: var(--font-mono); + text-align: center; + line-height: 1; + color: var(--text-dim); + transition: color 0.3s; + padding: 8px 0; +} + +.wfl-rssi-display.good { + color: #22c55e; + text-shadow: 0 0 20px rgba(34, 197, 94, 0.3); +} + +.wfl-rssi-display.medium { + color: #eab308; + text-shadow: 0 0 20px rgba(234, 179, 8, 0.2); +} + +.wfl-rssi-display.weak { + color: #ef4444; + text-shadow: 0 0 20px rgba(239, 68, 68, 0.2); +} + +/* ============================================ + DISTANCE ESTIMATE + ============================================ */ + +.wfl-distance { + text-align: center; + font-size: 16px; + font-family: var(--font-mono); + color: var(--text-secondary); + margin-top: -4px; +} + +/* ============================================ + SIGNAL BAR — 20 horizontal segments + ============================================ */ + +.wfl-bar-container { + display: flex; + gap: 3px; + padding: 8px 0; + align-items: center; + justify-content: center; +} + +.wfl-bar-segment { + width: 100%; + height: 28px; + flex: 1; + border-radius: 2px; + background: rgba(255, 255, 255, 0.06); + transition: background 0.15s; +} + +.wfl-bar-segment.active:nth-child(-n+7) { + background: #ef4444; + box-shadow: 0 0 6px rgba(239, 68, 68, 0.3); +} + +.wfl-bar-segment.active:nth-child(n+8):nth-child(-n+14) { + background: #eab308; + box-shadow: 0 0 6px rgba(234, 179, 8, 0.3); +} + +.wfl-bar-segment.active:nth-child(n+15) { + background: #22c55e; + box-shadow: 0 0 6px rgba(34, 197, 94, 0.3); +} + +/* ============================================ + RSSI CHART — canvas wrapper + ============================================ */ + +.wfl-rssi-chart-container { + height: 120px; + background: rgba(0, 0, 0, 0.3); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 8px; + position: relative; + flex-shrink: 0; +} + +.wfl-rssi-chart-container .wfl-chart-label { + position: absolute; + top: 4px; + left: 8px; + font-size: 9px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; +} + +#wflRssiChart { + width: 100%; + height: 100%; +} + +/* ============================================ + STATS — 4-column grid + ============================================ */ + +.wfl-stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + padding: 8px 0; +} + +.wfl-stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.wfl-stat-value { + font-size: 16px; + font-weight: 700; + font-family: var(--font-mono); + color: var(--text-primary); +} + +.wfl-stat-label { + font-size: 9px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* ============================================ + SIGNAL LOST OVERLAY + ============================================ */ + +.wfl-signal-lost { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.7); + color: #ef4444; + font-size: 24px; + font-weight: 800; + font-family: var(--font-mono); + letter-spacing: 4px; + text-transform: uppercase; + z-index: 10; + animation: wfl-pulse 2s ease-in-out infinite; +} + +@keyframes wfl-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +/* ============================================ + WAITING STATE + ============================================ */ + +.wfl-waiting { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + color: var(--text-dim); + font-size: 13px; + text-align: center; + padding: 20px; +} + +/* ============================================ + LOCATE BUTTON — WiFi detail drawer + ============================================ */ + +.wfl-locate-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--accent-green, #00ff88); + background: rgba(0, 255, 136, 0.1); + border: 1px solid rgba(0, 255, 136, 0.3); + border-radius: 3px; + cursor: pointer; + transition: all 0.2s; +} + +.wfl-locate-btn:hover { + background: rgba(0, 255, 136, 0.2); + border-color: var(--accent-green, #00ff88); +} + +.wfl-locate-btn svg { + width: 10px; + height: 10px; +} + +/* ============================================ + RESPONSIVE + ============================================ */ + +@media (max-width: 900px) { + .wfl-rssi-display { + font-size: 48px; + } + + .wfl-bar-segment { + height: 22px; + } + + .wfl-stats { + grid-template-columns: repeat(2, 1fr); + } + + .wfl-hud-header { + flex-wrap: wrap; + gap: 8px; + } + + .wfl-rssi-chart-container { + height: 90px; + } +} diff --git a/static/css/responsive.css b/static/css/responsive.css index 54aba08..b94dd60 100644 --- a/static/css/responsive.css +++ b/static/css/responsive.css @@ -322,6 +322,57 @@ flex-shrink: 0; } +/* ============== MOBILE NAV GROUPS ============== */ +.mobile-nav-group { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.mobile-nav-group-label { + font-size: 9px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-tertiary, #6b7280); + padding: 4px 6px 4px 8px; + white-space: nowrap; + border-left: 2px solid var(--border-color, #1f2937); + flex-shrink: 0; +} + +.mobile-nav-group:first-child .mobile-nav-group-label { + border-left: none; + padding-left: 0; +} + +/* ============== MOBILE NAV UTILITIES ============== */ +.mobile-nav-utils { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + padding-left: 8px; + border-left: 2px solid var(--accent-cyan, #4a9eff); +} + +/* Hide mobile nav utilities on desktop (desktop has .nav-utilities) */ +@media (min-width: 1024px) { + .mobile-nav-utils { + display: none; + } +} + +/* ============== TABLET: WRAP MOBILE NAV ============== */ +@media (min-width: 768px) and (max-width: 1023px) { + .mobile-nav-bar { + flex-wrap: wrap; + overflow-x: visible; + justify-content: center; + } +} + /* Hide mobile nav bar on desktop */ @media (min-width: 1024px) { .mobile-nav-bar { diff --git a/static/js/core/cheat-sheets.js b/static/js/core/cheat-sheets.js index bed4336..2eab320 100644 --- a/static/js/core/cheat-sheets.js +++ b/static/js/core/cheat-sheets.js @@ -8,6 +8,7 @@ const CheatSheets = (function () { wifi: { title: 'WiFi Scanner', icon: '📡', hardware: 'WiFi adapter (monitor mode)', description: 'Scans WiFi networks and clients via airodump-ng or nmcli.', whatToExpect: 'SSIDs, BSSIDs, channel, signal strength, encryption type.', tips: ['Run airmon-ng check kill before monitoring', 'Proximity radar shows signal strength', 'TSCM baseline detects rogue APs'] }, bluetooth: { title: 'Bluetooth Scanner', icon: '🔵', hardware: 'Built-in or USB Bluetooth adapter', description: 'Scans BLE and classic Bluetooth devices. Identifies trackers.', whatToExpect: 'Device names, MACs, RSSI, manufacturer, tracker type.', tips: ['Proximity radar shows device distance', 'Known tracker DB has 47K+ fingerprints', 'Use BT Locate to physically find a tracker'] }, bt_locate: { title: 'BT Locate (SAR)', icon: '🎯', hardware: 'Bluetooth adapter + optional GPS', description: 'SAR Bluetooth locator. Tracks RSSI over time to triangulate position.', whatToExpect: 'RSSI chart, proximity band (IMMEDIATE/NEAR/FAR), GPS trail.', tips: ['Handoff from Bluetooth mode to lock onto a device', 'Indoor n=3.0 gives better distance estimates', 'Follow the heat trail toward stronger signal'] }, + wifi_locate: { title: 'WiFi Locate', icon: '📶', hardware: 'WiFi adapter (monitor mode)', description: 'Locate a WiFi AP by BSSID with real-time signal strength tracking.', whatToExpect: 'Big dBm meter, signal bar, RSSI chart, distance estimate, proximity beeps.', tips: ['Handoff from WiFi mode — click Locate on any network', 'Deep scan required for continuous RSSI updates', 'Indoor n=3.5 gives better distance estimates indoors', 'Enable audio for proximity tones that speed up as you get closer'] }, meshtastic: { title: 'Meshtastic', icon: '🕸️', hardware: 'Meshtastic LoRa node (USB)', description: 'Monitors Meshtastic LoRa mesh network messages and positions.', whatToExpect: 'Text messages, node map, telemetry.', tips: ['Default channel must match your mesh', 'Long-Fast has best range', 'GPS nodes appear on map automatically'] }, adsb: { title: 'ADS-B Aircraft', icon: '✈️', hardware: 'RTL-SDR + 1090MHz antenna', description: 'Tracks aircraft via ADS-B Mode S transponders using dump1090.', whatToExpect: 'Flight numbers, positions, altitude, speed, squawk codes.', tips: ['1090MHz — use a dedicated antenna', 'Emergency squawks: 7500 hijack, 7600 radio fail, 7700 emergency', 'Full Dashboard shows map view'] }, ais: { title: 'AIS Vessels', icon: '🚢', hardware: 'RTL-SDR + VHF antenna (162 MHz)', description: 'Tracks marine vessels via AIS using AIS-catcher.', whatToExpect: 'MMSI, vessel names, positions, speed, heading, cargo type.', tips: ['VHF antenna centered at 162MHz works best', 'DSC distress alerts appear in red', 'Coastline range ~40 nautical miles'] }, diff --git a/static/js/modes/wifi_locate.js b/static/js/modes/wifi_locate.js new file mode 100644 index 0000000..c04c5be --- /dev/null +++ b/static/js/modes/wifi_locate.js @@ -0,0 +1,556 @@ +/** + * WiFi Locate — WiFi AP Location Mode + * Real-time signal strength meter with proximity audio for locating WiFi devices by BSSID. + * Reuses existing WiFi v2 API (/wifi/v2/start, /wifi/v2/stop, /wifi/v2/stream, /wifi/v2/status). + */ +const WiFiLocate = (function() { + 'use strict'; + + const API_BASE = '/wifi/v2'; + const MAX_RSSI_POINTS = 60; + const SIGNAL_LOST_TIMEOUT_MS = 30000; + const BAR_SEGMENTS = 20; + const TX_POWER = -30; + + const ENV_PATH_LOSS = { + FREE_SPACE: 2.0, + OUTDOOR: 2.8, + INDOOR: 3.5, + }; + + let eventSource = null; + let targetBssid = null; + let targetSsid = null; + let rssiHistory = []; + let chartCanvas = null; + let chartCtx = null; + let audioCtx = null; + let audioEnabled = false; + let beepTimer = null; + let currentEnvironment = 'OUTDOOR'; + let handoffData = null; + let modeActive = false; + let locateActive = false; + let rssiMin = null; + let rssiMax = null; + let rssiSum = 0; + let rssiCount = 0; + let lastUpdateTime = 0; + let signalLostTimer = null; + + function debugLog(...args) { + console.debug('[WiFiLocate]', ...args); + } + + // ======================================================================== + // Lifecycle + // ======================================================================== + + function init() { + modeActive = true; + chartCanvas = document.getElementById('wflRssiChart'); + chartCtx = chartCanvas ? chartCanvas.getContext('2d') : null; + buildBarSegments(); + } + + function start() { + const bssidInput = document.getElementById('wflBssid'); + const bssid = (bssidInput?.value || '').trim().toUpperCase(); + + if (!bssid || !/^([0-9A-F]{2}:){5}[0-9A-F]{2}$/.test(bssid)) { + if (typeof showNotification === 'function') { + showNotification('Invalid BSSID', 'Enter a valid MAC address (AA:BB:CC:DD:EE:FF)'); + } + return; + } + + targetBssid = bssid; + targetSsid = handoffData?.ssid || null; + locateActive = true; + + // Reset stats + rssiHistory = []; + rssiMin = null; + rssiMax = null; + rssiSum = 0; + rssiCount = 0; + lastUpdateTime = 0; + + // Update UI + updateTargetDisplay(); + showHud(true); + updateStatDisplay('--', '--', '--', '--'); + updateRssiDisplay('--', ''); + updateDistanceDisplay('--'); + clearBarSegments(); + hideSignalLost(); + + // Toggle buttons + const startBtn = document.getElementById('wflStartBtn'); + const stopBtn = document.getElementById('wflStopBtn'); + const statusEl = document.getElementById('wflScanStatus'); + if (startBtn) startBtn.style.display = 'none'; + if (stopBtn) stopBtn.style.display = ''; + if (statusEl) statusEl.style.display = ''; + + // Check if WiFi scan is running, auto-start deep scan if needed + checkAndStartScan().then(() => { + connectSSE(); + }); + } + + function stop() { + locateActive = false; + + // Close SSE + if (eventSource) { + eventSource.close(); + eventSource = null; + } + + // Clear timers + clearBeepTimer(); + clearSignalLostTimer(); + + // Stop audio + stopAudio(); + + // Toggle buttons + const startBtn = document.getElementById('wflStartBtn'); + const stopBtn = document.getElementById('wflStopBtn'); + const statusEl = document.getElementById('wflScanStatus'); + if (startBtn) startBtn.style.display = ''; + if (stopBtn) stopBtn.style.display = 'none'; + if (statusEl) statusEl.style.display = 'none'; + + // Show idle UI + showHud(false); + } + + function destroy() { + stop(); + modeActive = false; + targetBssid = null; + targetSsid = null; + } + + function setActiveMode(active) { + modeActive = active; + } + + // ======================================================================== + // WiFi Scan Management + // ======================================================================== + + async function checkAndStartScan() { + try { + const resp = await fetch(`${API_BASE}/scan/status`); + const data = await resp.json(); + if (data.scanning && data.scan_type === 'deep') { + debugLog('Deep scan already running'); + return; + } + // Auto-start deep scan + debugLog('Starting deep scan for locate'); + await fetch(`${API_BASE}/scan/start`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ scan_type: 'deep' }), + }); + } catch (e) { + debugLog('Error checking/starting scan:', e); + } + } + + // ======================================================================== + // SSE Connection + // ======================================================================== + + function connectSSE() { + if (eventSource) { + eventSource.close(); + } + + const streamUrl = `${API_BASE}/stream`; + eventSource = new EventSource(streamUrl); + + eventSource.onopen = () => { + debugLog('SSE connected'); + }; + + eventSource.onmessage = (event) => { + if (!locateActive || !targetBssid) return; + try { + const data = JSON.parse(event.data); + if (data.type === 'keepalive') return; + + // Filter for our target BSSID + if (data.type === 'network_update' && data.network) { + const net = data.network; + const bssid = (net.bssid || '').toUpperCase(); + if (bssid === targetBssid) { + const rssi = parseInt(net.signal || net.rssi, 10); + if (!isNaN(rssi)) { + // Pick up SSID if we don't have it yet + if (!targetSsid && net.essid) { + targetSsid = net.essid; + updateTargetDisplay(); + } + updateMeter(rssi); + } + } + } + } catch (e) { + debugLog('SSE parse error:', e); + } + }; + + eventSource.onerror = () => { + debugLog('SSE error, reconnecting...'); + if (locateActive) { + setTimeout(() => { + if (locateActive) connectSSE(); + }, 3000); + } + }; + } + + // ======================================================================== + // Signal Processing + // ======================================================================== + + function updateMeter(rssi) { + lastUpdateTime = Date.now(); + hideSignalLost(); + resetSignalLostTimer(); + + // Update stats + rssiCount++; + rssiSum += rssi; + if (rssiMin === null || rssi < rssiMin) rssiMin = rssi; + if (rssiMax === null || rssi > rssiMax) rssiMax = rssi; + const avg = Math.round(rssiSum / rssiCount); + + // Update history + rssiHistory.push(rssi); + if (rssiHistory.length > MAX_RSSI_POINTS) { + rssiHistory.shift(); + } + + // Determine strength class + let cls = 'weak'; + if (rssi >= -50) cls = 'good'; + else if (rssi >= -70) cls = 'medium'; + + // Update displays + updateRssiDisplay(rssi, cls); + updateDistanceDisplay(estimateDistance(rssi)); + updateBarSegments(rssi); + updateStatDisplay(rssi, rssiMin, rssiMax, avg); + drawRssiChart(); + + // Audio + if (audioEnabled) { + scheduleBeeps(rssi); + } + } + + function estimateDistance(rssi) { + const n = ENV_PATH_LOSS[currentEnvironment] || 2.8; + const dist = Math.pow(10, (TX_POWER - rssi) / (10 * n)); + if (dist < 1) return dist.toFixed(2) + ' m'; + if (dist < 100) return dist.toFixed(1) + ' m'; + return Math.round(dist) + ' m'; + } + + // ======================================================================== + // UI Updates + // ======================================================================== + + function showHud(show) { + const hud = document.getElementById('wflHud'); + const waiting = document.getElementById('wflWaiting'); + if (hud) hud.style.display = show ? '' : 'none'; + if (waiting) waiting.style.display = show ? 'none' : ''; + } + + function updateTargetDisplay() { + const ssidEl = document.getElementById('wflTargetSsid'); + const bssidEl = document.getElementById('wflTargetBssid'); + if (ssidEl) ssidEl.textContent = targetSsid || 'Unknown SSID'; + if (bssidEl) bssidEl.textContent = targetBssid || '--'; + } + + function updateRssiDisplay(value, cls) { + const el = document.getElementById('wflRssiValue'); + if (!el) return; + el.textContent = typeof value === 'number' ? value + ' dBm' : value; + el.className = 'wfl-rssi-display' + (cls ? ' ' + cls : ''); + } + + function updateDistanceDisplay(text) { + const el = document.getElementById('wflDistance'); + if (el) el.textContent = text; + } + + function updateStatDisplay(current, min, max, avg) { + const set = (id, v) => { + const el = document.getElementById(id); + if (el) el.textContent = v; + }; + set('wflStatCurrent', typeof current === 'number' ? current + ' dBm' : current); + set('wflStatMin', typeof min === 'number' ? min + ' dBm' : min); + set('wflStatMax', typeof max === 'number' ? max + ' dBm' : max); + set('wflStatAvg', typeof avg === 'number' ? avg + ' dBm' : avg); + } + + // ======================================================================== + // Bar Segments + // ======================================================================== + + function buildBarSegments() { + const container = document.getElementById('wflBarContainer'); + if (!container || container.children.length === BAR_SEGMENTS) return; + container.innerHTML = ''; + for (let i = 0; i < BAR_SEGMENTS; i++) { + const seg = document.createElement('div'); + seg.className = 'wfl-bar-segment'; + container.appendChild(seg); + } + } + + function updateBarSegments(rssi) { + const container = document.getElementById('wflBarContainer'); + if (!container) return; + // Map RSSI -100..-20 to 0..20 active segments + const strength = Math.max(0, Math.min(1, (rssi + 100) / 80)); + const activeCount = Math.round(strength * BAR_SEGMENTS); + const segments = container.children; + for (let i = 0; i < segments.length; i++) { + segments[i].classList.toggle('active', i < activeCount); + } + } + + function clearBarSegments() { + const container = document.getElementById('wflBarContainer'); + if (!container) return; + for (let i = 0; i < container.children.length; i++) { + container.children[i].classList.remove('active'); + } + } + + // ======================================================================== + // RSSI Chart + // ======================================================================== + + function drawRssiChart() { + if (!chartCtx || !chartCanvas) return; + + const w = chartCanvas.width = chartCanvas.parentElement.clientWidth - 16; + const h = chartCanvas.height = chartCanvas.parentElement.clientHeight - 24; + chartCtx.clearRect(0, 0, w, h); + + if (rssiHistory.length < 2) return; + + const minR = -100, maxR = -20; + const range = maxR - minR; + + // Grid lines + chartCtx.strokeStyle = 'rgba(255,255,255,0.05)'; + chartCtx.lineWidth = 1; + [-30, -50, -70, -90].forEach(v => { + const y = h - ((v - minR) / range) * h; + chartCtx.beginPath(); + chartCtx.moveTo(0, y); + chartCtx.lineTo(w, y); + chartCtx.stroke(); + }); + + // Draw RSSI line + const step = w / (MAX_RSSI_POINTS - 1); + chartCtx.beginPath(); + chartCtx.strokeStyle = '#00ff88'; + chartCtx.lineWidth = 2; + + rssiHistory.forEach((rssi, i) => { + const x = i * step; + const y = h - ((rssi - minR) / range) * h; + if (i === 0) chartCtx.moveTo(x, y); + else chartCtx.lineTo(x, y); + }); + chartCtx.stroke(); + + // Fill under + const lastIdx = rssiHistory.length - 1; + chartCtx.lineTo(lastIdx * step, h); + chartCtx.lineTo(0, h); + chartCtx.closePath(); + chartCtx.fillStyle = 'rgba(0,255,136,0.08)'; + chartCtx.fill(); + } + + // ======================================================================== + // Audio Proximity + // ======================================================================== + + function playTone(freq, duration) { + if (!audioCtx || audioCtx.state !== 'running') return; + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + osc.connect(gain); + gain.connect(audioCtx.destination); + osc.frequency.value = freq; + osc.type = 'sine'; + gain.gain.value = 0.2; + gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration); + osc.start(); + osc.stop(audioCtx.currentTime + duration); + } + + function playProximityTone(rssi) { + if (!audioCtx || audioCtx.state !== 'running') return; + const strength = Math.max(0, Math.min(1, (rssi + 100) / 70)); + const freq = 400 + strength * 800; + const duration = 0.06 + (1 - strength) * 0.12; + playTone(freq, duration); + } + + function scheduleBeeps(rssi) { + clearBeepTimer(); + playProximityTone(rssi); + // Repeat interval: stronger signal = faster beeps + const strength = Math.max(0, Math.min(1, (rssi + 100) / 70)); + const interval = 1200 - strength * 1000; // 1200ms (weak) to 200ms (strong) + beepTimer = setInterval(() => { + if (audioEnabled && locateActive) { + playProximityTone(rssi); + } else { + clearBeepTimer(); + } + }, interval); + } + + function clearBeepTimer() { + if (beepTimer) { + clearInterval(beepTimer); + beepTimer = null; + } + } + + function toggleAudio() { + const cb = document.getElementById('wflAudioEnable'); + audioEnabled = cb?.checked || false; + if (audioEnabled) { + if (!audioCtx) { + try { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } catch (e) { + console.error('[WiFiLocate] AudioContext creation failed:', e); + return; + } + } + audioCtx.resume().then(() => { + playTone(600, 0.08); + }); + } else { + stopAudio(); + } + } + + function stopAudio() { + audioEnabled = false; + clearBeepTimer(); + const cb = document.getElementById('wflAudioEnable'); + if (cb) cb.checked = false; + } + + // ======================================================================== + // Signal Lost Timer + // ======================================================================== + + function resetSignalLostTimer() { + clearSignalLostTimer(); + signalLostTimer = setTimeout(() => { + if (locateActive) showSignalLost(); + }, SIGNAL_LOST_TIMEOUT_MS); + } + + function clearSignalLostTimer() { + if (signalLostTimer) { + clearTimeout(signalLostTimer); + signalLostTimer = null; + } + } + + function showSignalLost() { + const el = document.getElementById('wflSignalLost'); + if (el) el.style.display = ''; + clearBeepTimer(); + } + + function hideSignalLost() { + const el = document.getElementById('wflSignalLost'); + if (el) el.style.display = 'none'; + } + + // ======================================================================== + // Environment + // ======================================================================== + + function setEnvironment(env) { + currentEnvironment = env; + document.querySelectorAll('.wfl-env-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.env === env); + }); + // Recalc distance with last known RSSI + if (rssiHistory.length > 0) { + const lastRssi = rssiHistory[rssiHistory.length - 1]; + updateDistanceDisplay(estimateDistance(lastRssi)); + } + } + + // ======================================================================== + // Handoff from WiFi mode + // ======================================================================== + + function handoff(info) { + handoffData = info; + const bssidInput = document.getElementById('wflBssid'); + if (bssidInput) bssidInput.value = info.bssid || ''; + targetSsid = info.ssid || null; + + const card = document.getElementById('wflHandoffCard'); + const nameEl = document.getElementById('wflHandoffName'); + const metaEl = document.getElementById('wflHandoffMeta'); + if (card) card.style.display = ''; + if (nameEl) nameEl.textContent = info.ssid || 'Hidden Network'; + if (metaEl) metaEl.textContent = info.bssid || ''; + + // Switch to WiFi Locate mode + if (typeof switchMode === 'function') { + switchMode('wifi_locate'); + } + } + + function clearHandoff() { + handoffData = null; + const card = document.getElementById('wflHandoffCard'); + if (card) card.style.display = 'none'; + } + + // ======================================================================== + // Public API + // ======================================================================== + + return { + init, + start, + stop, + destroy, + handoff, + clearHandoff, + setEnvironment, + toggleAudio, + setActiveMode, + }; +})(); diff --git a/templates/index.html b/templates/index.html index 09f317a..636d872 100644 --- a/templates/index.html +++ b/templates/index.html @@ -81,6 +81,7 @@ gps: "{{ url_for('static', filename='css/modes/gps.css') }}", subghz: "{{ url_for('static', filename='css/modes/subghz.css') }}?v={{ version }}&r=subghz_layout9", bt_locate: "{{ url_for('static', filename='css/modes/bt_locate.css') }}?v={{ version }}&r=btlocate4", + wifi_locate: "{{ url_for('static', filename='css/modes/wifi_locate.css') }}?v={{ version }}&r=wflocate1", spaceweather: "{{ url_for('static', filename='css/modes/space-weather.css') }}", wefax: "{{ url_for('static', filename='css/modes/wefax.css') }}", morse: "{{ url_for('static', filename='css/modes/morse.css') }}", @@ -369,6 +370,10 @@ BT Locate +
+
@@ -1963,6 +1973,39 @@
+ + + @@ -213,43 +214,78 @@ {# Mobile Navigation Bar #} {# JavaScript stub for pages that don't have switchMode defined #}