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
+
+
+ WF Locate
+
Meshtastic
@@ -721,6 +726,7 @@
{% include 'partials/modes/subghz.html' %}
{% include 'partials/modes/bt_locate.html' %}
+ {% include 'partials/modes/wifi_locate.html' %}
{% include 'partials/modes/waterfall.html' %}
{% include 'partials/modes/meteor.html' %}
{% include 'partials/modes/system.html' %}
@@ -889,6 +895,10 @@
Network Name
00:00:00:00:00:00
+
+
+ Locate
+
×
@@ -1963,6 +1973,39 @@
+
+
+
+
+
+
--
+
+
+
+
-- Current
+
-- Min
+
-- Max
+
-- Avg
+
+
SIGNAL LOST
+
+
+
Enter a target BSSID and click Start Locate
+
+
+
@@ -3391,6 +3434,7 @@
+
@@ -3546,6 +3590,7 @@
wifi: { label: 'WiFi', indicator: 'WIFI', outputTitle: 'WiFi Scanner', group: 'wireless' },
bluetooth: { label: 'Bluetooth', indicator: 'BLUETOOTH', outputTitle: 'Bluetooth Scanner', group: 'wireless' },
bt_locate: { label: 'BT Locate', indicator: 'BT LOCATE', outputTitle: 'BT Locate — SAR Tracker', group: 'wireless' },
+ wifi_locate: { label: 'WiFi Locate', indicator: 'WF LOCATE', outputTitle: 'WiFi Locate', group: 'wireless' },
meshtastic: { label: 'Meshtastic', indicator: 'MESHTASTIC', outputTitle: 'Meshtastic Mesh Monitor', group: 'wireless' },
tscm: { label: 'TSCM', indicator: 'TSCM', outputTitle: 'TSCM Counter-Surveillance', group: 'intel' },
spystations: { label: 'Spy Stations', indicator: 'SPY STATIONS', outputTitle: 'Spy Stations', group: 'intel' },
@@ -4099,6 +4144,7 @@
bluetooth: () => typeof BluetoothMode !== 'undefined' && BluetoothMode.destroy?.(),
wifi: () => typeof WiFiMode !== 'undefined' && WiFiMode.destroy?.(),
bt_locate: () => typeof BtLocate !== 'undefined' && BtLocate.destroy?.(),
+ wifi_locate: () => typeof WiFiLocate !== 'undefined' && WiFiLocate.destroy?.(),
sstv: () => typeof SSTV !== 'undefined' && SSTV.destroy?.(),
sstv_general: () => typeof SSTVGeneral !== 'undefined' && SSTVGeneral.destroy?.(),
websdr: () => typeof WebSDR !== 'undefined' && WebSDR.destroy?.(),
@@ -4317,7 +4363,10 @@
&& typeof WiFiMode.isScanning === 'function'
&& WiFiMode.isScanning()
) || isWifiRunning;
- if (wifiScanActive) {
+ const isWifiModeTransition =
+ (currentMode === 'wifi' && mode === 'wifi_locate') ||
+ (currentMode === 'wifi_locate' && mode === 'wifi');
+ if (wifiScanActive && !isWifiModeTransition) {
stopTasks.push(awaitStopAction('wifi', () => stopWifiScan(), LOCAL_STOP_TIMEOUT_MS));
}
const btScanActive = (typeof BluetoothMode !== 'undefined' &&
@@ -4378,6 +4427,10 @@
document.querySelectorAll('.mobile-nav-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
+ const activeMobileBtn = document.querySelector('.mobile-nav-btn.active');
+ if (activeMobileBtn) {
+ activeMobileBtn.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
+ }
document.getElementById('pagerMode')?.classList.toggle('active', mode === 'pager');
document.getElementById('sensorMode')?.classList.toggle('active', mode === 'sensor');
document.getElementById('rtlamrMode')?.classList.toggle('active', mode === 'rtlamr');
@@ -4390,6 +4443,7 @@
document.getElementById('wifiMode')?.classList.toggle('active', mode === 'wifi');
document.getElementById('bluetoothMode')?.classList.toggle('active', mode === 'bluetooth');
document.getElementById('btLocateMode')?.classList.toggle('active', mode === 'bt_locate');
+ document.getElementById('wflMode')?.classList.toggle('active', mode === 'wifi_locate');
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
document.getElementById('aisMode')?.classList.toggle('active', mode === 'ais');
@@ -4442,6 +4496,7 @@
const websdrVisuals = document.getElementById('websdrVisuals');
const subghzVisuals = document.getElementById('subghzVisuals');
const btLocateVisuals = document.getElementById('btLocateVisuals');
+ const wflVisuals = document.getElementById('wflVisuals');
const wefaxVisuals = document.getElementById('wefaxVisuals');
const spaceWeatherVisuals = document.getElementById('spaceWeatherVisuals');
const waterfallVisuals = document.getElementById('waterfallVisuals');
@@ -4466,6 +4521,7 @@
if (websdrVisuals) websdrVisuals.style.display = mode === 'websdr' ? 'flex' : 'none';
if (subghzVisuals) subghzVisuals.style.display = mode === 'subghz' ? 'flex' : 'none';
if (btLocateVisuals) btLocateVisuals.style.display = mode === 'bt_locate' ? 'flex' : 'none';
+ if (wflVisuals) wflVisuals.style.display = mode === 'wifi_locate' ? 'flex' : 'none';
if (wefaxVisuals) wefaxVisuals.style.display = mode === 'wefax' ? 'flex' : 'none';
if (spaceWeatherVisuals) spaceWeatherVisuals.style.display = mode === 'spaceweather' ? 'flex' : 'none';
if (waterfallVisuals) waterfallVisuals.style.display = mode === 'waterfall' ? 'flex' : 'none';
@@ -4477,6 +4533,9 @@
if (typeof BtLocate !== 'undefined' && BtLocate.setActiveMode) {
BtLocate.setActiveMode(mode === 'bt_locate');
}
+ if (typeof WiFiLocate !== 'undefined' && WiFiLocate.setActiveMode) {
+ WiFiLocate.setActiveMode(mode === 'wifi_locate');
+ }
// Hide sidebar by default for Meshtastic mode, show for others
const mainContent = document.querySelector('.main-content');
@@ -4655,6 +4714,8 @@
setTimeout(() => {
if (typeof BtLocate !== 'undefined' && BtLocate.invalidateMap) BtLocate.invalidateMap();
}, 320);
+ } else if (mode === 'wifi_locate') {
+ WiFiLocate.init();
} else if (mode === 'wefax') {
WeFax.init();
} else if (mode === 'spaceweather') {
diff --git a/templates/partials/help-modal.html b/templates/partials/help-modal.html
index 31bb437..7e92e0d 100644
--- a/templates/partials/help-modal.html
+++ b/templates/partials/help-modal.html
@@ -51,6 +51,7 @@
Bluetooth - BT/BLE scanner
BT Locate - Bluetooth device locator
+
WiFi Locate - WiFi AP locator
TSCM - Counter-surveillance
Satellite - Pass prediction
ISS SSTV - Space station image decoder
@@ -249,6 +250,16 @@
Useful for finding lost devices or detecting unwanted trackers
+
WiFi Locate Mode
+
+ Locate a WiFi access point by BSSID using real-time signal strength
+ Big dBm meter, 20-segment signal bar, and RSSI history chart
+ Distance estimation using log-distance path loss model with environment presets
+ Audio proximity tones that speed up as signal strengthens
+ Hand off from WiFi mode — click "Locate" on any network in the detail drawer
+ Deep scan auto-starts if not already running
+
+
TSCM Mode
Technical Surveillance Countermeasures sweep
@@ -362,6 +373,7 @@
WiFi: Monitor-mode adapter, aircrack-ng suite
Bluetooth: Bluetooth adapter, bluez (hcitool/bluetoothctl)
BT Locate: Bluetooth adapter, bluez
+ WiFi Locate: WiFi adapter (monitor mode), aircrack-ng suite
TSCM: WiFi adapter, Bluetooth adapter, RTL-SDR (all optional)
Run as root/sudo for full hardware access
diff --git a/templates/partials/modes/wifi_locate.html b/templates/partials/modes/wifi_locate.html
new file mode 100644
index 0000000..d1cadb5
--- /dev/null
+++ b/templates/partials/modes/wifi_locate.html
@@ -0,0 +1,66 @@
+
+
+
+
WiFi Locate
+
+ Locate a WiFi access point by BSSID — real-time signal strength meter with proximity audio.
+
+
+
+
+
+
Target
+
+
+ Handed off from WiFi
+ ×
+
+
+
+
+
+
BSSID (MAC Address)
+
+
+
+
+
+
Environment
+
+
+ 🏠
+ Open Field
+ n=2.0
+
+
+ 🌳
+ Outdoor
+ n=2.8
+
+
+ 🏢
+ Indoor
+ n=3.5
+
+
+
+
+
+
+
+ Deep scan recommended for continuous RSSI tracking. Will auto-start if not running.
+
+
+
+
+
+
+ Start Locate
+ Stop
+
+
+
+ WiFi scanner active
+
+
+
diff --git a/templates/partials/nav.html b/templates/partials/nav.html
index 41bb856..ca2918d 100644
--- a/templates/partials/nav.html
+++ b/templates/partials/nav.html
@@ -124,6 +124,7 @@
{{ mode_item('wifi', 'WiFi', '
') }}
{{ mode_item('bluetooth', 'Bluetooth', '
') }}
{{ mode_item('bt_locate', 'BT Locate', '
') }}
+ {{ mode_item('wifi_locate', 'WF Locate', '
') }}
{{ mode_item('meshtastic', 'Meshtastic', '
') }}
@@ -213,43 +214,78 @@
{# Mobile Navigation Bar #}
- {# Signals #}
- {{ mobile_item('pager', 'Pager', ' ') }}
- {{ mobile_item('sensor', '433MHz', ' ') }}
- {{ mobile_item('rtlamr', 'Meters', ' ') }}
- {{ mobile_item('subghz', 'SubGHz', ' ') }}
- {{ mobile_item('morse', 'Morse', ' ') }}
- {{ mobile_item('ook', 'OOK', ' ') }}
- {# Tracking #}
- {{ mobile_item('adsb', 'Aircraft', ' ', '/adsb/dashboard') }}
- {{ mobile_item('ais', 'Vessels', ' ', '/ais/dashboard') }}
- {{ mobile_item('aprs', 'APRS', ' ') }}
- {{ mobile_item('gps', 'GPS', ' ') }}
- {# Space #}
- {% if is_index_page %}
- {{ mobile_item('satellite', 'Sat', ' ') }}
- {% else %}
- {{ mobile_item('satellite', 'Sat', ' ', '/satellite/dashboard') }}
- {% endif %}
- {{ mobile_item('sstv', 'SSTV', ' ') }}
- {{ mobile_item('weathersat', 'WxSat', ' ') }}
- {{ mobile_item('wefax', 'WeFax', ' ') }}
- {{ mobile_item('sstv_general', 'HF SSTV', ' ') }}
- {{ mobile_item('spaceweather', 'SpaceWx', ' ') }}
- {{ mobile_item('meteor', 'Meteor', ' ') }}
- {# Wireless #}
- {{ mobile_item('wifi', 'WiFi', ' ') }}
- {{ mobile_item('bluetooth', 'BT', ' ') }}
- {{ mobile_item('bt_locate', 'Locate', ' ') }}
- {{ mobile_item('meshtastic', 'Mesh', ' ') }}
- {# Intel #}
- {{ mobile_item('tscm', 'TSCM', ' ') }}
- {{ mobile_item('spystations', 'Spy', ' ') }}
- {{ mobile_item('websdr', 'WebSDR', ' ') }}
- {# New modes #}
- {{ mobile_item('waterfall', 'Waterfall', ' ') }}
- {# System #}
- {{ mobile_item('system', 'System', ' ') }}
+ {# Signals Group #}
+
+
SIG
+ {{ mobile_item('pager', 'Pager', '
') }}
+ {{ mobile_item('sensor', '433MHz', '
') }}
+ {{ mobile_item('rtlamr', 'Meters', '
') }}
+ {{ mobile_item('subghz', 'SubGHz', '
') }}
+ {{ mobile_item('waterfall', 'Waterfall', '
') }}
+ {{ mobile_item('morse', 'Morse', '
') }}
+ {{ mobile_item('ook', 'OOK', '
') }}
+
+
+ {# Tracking Group #}
+
+
TRK
+ {{ mobile_item('adsb', 'Aircraft', '
', '/adsb/dashboard') }}
+ {{ mobile_item('ais', 'Vessels', '
', '/ais/dashboard') }}
+ {{ mobile_item('aprs', 'APRS', '
') }}
+ {{ mobile_item('gps', 'GPS', '
') }}
+ {{ mobile_item('radiosonde', 'Sonde', '
') }}
+
+
+ {# Space Group #}
+
+
SPC
+ {% if is_index_page %}
+ {{ mobile_item('satellite', 'Sat', '
') }}
+ {% else %}
+ {{ mobile_item('satellite', 'Sat', '
', '/satellite/dashboard') }}
+ {% endif %}
+ {{ mobile_item('sstv', 'SSTV', '
') }}
+ {{ mobile_item('weathersat', 'WxSat', '
') }}
+ {{ mobile_item('wefax', 'WeFax', '
') }}
+ {{ mobile_item('sstv_general', 'HF SSTV', '
') }}
+ {{ mobile_item('spaceweather', 'SpaceWx', '
') }}
+ {{ mobile_item('meteor', 'Meteor', '
') }}
+
+
+ {# Wireless Group #}
+
+
WLS
+ {{ mobile_item('wifi', 'WiFi', '
') }}
+ {{ mobile_item('bluetooth', 'BT', '
') }}
+ {{ mobile_item('bt_locate', 'Locate', '
') }}
+ {{ mobile_item('wifi_locate', 'WF Loc', '
') }}
+ {{ mobile_item('meshtastic', 'Mesh', '
') }}
+
+
+ {# Intel Group #}
+
+
INT
+ {{ mobile_item('tscm', 'TSCM', '
') }}
+ {{ mobile_item('spystations', 'Spy', '
') }}
+ {{ mobile_item('websdr', 'WebSDR', '
') }}
+
+
+ {# System Group #}
+
+ SYS
+ {{ mobile_item('system', 'System', ' ') }}
+
+
+ {# Utility Buttons (theme, settings, help) #}
+
{# JavaScript stub for pages that don't have switchMode defined #}