diff --git a/app.py b/app.py
index 8d778e7..eb069bc 100644
--- a/app.py
+++ b/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})
diff --git a/routes/__init__.py b/routes/__init__.py
index a311cf5..5b6a326 100644
--- a/routes/__init__.py
+++ b/routes/__init__.py
@@ -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)
diff --git a/routes/lora.py b/routes/lora.py
deleted file mode 100644
index 54cb258..0000000
--- a/routes/lora.py
+++ /dev/null
@@ -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
- })
diff --git a/static/css/index.css b/static/css/index.css
index 590a3e0..b1dbf26 100644
--- a/static/css/index.css
+++ b/static/css/index.css
@@ -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;
diff --git a/templates/index.html b/templates/index.html
index 09dbe7f..ab6ee3d 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -244,23 +244,6 @@
-
-
@@ -270,7 +253,6 @@
SDR / RF
-
@@ -504,70 +486,6 @@
-
-
-
-
LoRa Band
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
Device Patterns
-
- rtl_433 detects LoRa/LPWAN devices including:
- • Smart meters (water, gas, electric)
- • LoRaWAN gateways and nodes
- • IoT sensors and controllers
- • Agricultural monitoring systems
-
-
-
-
-
-
-
@@ -1451,81 +1365,6 @@
-
-
-
-
-
-
-
📋 Selected Device
-
-
Click a device to view details
-
-
-
-
-
-
Device Categories
-
-
🌡️ Sensors: 0
-
⚡ Meters: 0
-
📡 LoRaWAN: 0
-
🔵 Other: 0
-
-
-
-
-
-
📊 Band Activity
-
-
- Current Band:
- --
-
-
- Frequency:
- -- MHz
-
-
- Total Signals:
- 0
-
-
-
-
-
-
📜 Activity Log
-
-
Waiting for signals...
-
-
-
-
-
-
-
-
- Start monitoring to discover devices
-
-
-
-
-
@@ -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 = '
' + 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 = `
-
-
- ${data.id ? `ID: ${data.id}` : ''}
- ${data.channel ? `CH: ${data.channel}` : ''}
- ${data.is_lora ? 'LoRa' : ''}
-
- `;
- }
-
- 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 = '
';
- 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 += `${label}:${value}`;
- }
- }
- details += '
';
- 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 = `
[${time}] ${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;