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_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
satellite_lock = threading.Lock()
|
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
|
# 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),
|
'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),
|
'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),
|
'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': {
|
'data': {
|
||||||
'aircraft_count': len(adsb_aircraft),
|
'aircraft_count': len(adsb_aircraft),
|
||||||
@@ -323,7 +317,7 @@ def health_check() -> Response:
|
|||||||
@app.route('/killall', methods=['POST'])
|
@app.route('/killall', methods=['POST'])
|
||||||
def kill_all() -> Response:
|
def kill_all() -> Response:
|
||||||
"""Kill all decoder and WiFi processes."""
|
"""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
|
# Import adsb module to reset its state
|
||||||
from routes import adsb as adsb_module
|
from routes import adsb as adsb_module
|
||||||
@@ -357,10 +351,6 @@ def kill_all() -> Response:
|
|||||||
adsb_process = None
|
adsb_process = None
|
||||||
adsb_module.adsb_using_service = False
|
adsb_module.adsb_using_service = False
|
||||||
|
|
||||||
# Reset LoRa state
|
|
||||||
with lora_lock:
|
|
||||||
lora_process = None
|
|
||||||
|
|
||||||
return jsonify({'status': 'killed', 'processes': killed})
|
return jsonify({'status': 'killed', 'processes': killed})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ def register_blueprints(app):
|
|||||||
from .settings import settings_bp
|
from .settings import settings_bp
|
||||||
from .correlation import correlation_bp
|
from .correlation import correlation_bp
|
||||||
from .listening_post import listening_post_bp
|
from .listening_post import listening_post_bp
|
||||||
from .lora import lora_bp
|
|
||||||
|
|
||||||
app.register_blueprint(pager_bp)
|
app.register_blueprint(pager_bp)
|
||||||
app.register_blueprint(sensor_bp)
|
app.register_blueprint(sensor_bp)
|
||||||
@@ -24,4 +23,3 @@ def register_blueprints(app):
|
|||||||
app.register_blueprint(settings_bp)
|
app.register_blueprint(settings_bp)
|
||||||
app.register_blueprint(correlation_bp)
|
app.register_blueprint(correlation_bp)
|
||||||
app.register_blueprint(listening_post_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);
|
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) {
|
@media (max-width: 1200px) {
|
||||||
.bt-layout-container {
|
.bt-layout-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -244,23 +244,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -270,7 +253,6 @@
|
|||||||
<span class="mode-nav-label">SDR / RF</span>
|
<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 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('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('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('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>
|
<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>
|
</button>
|
||||||
</div>
|
</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 -->
|
<!-- WiFi MODE -->
|
||||||
<div id="wifiMode" class="mode-content">
|
<div id="wifiMode" class="mode-content">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@@ -1256,10 +1174,6 @@
|
|||||||
<div class="stats" id="satelliteStats" style="display: none;">
|
<div class="stats" id="satelliteStats" style="display: none;">
|
||||||
<div title="Upcoming Passes">🛰️ <span id="passCount">0</span></div>
|
<div title="Upcoming Passes">🛰️ <span id="passCount">0</span></div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1451,81 +1365,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Aircraft Visualizations - Leaflet Map -->
|
||||||
<div class="wifi-visuals" id="aircraftVisuals" style="display: none;">
|
<div class="wifi-visuals" id="aircraftVisuals" style="display: none;">
|
||||||
<!-- Map Panel -->
|
<!-- Map Panel -->
|
||||||
@@ -1966,7 +1805,6 @@
|
|||||||
let eventSource = null;
|
let eventSource = null;
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let isSensorRunning = false;
|
let isSensorRunning = false;
|
||||||
let isLoraRunning = false;
|
|
||||||
let isAdsbRunning = false;
|
let isAdsbRunning = false;
|
||||||
let isWifiRunning = false;
|
let isWifiRunning = false;
|
||||||
let isBtRunning = false;
|
let isBtRunning = false;
|
||||||
@@ -2470,7 +2308,6 @@
|
|||||||
// Stop any running scans when switching modes
|
// Stop any running scans when switching modes
|
||||||
if (isRunning) stopDecoding();
|
if (isRunning) stopDecoding();
|
||||||
if (isSensorRunning) stopSensorDecoding();
|
if (isSensorRunning) stopSensorDecoding();
|
||||||
if (isLoraRunning) stopLoraMonitoring();
|
|
||||||
if (isWifiRunning) stopWifiScan();
|
if (isWifiRunning) stopWifiScan();
|
||||||
if (isBtRunning) stopBtScan();
|
if (isBtRunning) stopBtScan();
|
||||||
if (isAdsbRunning) stopAdsbScan();
|
if (isAdsbRunning) stopAdsbScan();
|
||||||
@@ -2479,9 +2316,9 @@
|
|||||||
// Remove active from all nav buttons, then add to the correct one
|
// Remove active from all nav buttons, then add to the correct one
|
||||||
document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active'));
|
document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
const modeMap = {
|
const modeMap = {
|
||||||
'pager': 'pager', 'sensor': '433', 'lora': 'lora',
|
'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft',
|
||||||
'aircraft': 'aircraft', 'satellite': 'satellite', 'wifi': 'wifi',
|
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
|
||||||
'bluetooth': 'bluetooth', 'listening': 'listening'
|
'listening': 'listening'
|
||||||
};
|
};
|
||||||
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
|
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
|
||||||
const label = btn.querySelector('.nav-label');
|
const label = btn.querySelector('.nav-label');
|
||||||
@@ -2491,7 +2328,6 @@
|
|||||||
});
|
});
|
||||||
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
|
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
|
||||||
document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor');
|
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('aircraftMode').classList.toggle('active', mode === 'aircraft');
|
||||||
document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite');
|
document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite');
|
||||||
document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi');
|
document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi');
|
||||||
@@ -2499,7 +2335,6 @@
|
|||||||
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
|
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
|
||||||
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
|
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
|
||||||
document.getElementById('sensorStats').style.display = mode === 'sensor' ? '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('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none';
|
||||||
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none';
|
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none';
|
||||||
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none';
|
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none';
|
||||||
@@ -2511,7 +2346,6 @@
|
|||||||
// Update header stats groups
|
// Update header stats groups
|
||||||
document.getElementById('headerPagerStats').classList.toggle('active', mode === 'pager');
|
document.getElementById('headerPagerStats').classList.toggle('active', mode === 'pager');
|
||||||
document.getElementById('headerSensorStats').classList.toggle('active', mode === 'sensor');
|
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('headerAircraftStats').classList.toggle('active', mode === 'aircraft');
|
||||||
document.getElementById('headerSatelliteStats').classList.toggle('active', mode === 'satellite');
|
document.getElementById('headerSatelliteStats').classList.toggle('active', mode === 'satellite');
|
||||||
document.getElementById('headerWifiStats').classList.toggle('active', mode === 'wifi');
|
document.getElementById('headerWifiStats').classList.toggle('active', mode === 'wifi');
|
||||||
@@ -2525,7 +2359,6 @@
|
|||||||
const modeNames = {
|
const modeNames = {
|
||||||
'pager': 'PAGER',
|
'pager': 'PAGER',
|
||||||
'sensor': '433MHZ',
|
'sensor': '433MHZ',
|
||||||
'lora': 'LORA/ISM',
|
|
||||||
'aircraft': 'AIRCRAFT',
|
'aircraft': 'AIRCRAFT',
|
||||||
'satellite': 'SATELLITE',
|
'satellite': 'SATELLITE',
|
||||||
'wifi': 'WIFI',
|
'wifi': 'WIFI',
|
||||||
@@ -2535,7 +2368,6 @@
|
|||||||
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
|
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
|
||||||
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
|
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
|
||||||
document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? '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
|
// Respect the "Show Radar Display" checkbox for aircraft mode
|
||||||
const showRadar = document.getElementById('adsbEnableMap').checked;
|
const showRadar = document.getElementById('adsbEnableMap').checked;
|
||||||
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none';
|
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none';
|
||||||
@@ -2546,7 +2378,6 @@
|
|||||||
const titles = {
|
const titles = {
|
||||||
'pager': 'Pager Decoder',
|
'pager': 'Pager Decoder',
|
||||||
'sensor': '433MHz Sensor Monitor',
|
'sensor': '433MHz Sensor Monitor',
|
||||||
'lora': 'LoRa/ISM Band Monitor',
|
|
||||||
'aircraft': 'ADS-B Aircraft Tracker',
|
'aircraft': 'ADS-B Aircraft Tracker',
|
||||||
'satellite': 'Satellite Monitor',
|
'satellite': 'Satellite Monitor',
|
||||||
'wifi': 'WiFi Scanner',
|
'wifi': 'WiFi Scanner',
|
||||||
@@ -2572,7 +2403,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show RTL-SDR device section for modes that use it
|
// 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
|
// Toggle mode-specific tool status displays
|
||||||
document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none';
|
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
|
// Audio alert settings
|
||||||
let audioMuted = localStorage.getItem('audioMuted') === 'true';
|
let audioMuted = localStorage.getItem('audioMuted') === 'true';
|
||||||
let audioContext = null;
|
let audioContext = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user