mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Remove LoRa/ISM mode (redundant with 433MHz)
The LoRa mode was removed because: - rtl_433 cannot decode actual LoRa (CSS modulation) - The 433MHz mode already handles ISM band devices - True LoRa decoding requires specialized tools like gr-lora Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
12
app.py
12
app.py
@@ -103,11 +103,6 @@ satellite_process = None
|
||||
satellite_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||
satellite_lock = threading.Lock()
|
||||
|
||||
# LoRa/ISM band
|
||||
lora_process = None
|
||||
lora_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||
lora_lock = threading.Lock()
|
||||
|
||||
# ============================================
|
||||
# GLOBAL STATE DICTIONARIES
|
||||
# ============================================
|
||||
@@ -309,7 +304,6 @@ def health_check() -> Response:
|
||||
'adsb': adsb_process is not None and (adsb_process.poll() is None if adsb_process else False),
|
||||
'wifi': wifi_process is not None and (wifi_process.poll() is None if wifi_process else False),
|
||||
'bluetooth': bt_process is not None and (bt_process.poll() is None if bt_process else False),
|
||||
'lora': lora_process is not None and (lora_process.poll() is None if lora_process else False),
|
||||
},
|
||||
'data': {
|
||||
'aircraft_count': len(adsb_aircraft),
|
||||
@@ -323,7 +317,7 @@ def health_check() -> Response:
|
||||
@app.route('/killall', methods=['POST'])
|
||||
def kill_all() -> Response:
|
||||
"""Kill all decoder and WiFi processes."""
|
||||
global current_process, sensor_process, wifi_process, adsb_process, lora_process
|
||||
global current_process, sensor_process, wifi_process, adsb_process
|
||||
|
||||
# Import adsb module to reset its state
|
||||
from routes import adsb as adsb_module
|
||||
@@ -357,10 +351,6 @@ def kill_all() -> Response:
|
||||
adsb_process = None
|
||||
adsb_module.adsb_using_service = False
|
||||
|
||||
# Reset LoRa state
|
||||
with lora_lock:
|
||||
lora_process = None
|
||||
|
||||
return jsonify({'status': 'killed', 'processes': killed})
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ def register_blueprints(app):
|
||||
from .settings import settings_bp
|
||||
from .correlation import correlation_bp
|
||||
from .listening_post import listening_post_bp
|
||||
from .lora import lora_bp
|
||||
|
||||
app.register_blueprint(pager_bp)
|
||||
app.register_blueprint(sensor_bp)
|
||||
@@ -24,4 +23,3 @@ def register_blueprints(app):
|
||||
app.register_blueprint(settings_bp)
|
||||
app.register_blueprint(correlation_bp)
|
||||
app.register_blueprint(listening_post_bp)
|
||||
app.register_blueprint(lora_bp)
|
||||
|
||||
317
routes/lora.py
317
routes/lora.py
@@ -1,317 +0,0 @@
|
||||
"""LoRa/ISM band monitoring routes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import queue
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Generator
|
||||
|
||||
from flask import Blueprint, jsonify, request, Response
|
||||
|
||||
import app as app_module
|
||||
from utils.logging import get_logger
|
||||
from utils.validation import (
|
||||
validate_frequency, validate_device_index, validate_gain, validate_ppm,
|
||||
validate_rtl_tcp_host, validate_rtl_tcp_port
|
||||
)
|
||||
from utils.sse import format_sse
|
||||
from utils.sdr import SDRFactory, SDRType
|
||||
|
||||
logger = get_logger('intercept.lora')
|
||||
|
||||
lora_bp = Blueprint('lora', __name__, url_prefix='/lora')
|
||||
|
||||
# LoRa frequency bands by region
|
||||
LORA_BANDS = {
|
||||
'eu868': {
|
||||
'name': 'EU 868 MHz',
|
||||
'frequency': 868.0,
|
||||
'range': (863.0, 870.0),
|
||||
'channels': [868.1, 868.3, 868.5, 867.1, 867.3, 867.5, 867.7, 867.9]
|
||||
},
|
||||
'us915': {
|
||||
'name': 'US 915 MHz',
|
||||
'frequency': 915.0,
|
||||
'range': (902.0, 928.0),
|
||||
'channels': [902.3, 902.5, 902.7, 902.9, 903.1, 903.3, 903.5, 903.7]
|
||||
},
|
||||
'au915': {
|
||||
'name': 'AU 915 MHz',
|
||||
'frequency': 915.0,
|
||||
'range': (915.0, 928.0),
|
||||
'channels': [915.2, 915.4, 915.6, 915.8, 916.0, 916.2, 916.4, 916.6]
|
||||
},
|
||||
'as923': {
|
||||
'name': 'AS 923 MHz',
|
||||
'frequency': 923.0,
|
||||
'range': (920.0, 925.0),
|
||||
'channels': [923.2, 923.4, 923.6, 923.8, 924.0, 924.2, 924.4, 924.6]
|
||||
},
|
||||
'in865': {
|
||||
'name': 'IN 865 MHz',
|
||||
'frequency': 865.0,
|
||||
'range': (865.0, 867.0),
|
||||
'channels': [865.0625, 865.4025, 865.985]
|
||||
},
|
||||
'ism433': {
|
||||
'name': 'ISM 433 MHz',
|
||||
'frequency': 433.92,
|
||||
'range': (433.05, 434.79),
|
||||
'channels': [433.05, 433.42, 433.92, 434.42]
|
||||
}
|
||||
}
|
||||
|
||||
# Device patterns that indicate LoRa/LPWAN devices
|
||||
LORA_DEVICE_PATTERNS = [
|
||||
'LoRa', 'Dragino', 'RAK', 'Heltec', 'TTGO', 'LoPy', 'Pycom',
|
||||
'Semtech', 'SX127', 'RFM95', 'RFM96', 'Murata', 'Microchip',
|
||||
'The Things', 'TTN', 'Helium', 'Chirpstack', 'LoRaWAN',
|
||||
'Smart meter', 'Sensus', 'Itron', 'Landis', 'Water meter',
|
||||
'Gas meter', 'Electric meter', 'Utility meter'
|
||||
]
|
||||
|
||||
|
||||
def is_lora_device(model: str, protocol: str = '') -> bool:
|
||||
"""Check if a device model/protocol indicates LoRa/LPWAN."""
|
||||
combined = f"{model} {protocol}".lower()
|
||||
return any(pattern.lower() in combined for pattern in LORA_DEVICE_PATTERNS)
|
||||
|
||||
|
||||
def stream_lora_output(process: subprocess.Popen[bytes]) -> None:
|
||||
"""Stream rtl_433 JSON output to LoRa queue."""
|
||||
try:
|
||||
app_module.lora_queue.put({'type': 'status', 'text': 'started'})
|
||||
|
||||
for line in iter(process.stdout.readline, b''):
|
||||
line = line.decode('utf-8', errors='replace').strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
# rtl_433 outputs JSON objects
|
||||
data = json.loads(line)
|
||||
|
||||
# Enhance with LoRa-specific info
|
||||
model = data.get('model', 'Unknown')
|
||||
protocol = data.get('protocol', '')
|
||||
data['type'] = 'lora_device'
|
||||
data['is_lora'] = is_lora_device(model, protocol)
|
||||
data['timestamp'] = datetime.now().isoformat()
|
||||
|
||||
# Calculate signal quality if RSSI available
|
||||
rssi = data.get('rssi')
|
||||
if rssi is not None:
|
||||
# Normalize RSSI to quality percentage
|
||||
# Typical LoRa range: -120 dBm (weak) to -30 dBm (strong)
|
||||
quality = max(0, min(100, (rssi + 120) * 100 / 90))
|
||||
data['signal_quality'] = round(quality)
|
||||
|
||||
app_module.lora_queue.put(data)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# Not JSON, could be info message
|
||||
if line and not line.startswith('_'):
|
||||
app_module.lora_queue.put({'type': 'info', 'text': line})
|
||||
|
||||
except Exception as e:
|
||||
app_module.lora_queue.put({'type': 'error', 'text': str(e)})
|
||||
finally:
|
||||
process.wait()
|
||||
app_module.lora_queue.put({'type': 'status', 'text': 'stopped'})
|
||||
with app_module.lora_lock:
|
||||
app_module.lora_process = None
|
||||
|
||||
|
||||
@lora_bp.route('/bands')
|
||||
def get_bands() -> Response:
|
||||
"""Get available LoRa frequency bands."""
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'bands': LORA_BANDS
|
||||
})
|
||||
|
||||
|
||||
@lora_bp.route('/start', methods=['POST'])
|
||||
def start_lora() -> Response:
|
||||
"""Start LoRa band monitoring."""
|
||||
with app_module.lora_lock:
|
||||
if app_module.lora_process:
|
||||
return jsonify({'status': 'error', 'message': 'LoRa monitor already running'}), 409
|
||||
|
||||
data = request.json or {}
|
||||
|
||||
# Get band or custom frequency
|
||||
band_id = data.get('band', 'eu868')
|
||||
band = LORA_BANDS.get(band_id, LORA_BANDS['eu868'])
|
||||
|
||||
# Allow custom frequency override
|
||||
custom_freq = data.get('frequency')
|
||||
if custom_freq:
|
||||
try:
|
||||
freq = validate_frequency(custom_freq)
|
||||
except ValueError as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||
else:
|
||||
freq = band['frequency']
|
||||
|
||||
# Validate other inputs
|
||||
try:
|
||||
gain = validate_gain(data.get('gain', '40')) # Higher default gain for weak signals
|
||||
ppm = validate_ppm(data.get('ppm', '0'))
|
||||
device = validate_device_index(data.get('device', '0'))
|
||||
except ValueError as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||
|
||||
# Clear queue
|
||||
while not app_module.lora_queue.empty():
|
||||
try:
|
||||
app_module.lora_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
# Get SDR type
|
||||
sdr_type_str = data.get('sdr_type', 'rtlsdr')
|
||||
try:
|
||||
sdr_type = SDRType(sdr_type_str)
|
||||
except ValueError:
|
||||
sdr_type = SDRType.RTL_SDR
|
||||
|
||||
# Check for rtl_tcp
|
||||
rtl_tcp_host = data.get('rtl_tcp_host')
|
||||
rtl_tcp_port = data.get('rtl_tcp_port', 1234)
|
||||
|
||||
if rtl_tcp_host:
|
||||
try:
|
||||
rtl_tcp_host = validate_rtl_tcp_host(rtl_tcp_host)
|
||||
rtl_tcp_port = validate_rtl_tcp_port(rtl_tcp_port)
|
||||
except ValueError as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||
|
||||
sdr_device = SDRFactory.create_network_device(rtl_tcp_host, rtl_tcp_port)
|
||||
else:
|
||||
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
||||
|
||||
builder = SDRFactory.get_builder(sdr_device.sdr_type)
|
||||
|
||||
# Build command for LoRa band monitoring
|
||||
bias_t = data.get('bias_t', False)
|
||||
|
||||
# Use rtl_433 with settings optimized for LoRa bands
|
||||
# -f frequency, -g gain, -F json, -M time:utc, -Y autolevel
|
||||
cmd = builder.build_ism_command(
|
||||
device=sdr_device,
|
||||
frequency_mhz=freq,
|
||||
gain=float(gain) if gain else 40.0,
|
||||
ppm=int(ppm) if ppm else None,
|
||||
bias_t=bias_t
|
||||
)
|
||||
|
||||
# Add hop frequencies for the band if enabled
|
||||
hop_enabled = data.get('hop_enabled', False)
|
||||
if hop_enabled and 'channels' in band:
|
||||
# Add additional frequencies to monitor
|
||||
for ch_freq in band['channels'][:4]: # Limit to 4 hop frequencies
|
||||
if ch_freq != freq:
|
||||
cmd.extend(['-f', f'{ch_freq}M'])
|
||||
|
||||
full_cmd = ' '.join(cmd)
|
||||
logger.info(f"Running: {full_cmd}")
|
||||
|
||||
try:
|
||||
app_module.lora_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
# Start output thread
|
||||
thread = threading.Thread(target=stream_lora_output, args=(app_module.lora_process,))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
# Monitor stderr
|
||||
def monitor_stderr():
|
||||
for line in app_module.lora_process.stderr:
|
||||
err = line.decode('utf-8', errors='replace').strip()
|
||||
if err:
|
||||
logger.debug(f"[rtl_433] {err}")
|
||||
|
||||
stderr_thread = threading.Thread(target=monitor_stderr)
|
||||
stderr_thread.daemon = True
|
||||
stderr_thread.start()
|
||||
|
||||
app_module.lora_queue.put({
|
||||
'type': 'info',
|
||||
'text': f'Monitoring {band["name"]} at {freq} MHz'
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'status': 'started',
|
||||
'band': band_id,
|
||||
'frequency': freq,
|
||||
'command': full_cmd
|
||||
})
|
||||
|
||||
except FileNotFoundError:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'rtl_433 not found. Install with: sudo apt install rtl-433'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
|
||||
@lora_bp.route('/stop', methods=['POST'])
|
||||
def stop_lora() -> Response:
|
||||
"""Stop LoRa monitoring."""
|
||||
with app_module.lora_lock:
|
||||
if app_module.lora_process:
|
||||
app_module.lora_process.terminate()
|
||||
try:
|
||||
app_module.lora_process.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
app_module.lora_process.kill()
|
||||
app_module.lora_process = None
|
||||
return jsonify({'status': 'stopped'})
|
||||
|
||||
return jsonify({'status': 'not_running'})
|
||||
|
||||
|
||||
@lora_bp.route('/stream')
|
||||
def stream_lora() -> Response:
|
||||
"""SSE stream for LoRa data."""
|
||||
def generate() -> Generator[str, None, None]:
|
||||
last_keepalive = time.time()
|
||||
keepalive_interval = 30.0
|
||||
|
||||
while True:
|
||||
try:
|
||||
msg = app_module.lora_queue.get(timeout=1)
|
||||
last_keepalive = time.time()
|
||||
yield format_sse(msg)
|
||||
except queue.Empty:
|
||||
now = time.time()
|
||||
if now - last_keepalive >= keepalive_interval:
|
||||
yield format_sse({'type': 'keepalive'})
|
||||
last_keepalive = now
|
||||
|
||||
response = Response(generate(), mimetype='text/event-stream')
|
||||
response.headers['Cache-Control'] = 'no-cache'
|
||||
response.headers['X-Accel-Buffering'] = 'no'
|
||||
response.headers['Connection'] = 'keep-alive'
|
||||
return response
|
||||
|
||||
|
||||
@lora_bp.route('/status')
|
||||
def lora_status() -> Response:
|
||||
"""Get LoRa monitoring status."""
|
||||
with app_module.lora_lock:
|
||||
running = app_module.lora_process is not None
|
||||
return jsonify({
|
||||
'status': 'running' if running else 'stopped',
|
||||
'running': running
|
||||
})
|
||||
@@ -2779,57 +2779,6 @@ header p {
|
||||
background: rgba(0, 122, 255, 0.05);
|
||||
}
|
||||
|
||||
/* LoRa Layout Container */
|
||||
.lora-layout-container {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background: var(--bg-secondary);
|
||||
margin: 0 15px 10px 15px;
|
||||
border: 1px solid var(--border-color);
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.lora-layout-container .wifi-visuals {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.lora-device-list {
|
||||
border-left-color: var(--accent-green) !important;
|
||||
}
|
||||
|
||||
.lora-device-list .wifi-device-list-header h5 {
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.lora-device-card {
|
||||
border-left-color: var(--accent-green) !important;
|
||||
}
|
||||
|
||||
.lora-device-card.selected {
|
||||
border-color: var(--accent-green);
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.lora-layout-container {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
max-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.lora-layout-container .wifi-visuals {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.lora-device-list {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.bt-layout-container {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -244,23 +244,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LoRa Stats -->
|
||||
<div class="header-stats-group" id="headerLoraStats">
|
||||
<div class="stat-badge">
|
||||
<span class="badge-icon">📶</span>
|
||||
<div>
|
||||
<span class="badge-value" id="headerLoraDeviceCount">0</span>
|
||||
<span class="badge-label">devices</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-badge">
|
||||
<span class="badge-icon">📡</span>
|
||||
<div>
|
||||
<span class="badge-value" id="headerLoraSignalCount">0</span>
|
||||
<span class="badge-label">signals</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -270,7 +253,6 @@
|
||||
<span class="mode-nav-label">SDR / RF</span>
|
||||
<button class="mode-nav-btn active" onclick="switchMode('pager')"><span class="nav-icon">📟</span><span class="nav-label">Pager</span></button>
|
||||
<button class="mode-nav-btn" onclick="switchMode('sensor')"><span class="nav-icon">📡</span><span class="nav-label">433MHz</span></button>
|
||||
<button class="mode-nav-btn" onclick="switchMode('lora')"><span class="nav-icon">📶</span><span class="nav-label">LoRa/ISM</span></button>
|
||||
<button class="mode-nav-btn" onclick="switchMode('aircraft')"><span class="nav-icon">✈️</span><span class="nav-label">Aircraft</span></button>
|
||||
<button class="mode-nav-btn" onclick="switchMode('satellite')"><span class="nav-icon">🛰️</span><span class="nav-label">Satellite</span></button>
|
||||
<button class="mode-nav-btn" onclick="switchMode('listening')"><span class="nav-icon">📻</span><span class="nav-label">Listening Post</span></button>
|
||||
@@ -504,70 +486,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- LoRa MODE -->
|
||||
<div id="loraMode" class="mode-content">
|
||||
<div class="section">
|
||||
<h3>LoRa Band</h3>
|
||||
<div class="form-group">
|
||||
<label>Region/Band</label>
|
||||
<select id="loraBandSelect" onchange="onLoraBandChanged()">
|
||||
<option value="eu868">EU 868 MHz</option>
|
||||
<option value="us915">US 915 MHz</option>
|
||||
<option value="au915">AU 915 MHz</option>
|
||||
<option value="as923">AS 923 MHz</option>
|
||||
<option value="in865">IN 865 MHz</option>
|
||||
<option value="ism433">ISM 433 MHz</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Frequency (MHz)</label>
|
||||
<input type="text" id="loraFrequency" value="868.0" placeholder="e.g., 868.0">
|
||||
</div>
|
||||
<div class="preset-buttons" id="loraChannelButtons">
|
||||
<!-- Populated by JavaScript based on selected band -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Settings</h3>
|
||||
<div class="form-group">
|
||||
<label>Gain (dB, higher for weak signals)</label>
|
||||
<input type="text" id="loraGain" value="40" placeholder="0-50, 40 recommended">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>PPM Correction</label>
|
||||
<input type="text" id="loraPpm" value="0" placeholder="Frequency correction">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="inline-checkbox">
|
||||
<input type="checkbox" id="loraHop">
|
||||
Enable Channel Hopping
|
||||
</label>
|
||||
<div class="info-text" style="font-size: 10px; color: #666; margin-top: 4px;">
|
||||
Monitor multiple channels in the selected band
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Device Patterns</h3>
|
||||
<div class="info-text" style="font-size: 11px;">
|
||||
rtl_433 detects LoRa/LPWAN devices including:<br>
|
||||
<span style="color: var(--accent-cyan);">• Smart meters</span> (water, gas, electric)<br>
|
||||
<span style="color: var(--accent-green);">• LoRaWAN</span> gateways and nodes<br>
|
||||
<span style="color: var(--accent-orange);">• IoT sensors</span> and controllers<br>
|
||||
<span style="color: var(--accent-purple);">• Agricultural</span> monitoring systems
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="run-btn" id="startLoraBtn" onclick="startLoraMonitoring()">
|
||||
Start Monitoring
|
||||
</button>
|
||||
<button class="stop-btn" id="stopLoraBtn" onclick="stopLoraMonitoring()" style="display: none;">
|
||||
Stop Monitoring
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- WiFi MODE -->
|
||||
<div id="wifiMode" class="mode-content">
|
||||
<div class="section">
|
||||
@@ -1256,10 +1174,6 @@
|
||||
<div class="stats" id="satelliteStats" style="display: none;">
|
||||
<div title="Upcoming Passes">🛰️ <span id="passCount">0</span></div>
|
||||
</div>
|
||||
<div class="stats" id="loraStats" style="display: none;">
|
||||
<div title="LoRa Devices">📶 <span id="loraDeviceCount">0</span></div>
|
||||
<div title="Signals Detected">📡 <span id="loraSignalCount">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1451,81 +1365,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LoRa Layout Container -->
|
||||
<div class="lora-layout-container" id="loraLayoutContainer" style="display: none;">
|
||||
<!-- Left: LoRa Visualizations -->
|
||||
<div class="wifi-visuals" id="loraVisuals">
|
||||
<!-- Selected Device Info -->
|
||||
<div class="wifi-visual-panel" style="grid-column: span 2;">
|
||||
<h5>📋 Selected Device</h5>
|
||||
<div id="loraSelectedDevice" style="font-size: 11px; min-height: 100px;">
|
||||
<div style="color: var(--text-dim); padding: 20px; text-align: center;">Click a device to view details</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Row 1: Signal Radar + Device Types -->
|
||||
<div class="wifi-visual-panel">
|
||||
<h5>Signal Radar</h5>
|
||||
<div class="radar-container">
|
||||
<canvas id="loraRadarCanvas" width="150" height="150"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wifi-visual-panel">
|
||||
<h5>Device Categories</h5>
|
||||
<div class="bt-type-overview" id="loraTypeOverview">
|
||||
<div class="bt-type-item"><span class="bt-type-icon">🌡️</span> Sensors: <strong id="loraSensorCount">0</strong></div>
|
||||
<div class="bt-type-item"><span class="bt-type-icon">⚡</span> Meters: <strong id="loraMeterCount">0</strong></div>
|
||||
<div class="bt-type-item"><span class="bt-type-icon">📡</span> LoRaWAN: <strong id="loraLorawanCount">0</strong></div>
|
||||
<div class="bt-type-item"><span class="bt-type-icon">🔵</span> Other: <strong id="loraOtherTypeCount">0</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Row 2: Signal Quality + Band Activity -->
|
||||
<div class="wifi-visual-panel">
|
||||
<h5>📶 Signal Quality</h5>
|
||||
<div class="bt-signal-dist" id="loraSignalDist">
|
||||
<div class="signal-range"><span>Strong (-50+)</span><div class="signal-bar-bg"><div class="signal-bar strong" id="loraSignalStrong" style="width: 0%;"></div></div><span id="loraSignalStrongCount">0</span></div>
|
||||
<div class="signal-range"><span>Medium (-80)</span><div class="signal-bar-bg"><div class="signal-bar medium" id="loraSignalMedium" style="width: 0%;"></div></div><span id="loraSignalMediumCount">0</span></div>
|
||||
<div class="signal-range"><span>Weak (-100)</span><div class="signal-bar-bg"><div class="signal-bar weak" id="loraSignalWeak" style="width: 0%;"></div></div><span id="loraSignalWeakCount">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wifi-visual-panel">
|
||||
<h5>📊 Band Activity</h5>
|
||||
<div id="loraBandActivity" style="font-size: 11px;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||
<span>Current Band:</span>
|
||||
<span id="loraCurrentBand" style="color: var(--accent-cyan);">--</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||
<span>Frequency:</span>
|
||||
<span id="loraCurrentFreq" style="color: var(--accent-green);">-- MHz</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span>Total Signals:</span>
|
||||
<span id="loraTotalSignals" style="color: var(--accent-orange);">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Row 3: Activity Log -->
|
||||
<div class="wifi-visual-panel" style="grid-column: span 2;">
|
||||
<h5>📜 Activity Log</h5>
|
||||
<div id="loraActivityLog" style="max-height: 120px; overflow-y: auto; font-size: 11px; font-family: 'JetBrains Mono', monospace;">
|
||||
<div style="color: var(--text-dim); padding: 10px; text-align: center;">Waiting for signals...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right: LoRa Device Cards -->
|
||||
<div class="wifi-device-list lora-device-list" id="loraDeviceListPanel">
|
||||
<div class="wifi-device-list-header">
|
||||
<h5>📶 LoRa/ISM Devices</h5>
|
||||
<span class="device-count">(<span id="loraDeviceListCount">0</span>)</span>
|
||||
</div>
|
||||
<div class="wifi-device-list-content" id="loraDeviceListContent">
|
||||
<div style="color: var(--text-dim); text-align: center; padding: 30px;">
|
||||
Start monitoring to discover devices
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aircraft Visualizations - Leaflet Map -->
|
||||
<div class="wifi-visuals" id="aircraftVisuals" style="display: none;">
|
||||
<!-- Map Panel -->
|
||||
@@ -1966,7 +1805,6 @@
|
||||
let eventSource = null;
|
||||
let isRunning = false;
|
||||
let isSensorRunning = false;
|
||||
let isLoraRunning = false;
|
||||
let isAdsbRunning = false;
|
||||
let isWifiRunning = false;
|
||||
let isBtRunning = false;
|
||||
@@ -2470,7 +2308,6 @@
|
||||
// Stop any running scans when switching modes
|
||||
if (isRunning) stopDecoding();
|
||||
if (isSensorRunning) stopSensorDecoding();
|
||||
if (isLoraRunning) stopLoraMonitoring();
|
||||
if (isWifiRunning) stopWifiScan();
|
||||
if (isBtRunning) stopBtScan();
|
||||
if (isAdsbRunning) stopAdsbScan();
|
||||
@@ -2479,9 +2316,9 @@
|
||||
// Remove active from all nav buttons, then add to the correct one
|
||||
document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active'));
|
||||
const modeMap = {
|
||||
'pager': 'pager', 'sensor': '433', 'lora': 'lora',
|
||||
'aircraft': 'aircraft', 'satellite': 'satellite', 'wifi': 'wifi',
|
||||
'bluetooth': 'bluetooth', 'listening': 'listening'
|
||||
'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft',
|
||||
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
|
||||
'listening': 'listening'
|
||||
};
|
||||
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
|
||||
const label = btn.querySelector('.nav-label');
|
||||
@@ -2491,7 +2328,6 @@
|
||||
});
|
||||
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
|
||||
document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor');
|
||||
document.getElementById('loraMode').classList.toggle('active', mode === 'lora');
|
||||
document.getElementById('aircraftMode').classList.toggle('active', mode === 'aircraft');
|
||||
document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite');
|
||||
document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi');
|
||||
@@ -2499,7 +2335,6 @@
|
||||
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
|
||||
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
|
||||
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none';
|
||||
document.getElementById('loraStats').style.display = mode === 'lora' ? 'flex' : 'none';
|
||||
document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none';
|
||||
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none';
|
||||
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none';
|
||||
@@ -2511,7 +2346,6 @@
|
||||
// Update header stats groups
|
||||
document.getElementById('headerPagerStats').classList.toggle('active', mode === 'pager');
|
||||
document.getElementById('headerSensorStats').classList.toggle('active', mode === 'sensor');
|
||||
document.getElementById('headerLoraStats').classList.toggle('active', mode === 'lora');
|
||||
document.getElementById('headerAircraftStats').classList.toggle('active', mode === 'aircraft');
|
||||
document.getElementById('headerSatelliteStats').classList.toggle('active', mode === 'satellite');
|
||||
document.getElementById('headerWifiStats').classList.toggle('active', mode === 'wifi');
|
||||
@@ -2525,7 +2359,6 @@
|
||||
const modeNames = {
|
||||
'pager': 'PAGER',
|
||||
'sensor': '433MHZ',
|
||||
'lora': 'LORA/ISM',
|
||||
'aircraft': 'AIRCRAFT',
|
||||
'satellite': 'SATELLITE',
|
||||
'wifi': 'WIFI',
|
||||
@@ -2535,7 +2368,6 @@
|
||||
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
|
||||
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
|
||||
document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? 'flex' : 'none';
|
||||
document.getElementById('loraLayoutContainer').style.display = mode === 'lora' ? 'flex' : 'none';
|
||||
// Respect the "Show Radar Display" checkbox for aircraft mode
|
||||
const showRadar = document.getElementById('adsbEnableMap').checked;
|
||||
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none';
|
||||
@@ -2546,7 +2378,6 @@
|
||||
const titles = {
|
||||
'pager': 'Pager Decoder',
|
||||
'sensor': '433MHz Sensor Monitor',
|
||||
'lora': 'LoRa/ISM Band Monitor',
|
||||
'aircraft': 'ADS-B Aircraft Tracker',
|
||||
'satellite': 'Satellite Monitor',
|
||||
'wifi': 'WiFi Scanner',
|
||||
@@ -2572,7 +2403,7 @@
|
||||
}
|
||||
|
||||
// Show RTL-SDR device section for modes that use it
|
||||
document.getElementById('rtlDeviceSection').style.display = (mode === 'pager' || mode === 'sensor' || mode === 'lora' || mode === 'aircraft' || mode === 'listening') ? 'block' : 'none';
|
||||
document.getElementById('rtlDeviceSection').style.display = (mode === 'pager' || mode === 'sensor' || mode === 'aircraft' || mode === 'listening') ? 'block' : 'none';
|
||||
|
||||
// Toggle mode-specific tool status displays
|
||||
document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none';
|
||||
@@ -2779,345 +2610,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LoRa/ISM BAND MONITORING
|
||||
// ============================================
|
||||
|
||||
let loraEventSource = null;
|
||||
let loraDevices = {};
|
||||
let loraSignalCount = 0;
|
||||
let loraDeviceCount = 0;
|
||||
|
||||
// LoRa band definitions
|
||||
const LORA_BANDS = {
|
||||
'eu868': { name: 'EU 868 MHz', frequency: 868.0, channels: [868.1, 868.3, 868.5, 867.1, 867.3, 867.5, 867.7, 867.9] },
|
||||
'us915': { name: 'US 915 MHz', frequency: 915.0, channels: [902.3, 902.5, 902.7, 902.9, 903.1, 903.3, 903.5, 903.7] },
|
||||
'au915': { name: 'AU 915 MHz', frequency: 915.0, channels: [915.2, 915.4, 915.6, 915.8, 916.0, 916.2, 916.4, 916.6] },
|
||||
'as923': { name: 'AS 923 MHz', frequency: 923.0, channels: [923.2, 923.4, 923.6, 923.8, 924.0, 924.2, 924.4, 924.6] },
|
||||
'in865': { name: 'IN 865 MHz', frequency: 865.0, channels: [865.0625, 865.4025, 865.985] },
|
||||
'ism433': { name: 'ISM 433 MHz', frequency: 433.92, channels: [433.05, 433.42, 433.92, 434.42] }
|
||||
};
|
||||
|
||||
function onLoraBandChanged() {
|
||||
const bandId = document.getElementById('loraBandSelect').value;
|
||||
const band = LORA_BANDS[bandId];
|
||||
if (band) {
|
||||
document.getElementById('loraFrequency').value = band.frequency;
|
||||
updateLoraChannelButtons(bandId);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLoraChannelButtons(bandId) {
|
||||
const band = LORA_BANDS[bandId];
|
||||
const container = document.getElementById('loraChannelButtons');
|
||||
container.innerHTML = '';
|
||||
if (band && band.channels) {
|
||||
band.channels.slice(0, 4).forEach(freq => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'preset-btn';
|
||||
btn.textContent = freq;
|
||||
btn.onclick = () => document.getElementById('loraFrequency').value = freq;
|
||||
container.appendChild(btn);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setLoraRunning(running) {
|
||||
isLoraRunning = running;
|
||||
document.getElementById('statusDot').classList.toggle('running', running);
|
||||
document.getElementById('startLoraBtn').style.display = running ? 'none' : 'block';
|
||||
document.getElementById('stopLoraBtn').style.display = running ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function startLoraMonitoring() {
|
||||
const band = document.getElementById('loraBandSelect').value;
|
||||
const freq = document.getElementById('loraFrequency').value;
|
||||
const gain = document.getElementById('loraGain').value;
|
||||
const ppm = document.getElementById('loraPpm').value;
|
||||
const hop = document.getElementById('loraHop').checked;
|
||||
const device = getSelectedDevice();
|
||||
|
||||
// Check if device is available
|
||||
if (!checkDeviceAvailability('lora')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for remote SDR
|
||||
const remoteConfig = getRemoteSDRConfig();
|
||||
if (remoteConfig === false) return;
|
||||
|
||||
const config = {
|
||||
band: band,
|
||||
frequency: freq,
|
||||
gain: gain,
|
||||
ppm: ppm,
|
||||
device: device,
|
||||
hop_enabled: hop,
|
||||
sdr_type: getSelectedSDRType(),
|
||||
bias_t: getBiasTEnabled()
|
||||
};
|
||||
|
||||
if (remoteConfig) {
|
||||
config.rtl_tcp_host = remoteConfig.host;
|
||||
config.rtl_tcp_port = remoteConfig.port;
|
||||
}
|
||||
|
||||
fetch('/lora/start', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(config)
|
||||
}).then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
reserveDevice(parseInt(device), 'lora');
|
||||
setLoraRunning(true);
|
||||
startLoraStream();
|
||||
// Update band info
|
||||
document.getElementById('loraCurrentBand').textContent = LORA_BANDS[band]?.name || band;
|
||||
document.getElementById('loraCurrentFreq').textContent = freq + ' MHz';
|
||||
addLoraLogEntry('Started monitoring ' + LORA_BANDS[band]?.name + ' at ' + freq + ' MHz');
|
||||
} else {
|
||||
showError('Failed to start: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function stopLoraMonitoring() {
|
||||
fetch('/lora/stop', {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
releaseDevice('lora');
|
||||
setLoraRunning(false);
|
||||
if (loraEventSource) {
|
||||
loraEventSource.close();
|
||||
loraEventSource = null;
|
||||
}
|
||||
addLoraLogEntry('Monitoring stopped');
|
||||
});
|
||||
}
|
||||
|
||||
function startLoraStream() {
|
||||
if (loraEventSource) {
|
||||
loraEventSource.close();
|
||||
}
|
||||
|
||||
loraEventSource = new EventSource('/lora/stream');
|
||||
|
||||
loraEventSource.onopen = function() {
|
||||
addLoraLogEntry('Stream connected...');
|
||||
};
|
||||
|
||||
loraEventSource.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'lora_device') {
|
||||
handleLoraDevice(data);
|
||||
} else if (data.type === 'status') {
|
||||
if (data.text === 'stopped') {
|
||||
setLoraRunning(false);
|
||||
} else if (data.text === 'started') {
|
||||
addLoraLogEntry('Receiver started');
|
||||
}
|
||||
} else if (data.type === 'info') {
|
||||
addLoraLogEntry(data.text);
|
||||
} else if (data.type === 'error') {
|
||||
addLoraLogEntry('Error: ' + data.text, true);
|
||||
}
|
||||
};
|
||||
|
||||
loraEventSource.onerror = function(e) {
|
||||
console.error('LoRa stream error');
|
||||
addLoraLogEntry('Stream connection error', true);
|
||||
};
|
||||
}
|
||||
|
||||
function handleLoraDevice(data) {
|
||||
loraSignalCount++;
|
||||
document.getElementById('loraSignalCount').textContent = loraSignalCount;
|
||||
document.getElementById('headerLoraSignalCount').textContent = loraSignalCount;
|
||||
document.getElementById('loraTotalSignals').textContent = loraSignalCount;
|
||||
|
||||
// Create device key
|
||||
const deviceKey = (data.model || 'Unknown') + '_' + (data.id || data.channel || Math.random().toString(36).substr(2, 9));
|
||||
|
||||
// Track device
|
||||
if (!loraDevices[deviceKey]) {
|
||||
loraDeviceCount++;
|
||||
document.getElementById('loraDeviceCount').textContent = loraDeviceCount;
|
||||
document.getElementById('headerLoraDeviceCount').textContent = loraDeviceCount;
|
||||
document.getElementById('loraDeviceListCount').textContent = loraDeviceCount;
|
||||
}
|
||||
|
||||
loraDevices[deviceKey] = {
|
||||
...data,
|
||||
key: deviceKey,
|
||||
lastSeen: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Update visualizations
|
||||
updateLoraStats();
|
||||
addLoraDeviceCard(data, deviceKey);
|
||||
addLoraLogEntry('Signal: ' + (data.model || 'Unknown') + (data.id ? ' ID:' + data.id : ''));
|
||||
|
||||
// Update radar
|
||||
updateLoraRadar();
|
||||
}
|
||||
|
||||
function addLoraDeviceCard(data, deviceKey) {
|
||||
const container = document.getElementById('loraDeviceListContent');
|
||||
|
||||
// Remove placeholder
|
||||
const placeholder = container.querySelector('[style*="padding: 30px"]');
|
||||
if (placeholder) placeholder.remove();
|
||||
|
||||
// Check if card exists
|
||||
let card = container.querySelector(`[data-device-key="${deviceKey}"]`);
|
||||
if (!card) {
|
||||
card = document.createElement('div');
|
||||
card.className = 'wifi-device-card lora-device-card';
|
||||
card.setAttribute('data-device-key', deviceKey);
|
||||
card.onclick = () => selectLoraDevice(deviceKey);
|
||||
container.insertBefore(card, container.firstChild);
|
||||
}
|
||||
|
||||
const rssi = data.rssi || data.signal_quality;
|
||||
const signalClass = rssi && rssi > -50 ? 'strong' : rssi && rssi > -80 ? 'medium' : 'weak';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="device-card-header">
|
||||
<span class="device-name">${data.model || 'Unknown Device'}</span>
|
||||
<span class="device-signal ${signalClass}">${rssi ? rssi + ' dBm' : 'N/A'}</span>
|
||||
</div>
|
||||
<div class="device-card-details">
|
||||
${data.id ? `<span>ID: ${data.id}</span>` : ''}
|
||||
${data.channel ? `<span>CH: ${data.channel}</span>` : ''}
|
||||
${data.is_lora ? '<span style="color: var(--accent-green);">LoRa</span>' : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function selectLoraDevice(deviceKey) {
|
||||
const device = loraDevices[deviceKey];
|
||||
if (!device) return;
|
||||
|
||||
document.querySelectorAll('.lora-device-card').forEach(c => c.classList.remove('selected'));
|
||||
const card = document.querySelector(`[data-device-key="${deviceKey}"]`);
|
||||
if (card) card.classList.add('selected');
|
||||
|
||||
const container = document.getElementById('loraSelectedDevice');
|
||||
let details = '<div style="display: grid; grid-template-columns: auto 1fr; gap: 4px 12px;">';
|
||||
for (const [key, value] of Object.entries(device)) {
|
||||
if (value !== null && value !== undefined && !['type', 'key'].includes(key)) {
|
||||
const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
details += `<span style="color: var(--text-dim);">${label}:</span><span>${value}</span>`;
|
||||
}
|
||||
}
|
||||
details += '</div>';
|
||||
container.innerHTML = details;
|
||||
}
|
||||
|
||||
function updateLoraStats() {
|
||||
// Count device types
|
||||
let sensors = 0, meters = 0, lorawan = 0, other = 0;
|
||||
let strong = 0, medium = 0, weak = 0;
|
||||
|
||||
for (const device of Object.values(loraDevices)) {
|
||||
const model = (device.model || '').toLowerCase();
|
||||
if (model.includes('sensor') || model.includes('weather') || model.includes('temperature')) {
|
||||
sensors++;
|
||||
} else if (model.includes('meter') || model.includes('smart') || model.includes('utility')) {
|
||||
meters++;
|
||||
} else if (model.includes('lora') || model.includes('lpwan') || device.is_lora) {
|
||||
lorawan++;
|
||||
} else {
|
||||
other++;
|
||||
}
|
||||
|
||||
const rssi = device.rssi;
|
||||
if (rssi && rssi > -50) strong++;
|
||||
else if (rssi && rssi > -80) medium++;
|
||||
else weak++;
|
||||
}
|
||||
|
||||
document.getElementById('loraSensorCount').textContent = sensors;
|
||||
document.getElementById('loraMeterCount').textContent = meters;
|
||||
document.getElementById('loraLorawanCount').textContent = lorawan;
|
||||
document.getElementById('loraOtherTypeCount').textContent = other;
|
||||
|
||||
// Update signal distribution
|
||||
const total = Object.keys(loraDevices).length || 1;
|
||||
document.getElementById('loraSignalStrong').style.width = (strong / total * 100) + '%';
|
||||
document.getElementById('loraSignalMedium').style.width = (medium / total * 100) + '%';
|
||||
document.getElementById('loraSignalWeak').style.width = (weak / total * 100) + '%';
|
||||
document.getElementById('loraSignalStrongCount').textContent = strong;
|
||||
document.getElementById('loraSignalMediumCount').textContent = medium;
|
||||
document.getElementById('loraSignalWeakCount').textContent = weak;
|
||||
}
|
||||
|
||||
function addLoraLogEntry(text, isError = false) {
|
||||
const log = document.getElementById('loraActivityLog');
|
||||
const placeholder = log.querySelector('[style*="padding: 10px"]');
|
||||
if (placeholder) placeholder.remove();
|
||||
|
||||
const entry = document.createElement('div');
|
||||
entry.style.cssText = `padding: 2px 0; border-bottom: 1px solid rgba(255,255,255,0.1); color: ${isError ? 'var(--accent-red)' : 'var(--text-secondary)'};`;
|
||||
const time = new Date().toLocaleTimeString();
|
||||
entry.innerHTML = `<span style="color: var(--text-dim);">[${time}]</span> ${text}`;
|
||||
log.insertBefore(entry, log.firstChild);
|
||||
|
||||
// Limit entries
|
||||
while (log.children.length > 50) {
|
||||
log.removeChild(log.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLoraRadar() {
|
||||
const canvas = document.getElementById('loraRadarCanvas');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const w = canvas.width, h = canvas.height;
|
||||
const cx = w / 2, cy = h / 2;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Draw radar circles
|
||||
ctx.strokeStyle = 'rgba(0, 212, 255, 0.3)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let r = 20; r <= 60; r += 20) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw crosshairs
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx, 10);
|
||||
ctx.lineTo(cx, h - 10);
|
||||
ctx.moveTo(10, cy);
|
||||
ctx.lineTo(w - 10, cy);
|
||||
ctx.stroke();
|
||||
|
||||
// Plot devices
|
||||
let i = 0;
|
||||
for (const device of Object.values(loraDevices)) {
|
||||
const rssi = device.rssi || -100;
|
||||
const dist = Math.max(10, Math.min(60, (rssi + 120) * 0.6));
|
||||
const angle = (i * 137.5) * Math.PI / 180;
|
||||
const x = cx + dist * Math.cos(angle);
|
||||
const y = cy + dist * Math.sin(angle);
|
||||
|
||||
ctx.fillStyle = device.is_lora ? 'var(--accent-green)' : 'var(--accent-cyan)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize LoRa on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateLoraChannelButtons('eu868');
|
||||
});
|
||||
|
||||
// Audio alert settings
|
||||
let audioMuted = localStorage.getItem('audioMuted') === 'true';
|
||||
let audioContext = null;
|
||||
|
||||
Reference in New Issue
Block a user