mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
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 <noreply@anthropic.com>
This commit is contained in:
12
CHANGELOG.md
12
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
|
||||
|
||||
@@ -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/ \
|
||||
|
||||
@@ -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
|
||||
|
||||
25
app.py
25
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(
|
||||
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'
|
||||
|
||||
12
config.py
12
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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -212,6 +212,7 @@ 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
|
||||
@@ -220,7 +221,7 @@ 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
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -192,6 +192,11 @@
|
||||
<h3>BT Locate</h3>
|
||||
<p>SAR Bluetooth device location with GPS-tagged signal trail mapping, IRK-based RPA resolution, and proximity audio alerts.</p>
|
||||
</div>
|
||||
<div class="feature-card" data-category="wireless">
|
||||
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/><circle cx="12" cy="10" r="2"/><path d="M12 14v-2"/></svg></div>
|
||||
<h3>WiFi Locate</h3>
|
||||
<p>Locate WiFi access points by BSSID with real-time signal meter, distance estimation, RSSI chart, and audio proximity tones.</p>
|
||||
</div>
|
||||
<div class="feature-card" data-category="intel">
|
||||
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s-8-4.5-8-11.8A8 8 0 0 1 12 2a8 8 0 0 1 8 8.2c0 7.3-8 11.8-8 11.8z"/><circle cx="12" cy="10" r="3"/><path d="M12 2v3"/><path d="M4.93 4.93l2.12 2.12"/><path d="M20 12h-3"/></svg></div>
|
||||
<h3>TSCM</h3>
|
||||
|
||||
@@ -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"
|
||||
|
||||
6
setup.sh
6
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
|
||||
}
|
||||
|
||||
385
static/css/modes/wifi_locate.css
Normal file
385
static/css/modes/wifi_locate.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'] },
|
||||
|
||||
556
static/js/modes/wifi_locate.js
Normal file
556
static/js/modes/wifi_locate.js
Normal file
@@ -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,
|
||||
};
|
||||
})();
|
||||
@@ -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 @@
|
||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/><path d="M9.5 8.5l3 3 2-4-2 4-3 3"/></svg></span>
|
||||
<span class="mode-name">BT Locate</span>
|
||||
</button>
|
||||
<button class="mode-card mode-card-sm" onclick="selectMode('wifi_locate')">
|
||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/><circle cx="12" cy="10" r="2"/><path d="M12 14v-2"/></svg></span>
|
||||
<span class="mode-name">WF Locate</span>
|
||||
</button>
|
||||
<button class="mode-card mode-card-sm" onclick="selectMode('meshtastic')">
|
||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg></span>
|
||||
<span class="mode-name">Meshtastic</span>
|
||||
@@ -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 @@
|
||||
<span class="wifi-detail-essid" id="wifiDetailEssid">Network Name</span>
|
||||
<span class="wifi-detail-bssid" id="wifiDetailBssid">00:00:00:00:00:00</span>
|
||||
</div>
|
||||
<button class="wfl-locate-btn" onclick="WiFiLocate.handoff({bssid: document.getElementById('wifiDetailBssid')?.textContent, ssid: document.getElementById('wifiDetailEssid')?.textContent})" title="Locate this AP">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>
|
||||
Locate
|
||||
</button>
|
||||
<button class="wifi-detail-close" onclick="WiFiMode.closeDetail()">×</button>
|
||||
</div>
|
||||
<div class="wifi-detail-content" id="wifiDetailContent">
|
||||
@@ -1963,6 +1973,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WiFi Locate Dashboard -->
|
||||
<div id="wflVisuals" class="wfl-visuals-container" style="display: none;">
|
||||
<div class="wfl-hud" id="wflHud" style="display: none;">
|
||||
<div class="wfl-hud-header">
|
||||
<div class="wfl-hud-target">
|
||||
<span class="wfl-target-ssid" id="wflTargetSsid">--</span>
|
||||
<span class="wfl-target-bssid" id="wflTargetBssid">--</span>
|
||||
</div>
|
||||
<label class="wfl-hud-audio-toggle">
|
||||
<input type="checkbox" id="wflAudioEnable" onchange="WiFiLocate.toggleAudio()"> Audio
|
||||
</label>
|
||||
<button class="wfl-hud-stop-btn" onclick="WiFiLocate.stop()">Stop Tracking</button>
|
||||
</div>
|
||||
<div class="wfl-rssi-display" id="wflRssiValue">--</div>
|
||||
<div class="wfl-distance" id="wflDistance">--</div>
|
||||
<div class="wfl-bar-container" id="wflBarContainer"></div>
|
||||
<div class="wfl-rssi-chart-container">
|
||||
<span class="wfl-chart-label">RSSI History</span>
|
||||
<canvas id="wflRssiChart"></canvas>
|
||||
</div>
|
||||
<div class="wfl-stats">
|
||||
<div class="wfl-stat"><span class="wfl-stat-value" id="wflStatCurrent">--</span><span class="wfl-stat-label">Current</span></div>
|
||||
<div class="wfl-stat"><span class="wfl-stat-value" id="wflStatMin">--</span><span class="wfl-stat-label">Min</span></div>
|
||||
<div class="wfl-stat"><span class="wfl-stat-value" id="wflStatMax">--</span><span class="wfl-stat-label">Max</span></div>
|
||||
<div class="wfl-stat"><span class="wfl-stat-value" id="wflStatAvg">--</span><span class="wfl-stat-label">Avg</span></div>
|
||||
</div>
|
||||
<div class="wfl-signal-lost" id="wflSignalLost" style="display: none;">SIGNAL LOST</div>
|
||||
</div>
|
||||
<div class="wfl-waiting" id="wflWaiting">
|
||||
<p>Enter a target BSSID and click Start Locate</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebSDR Dashboard -->
|
||||
<div id="websdrVisuals" style="display: none; padding: 12px; flex-direction: column; gap: 12px; flex: 1; min-height: 0; overflow: hidden;">
|
||||
<!-- Audio Control Bar (hidden until connected) -->
|
||||
@@ -3391,6 +3434,7 @@
|
||||
<script src="{{ url_for('static', filename='js/modes/websdr.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/subghz.js') }}?v={{ version }}&r=subghz_layout9"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/bt_locate.js') }}?v={{ version }}&r=btlocate4"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/wifi_locate.js') }}?v={{ version }}&r=wflocate1"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/wefax.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/morse.js') }}?v={{ version }}&r=morse_iq12"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/ook.js') }}?v={{ version }}&r=ook2"></script>
|
||||
@@ -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') {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span><span class="desc">WiFi - Network scanner</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span><span class="desc">Bluetooth - BT/BLE scanner</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/><path d="M9.5 8.5l3 3 2-4-2 4-3 3"/></svg></span><span class="desc">BT Locate - Bluetooth device locator</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/><circle cx="12" cy="10" r="2"/><path d="M12 14v-2"/></svg></span><span class="desc">WiFi Locate - WiFi AP locator</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span><span class="desc">TSCM - Counter-surveillance</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg></span><span class="desc">Satellite - Pass prediction</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M3 9h2"/><path d="M19 9h2"/><path d="M3 15h2"/><path d="M19 15h2"/></svg></span><span class="desc">ISS SSTV - Space station image decoder</span></div>
|
||||
@@ -249,6 +250,16 @@
|
||||
<li>Useful for finding lost devices or detecting unwanted trackers</li>
|
||||
</ul>
|
||||
|
||||
<h3>WiFi Locate Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Locate a WiFi access point by BSSID using real-time signal strength</li>
|
||||
<li>Big dBm meter, 20-segment signal bar, and RSSI history chart</li>
|
||||
<li>Distance estimation using log-distance path loss model with environment presets</li>
|
||||
<li>Audio proximity tones that speed up as signal strengthens</li>
|
||||
<li>Hand off from WiFi mode — click "Locate" on any network in the detail drawer</li>
|
||||
<li>Deep scan auto-starts if not already running</li>
|
||||
</ul>
|
||||
|
||||
<h3>TSCM Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Technical Surveillance Countermeasures sweep</li>
|
||||
@@ -362,6 +373,7 @@
|
||||
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
|
||||
<li><strong>Bluetooth:</strong> Bluetooth adapter, bluez (hcitool/bluetoothctl)</li>
|
||||
<li><strong>BT Locate:</strong> Bluetooth adapter, bluez</li>
|
||||
<li><strong>WiFi Locate:</strong> WiFi adapter (monitor mode), aircrack-ng suite</li>
|
||||
<li><strong>TSCM:</strong> WiFi adapter, Bluetooth adapter, RTL-SDR (all optional)</li>
|
||||
<li>Run as root/sudo for full hardware access</li>
|
||||
</ul>
|
||||
|
||||
66
templates/partials/modes/wifi_locate.html
Normal file
66
templates/partials/modes/wifi_locate.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!-- WIFI LOCATE MODE -->
|
||||
<div id="wflMode" class="mode-content">
|
||||
<div class="section">
|
||||
<h3>WiFi Locate</h3>
|
||||
<p class="info-text" style="font-size: 11px; color: var(--text-dim); margin-bottom: 12px;">
|
||||
Locate a WiFi access point by BSSID — real-time signal strength meter with proximity audio.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Target Lock -->
|
||||
<div class="section">
|
||||
<h3>Target</h3>
|
||||
<div id="wflHandoffCard" style="display: none; background: rgba(0,255,136,0.08); border: 1px solid rgba(0,255,136,0.3); border-radius: 6px; padding: 8px; margin-bottom: 8px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-size: 10px; color: var(--accent-green); text-transform: uppercase; font-weight: 600;">Handed off from WiFi</span>
|
||||
<button onclick="WiFiLocate.clearHandoff()" style="background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 10px;">×</button>
|
||||
</div>
|
||||
<div id="wflHandoffName" style="font-size: 12px; font-weight: 600; color: var(--text-primary); margin-top: 4px;"></div>
|
||||
<div id="wflHandoffMeta" style="font-size: 10px; color: var(--text-dim); font-family: var(--font-mono);"></div>
|
||||
</div>
|
||||
|
||||
<label class="input-label">BSSID (MAC Address)</label>
|
||||
<input type="text" id="wflBssid" class="text-input" placeholder="AA:BB:CC:DD:EE:FF" style="font-family: var(--font-mono); font-size: 11px;">
|
||||
</div>
|
||||
|
||||
<!-- Environment Preset -->
|
||||
<div class="section">
|
||||
<h3>Environment</h3>
|
||||
<div class="wfl-env-grid">
|
||||
<button class="wfl-env-btn" data-env="FREE_SPACE" onclick="WiFiLocate.setEnvironment('FREE_SPACE')">
|
||||
<span class="wfl-env-icon">🏠</span>
|
||||
<span class="wfl-env-label">Open Field</span>
|
||||
<span class="wfl-env-n">n=2.0</span>
|
||||
</button>
|
||||
<button class="wfl-env-btn active" data-env="OUTDOOR" onclick="WiFiLocate.setEnvironment('OUTDOOR')">
|
||||
<span class="wfl-env-icon">🌳</span>
|
||||
<span class="wfl-env-label">Outdoor</span>
|
||||
<span class="wfl-env-n">n=2.8</span>
|
||||
</button>
|
||||
<button class="wfl-env-btn" data-env="INDOOR" onclick="WiFiLocate.setEnvironment('INDOOR')">
|
||||
<span class="wfl-env-icon">🏢</span>
|
||||
<span class="wfl-env-label">Indoor</span>
|
||||
<span class="wfl-env-n">n=3.5</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info note -->
|
||||
<div class="section">
|
||||
<p class="info-text" style="font-size: 10px; color: var(--text-dim);">
|
||||
Deep scan recommended for continuous RSSI tracking. Will auto-start if not running.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="section">
|
||||
<div style="display: flex; gap: 6px;">
|
||||
<button class="run-btn" id="wflStartBtn" onclick="WiFiLocate.start()">Start Locate</button>
|
||||
<button class="stop-btn" id="wflStopBtn" onclick="WiFiLocate.stop()" style="display: none;">Stop</button>
|
||||
</div>
|
||||
<div id="wflScanStatus" style="display: none; margin-top: 6px; font-size: 10px; color: var(--text-dim);">
|
||||
<span id="wflScanDot" style="display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #22c55e; margin-right: 4px; vertical-align: middle;"></span>
|
||||
<span id="wflScanText">WiFi scanner active</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,6 +124,7 @@
|
||||
{{ mode_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg>') }}
|
||||
{{ mode_item('bluetooth', 'Bluetooth', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}
|
||||
{{ mode_item('bt_locate', 'BT Locate', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/><path d="M9.5 8.5l3 3 2-4-2 4-3 3"/></svg>') }}
|
||||
{{ mode_item('wifi_locate', 'WF Locate', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/><circle cx="12" cy="10" r="2"/><path d="M12 14v-2"/></svg>') }}
|
||||
{{ mode_item('meshtastic', 'Meshtastic', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,19 +214,31 @@
|
||||
|
||||
{# Mobile Navigation Bar #}
|
||||
<nav class="mobile-nav-bar" id="mobileNavBar">
|
||||
{# Signals #}
|
||||
{# Signals Group #}
|
||||
<div class="mobile-nav-group" data-group="signals">
|
||||
<span class="mobile-nav-group-label">SIG</span>
|
||||
{{ mobile_item('pager', 'Pager', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg>') }}
|
||||
{{ mobile_item('sensor', '433MHz', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg>') }}
|
||||
{{ mobile_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
||||
{{ mobile_item('subghz', 'SubGHz', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h6l3-9 3 18 3-9h5"/></svg>') }}
|
||||
{{ mobile_item('waterfall', 'Waterfall', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h4l3-8 3 16 3-8h4"/></svg>') }}
|
||||
{{ mobile_item('morse', 'Morse', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="2" y1="12" x2="5" y2="12"/><line x1="7" y1="12" x2="13" y2="12"/><line x1="15" y1="12" x2="18" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/></svg>') }}
|
||||
{{ mobile_item('ook', 'OOK', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h3"/><path d="M19 12h3"/><rect x="5" y="8" width="4" height="8" rx="1"/><rect x="10" y="9" width="4" height="6" rx="1"/><rect x="15" y="7" width="4" height="10" rx="1"/></svg>') }}
|
||||
{# Tracking #}
|
||||
</div>
|
||||
|
||||
{# Tracking Group #}
|
||||
<div class="mobile-nav-group" data-group="tracking">
|
||||
<span class="mobile-nav-group-label">TRK</span>
|
||||
{{ mobile_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
||||
{{ mobile_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
||||
{{ mobile_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
|
||||
{{ mobile_item('gps', 'GPS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>') }}
|
||||
{# Space #}
|
||||
{{ mobile_item('radiosonde', 'Sonde', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v6"/><circle cx="12" cy="12" r="4"/><path d="M12 16v6"/><path d="M4.93 4.93l4.24 4.24"/><path d="M14.83 14.83l4.24 4.24"/></svg>') }}
|
||||
</div>
|
||||
|
||||
{# Space Group #}
|
||||
<div class="mobile-nav-group" data-group="space">
|
||||
<span class="mobile-nav-group-label">SPC</span>
|
||||
{% if is_index_page %}
|
||||
{{ mobile_item('satellite', 'Sat', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/></svg>') }}
|
||||
{% else %}
|
||||
@@ -237,19 +250,42 @@
|
||||
{{ mobile_item('sstv_general', 'HF SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg>') }}
|
||||
{{ mobile_item('spaceweather', 'SpaceWx', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/></svg>') }}
|
||||
{{ mobile_item('meteor', 'Meteor', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L2 22"/><path d="M18 2h4v4"/><circle cx="8" cy="16" r="4"/><path d="M16 6l-4 4"/></svg>') }}
|
||||
{# Wireless #}
|
||||
</div>
|
||||
|
||||
{# Wireless Group #}
|
||||
<div class="mobile-nav-group" data-group="wireless">
|
||||
<span class="mobile-nav-group-label">WLS</span>
|
||||
{{ mobile_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg>') }}
|
||||
{{ mobile_item('bluetooth', 'BT', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}
|
||||
{{ mobile_item('bt_locate', 'Locate', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>') }}
|
||||
{{ mobile_item('wifi_locate', 'WF Loc', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/><circle cx="12" cy="10" r="2"/></svg>') }}
|
||||
{{ mobile_item('meshtastic', 'Mesh', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>') }}
|
||||
{# Intel #}
|
||||
</div>
|
||||
|
||||
{# Intel Group #}
|
||||
<div class="mobile-nav-group" data-group="intel">
|
||||
<span class="mobile-nav-group-label">INT</span>
|
||||
{{ mobile_item('tscm', 'TSCM', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>') }}
|
||||
{{ mobile_item('spystations', 'Spy', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><circle cx="12" cy="12" r="2"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
|
||||
{{ mobile_item('websdr', 'WebSDR', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>') }}
|
||||
{# New modes #}
|
||||
{{ mobile_item('waterfall', 'Waterfall', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h4l3-8 3 16 3-8h4"/></svg>') }}
|
||||
{# System #}
|
||||
</div>
|
||||
|
||||
{# System Group #}
|
||||
<div class="mobile-nav-group" data-group="system">
|
||||
<span class="mobile-nav-group-label">SYS</span>
|
||||
{{ mobile_item('system', 'System', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>') }}
|
||||
</div>
|
||||
|
||||
{# Utility Buttons (theme, settings, help) #}
|
||||
<div class="mobile-nav-utils">
|
||||
<button type="button" class="mobile-nav-btn" onclick="toggleTheme()" aria-label="Toggle theme" title="Toggle theme">
|
||||
<span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></span>
|
||||
</button>
|
||||
<button type="button" class="mobile-nav-btn" onclick="showSettings()" aria-label="Settings" title="Settings">
|
||||
<span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
|
||||
</button>
|
||||
<button type="button" class="mobile-nav-btn" onclick="showHelp()" aria-label="Help" title="Help">?</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{# JavaScript stub for pages that don't have switchMode defined #}
|
||||
|
||||
Reference in New Issue
Block a user