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:
Smittix
2026-01-08 17:07:36 +00:00
parent f3b1865a79
commit a174884269
5 changed files with 5 additions and 893 deletions

12
app.py
View File

@@ -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})

View File

@@ -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)

View File

@@ -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
})

View File

@@ -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;

View File

@@ -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;