mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Merge branch 'main' of https://github.com/smittix/intercept
This commit is contained in:
@@ -61,7 +61,9 @@ docker compose up -d
|
|||||||
|
|
||||||
### Open the Interface
|
### Open the Interface
|
||||||
|
|
||||||
After starting, open **http://localhost:5050** in your browser. The username and password is admin:admin
|
After starting, open **http://localhost:5050** in your browser. The username and password is <b>admin</b>:<b>admin</b>
|
||||||
|
|
||||||
|
The credentials can be change in the ADMIN_USERNAME & ADMIN_PASSWORD variables in config.py
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
8
app.py
8
app.py
@@ -134,6 +134,11 @@ aprs_rtl_process = None
|
|||||||
aprs_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
aprs_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
aprs_lock = threading.Lock()
|
aprs_lock = threading.Lock()
|
||||||
|
|
||||||
|
# RTLAMR utility meter reading
|
||||||
|
rtlamr_process = None
|
||||||
|
rtlamr_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
|
rtlamr_lock = threading.Lock()
|
||||||
|
|
||||||
# TSCM (Technical Surveillance Countermeasures)
|
# TSCM (Technical Surveillance Countermeasures)
|
||||||
tscm_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
tscm_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
tscm_lock = threading.Lock()
|
tscm_lock = threading.Lock()
|
||||||
@@ -225,7 +230,8 @@ def index() -> str:
|
|||||||
tools = {
|
tools = {
|
||||||
'rtl_fm': check_tool('rtl_fm'),
|
'rtl_fm': check_tool('rtl_fm'),
|
||||||
'multimon': check_tool('multimon-ng'),
|
'multimon': check_tool('multimon-ng'),
|
||||||
'rtl_433': check_tool('rtl_433')
|
'rtl_433': check_tool('rtl_433'),
|
||||||
|
'rtlamr': check_tool('rtlamr')
|
||||||
}
|
}
|
||||||
devices = [d.to_dict() for d in SDRFactory.detect_devices()]
|
devices = [d.to_dict() for d in SDRFactory.detect_devices()]
|
||||||
return render_template('index.html', tools=tools, devices=devices, version=VERSION, changelog=CHANGELOG)
|
return render_template('index.html', tools=tools, devices=devices, version=VERSION, changelog=CHANGELOG)
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ dependencies = [
|
|||||||
"pyserial>=3.5",
|
"pyserial>=3.5",
|
||||||
"Werkzeug>=3.1.5",
|
"Werkzeug>=3.1.5",
|
||||||
"flask-limiter>=2.5.4",
|
"flask-limiter>=2.5.4",
|
||||||
|
"bleak>=0.21.0",
|
||||||
|
"flask-sock",
|
||||||
|
"requests>=2.28.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Core dependencies
|
# Core dependencies
|
||||||
flask>=2.0.0
|
flask>=2.0.0
|
||||||
|
flask-limiter>=2.5.4
|
||||||
requests>=2.28.0
|
requests>=2.28.0
|
||||||
|
Werkzeug>=3.1.5
|
||||||
|
|
||||||
# BLE scanning with manufacturer data detection (optional - for TSCM)
|
# BLE scanning with manufacturer data detection (optional - for TSCM)
|
||||||
bleak>=0.21.0
|
bleak>=0.21.0
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ def register_blueprints(app):
|
|||||||
"""Register all route blueprints with the Flask app."""
|
"""Register all route blueprints with the Flask app."""
|
||||||
from .pager import pager_bp
|
from .pager import pager_bp
|
||||||
from .sensor import sensor_bp
|
from .sensor import sensor_bp
|
||||||
|
from .rtlamr import rtlamr_bp
|
||||||
from .wifi import wifi_bp
|
from .wifi import wifi_bp
|
||||||
from .bluetooth import bluetooth_bp
|
from .bluetooth import bluetooth_bp
|
||||||
from .adsb import adsb_bp
|
from .adsb import adsb_bp
|
||||||
@@ -18,6 +19,7 @@ def register_blueprints(app):
|
|||||||
|
|
||||||
app.register_blueprint(pager_bp)
|
app.register_blueprint(pager_bp)
|
||||||
app.register_blueprint(sensor_bp)
|
app.register_blueprint(sensor_bp)
|
||||||
|
app.register_blueprint(rtlamr_bp)
|
||||||
app.register_blueprint(wifi_bp)
|
app.register_blueprint(wifi_bp)
|
||||||
app.register_blueprint(bluetooth_bp)
|
app.register_blueprint(bluetooth_bp)
|
||||||
app.register_blueprint(adsb_bp)
|
app.register_blueprint(adsb_bp)
|
||||||
|
|||||||
250
routes/rtlamr.py
Normal file
250
routes/rtlamr.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
"""RTLAMR utility meter 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 sensor_logger as logger
|
||||||
|
from utils.validation import (
|
||||||
|
validate_frequency, validate_device_index, validate_gain, validate_ppm
|
||||||
|
)
|
||||||
|
from utils.sse import format_sse
|
||||||
|
from utils.process import safe_terminate, register_process
|
||||||
|
|
||||||
|
rtlamr_bp = Blueprint('rtlamr', __name__)
|
||||||
|
|
||||||
|
# Store rtl_tcp process separately
|
||||||
|
rtl_tcp_process = None
|
||||||
|
rtl_tcp_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None:
|
||||||
|
"""Stream rtlamr JSON output to queue."""
|
||||||
|
try:
|
||||||
|
app_module.rtlamr_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:
|
||||||
|
# rtlamr outputs JSON objects, one per line
|
||||||
|
data = json.loads(line)
|
||||||
|
data['type'] = 'rtlamr'
|
||||||
|
app_module.rtlamr_queue.put(data)
|
||||||
|
|
||||||
|
# Log if enabled
|
||||||
|
if app_module.logging_enabled:
|
||||||
|
try:
|
||||||
|
with open(app_module.log_file_path, 'a') as f:
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
f.write(f"{timestamp} | RTLAMR | {json.dumps(data)}\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Not JSON, send as raw
|
||||||
|
app_module.rtlamr_queue.put({'type': 'raw', 'text': line})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_module.rtlamr_queue.put({'type': 'error', 'text': str(e)})
|
||||||
|
finally:
|
||||||
|
process.wait()
|
||||||
|
app_module.rtlamr_queue.put({'type': 'status', 'text': 'stopped'})
|
||||||
|
with app_module.rtlamr_lock:
|
||||||
|
app_module.rtlamr_process = None
|
||||||
|
|
||||||
|
|
||||||
|
@rtlamr_bp.route('/start_rtlamr', methods=['POST'])
|
||||||
|
def start_rtlamr() -> Response:
|
||||||
|
global rtl_tcp_process
|
||||||
|
|
||||||
|
with app_module.rtlamr_lock:
|
||||||
|
if app_module.rtlamr_process:
|
||||||
|
return jsonify({'status': 'error', 'message': 'RTLAMR already running'}), 409
|
||||||
|
|
||||||
|
data = request.json or {}
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
try:
|
||||||
|
freq = validate_frequency(data.get('frequency', '912.0'))
|
||||||
|
gain = validate_gain(data.get('gain', '0'))
|
||||||
|
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.rtlamr_queue.empty():
|
||||||
|
try:
|
||||||
|
app_module.rtlamr_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Get message type (default to scm)
|
||||||
|
msgtype = data.get('msgtype', 'scm')
|
||||||
|
output_format = data.get('format', 'json')
|
||||||
|
|
||||||
|
# Start rtl_tcp first
|
||||||
|
with rtl_tcp_lock:
|
||||||
|
if not rtl_tcp_process:
|
||||||
|
logger.info("Starting rtl_tcp server...")
|
||||||
|
try:
|
||||||
|
rtl_tcp_cmd = ['rtl_tcp', '-a', '0.0.0.0']
|
||||||
|
|
||||||
|
# Add device index if not 0
|
||||||
|
if device and device != '0':
|
||||||
|
rtl_tcp_cmd.extend(['-d', str(device)])
|
||||||
|
|
||||||
|
# Add gain if not auto
|
||||||
|
if gain and gain != '0':
|
||||||
|
rtl_tcp_cmd.extend(['-g', str(gain)])
|
||||||
|
|
||||||
|
# Add PPM correction if not 0
|
||||||
|
if ppm and ppm != '0':
|
||||||
|
rtl_tcp_cmd.extend(['-p', str(ppm)])
|
||||||
|
|
||||||
|
rtl_tcp_process = subprocess.Popen(
|
||||||
|
rtl_tcp_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait a moment for rtl_tcp to start
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
logger.info(f"rtl_tcp started: {' '.join(rtl_tcp_cmd)}")
|
||||||
|
app_module.rtlamr_queue.put({'type': 'info', 'text': f'rtl_tcp: {" ".join(rtl_tcp_cmd)}'})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start rtl_tcp: {e}")
|
||||||
|
return jsonify({'status': 'error', 'message': f'Failed to start rtl_tcp: {e}'}), 500
|
||||||
|
|
||||||
|
# Build rtlamr command
|
||||||
|
cmd = [
|
||||||
|
'rtlamr',
|
||||||
|
'-server=127.0.0.1:1234',
|
||||||
|
f'-msgtype={msgtype}',
|
||||||
|
f'-format={output_format}',
|
||||||
|
f'-centerfreq={int(float(freq) * 1e6)}'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add filter options if provided
|
||||||
|
filterid = data.get('filterid')
|
||||||
|
if filterid:
|
||||||
|
cmd.append(f'-filterid={filterid}')
|
||||||
|
|
||||||
|
filtertype = data.get('filtertype')
|
||||||
|
if filtertype:
|
||||||
|
cmd.append(f'-filtertype={filtertype}')
|
||||||
|
|
||||||
|
# Unique messages only
|
||||||
|
if data.get('unique', True):
|
||||||
|
cmd.append('-unique=true')
|
||||||
|
|
||||||
|
full_cmd = ' '.join(cmd)
|
||||||
|
logger.info(f"Running: {full_cmd}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
app_module.rtlamr_process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start output thread
|
||||||
|
thread = threading.Thread(target=stream_rtlamr_output, args=(app_module.rtlamr_process,))
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
# Monitor stderr
|
||||||
|
def monitor_stderr():
|
||||||
|
for line in app_module.rtlamr_process.stderr:
|
||||||
|
err = line.decode('utf-8', errors='replace').strip()
|
||||||
|
if err:
|
||||||
|
logger.debug(f"[rtlamr] {err}")
|
||||||
|
app_module.rtlamr_queue.put({'type': 'info', 'text': f'[rtlamr] {err}'})
|
||||||
|
|
||||||
|
stderr_thread = threading.Thread(target=monitor_stderr)
|
||||||
|
stderr_thread.daemon = True
|
||||||
|
stderr_thread.start()
|
||||||
|
|
||||||
|
app_module.rtlamr_queue.put({'type': 'info', 'text': f'Command: {full_cmd}'})
|
||||||
|
|
||||||
|
return jsonify({'status': 'started', 'command': full_cmd})
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
# If rtlamr fails, clean up rtl_tcp
|
||||||
|
with rtl_tcp_lock:
|
||||||
|
if rtl_tcp_process:
|
||||||
|
rtl_tcp_process.terminate()
|
||||||
|
rtl_tcp_process.wait(timeout=2)
|
||||||
|
rtl_tcp_process = None
|
||||||
|
return jsonify({'status': 'error', 'message': 'rtlamr not found. Install from https://github.com/bemasher/rtlamr'})
|
||||||
|
except Exception as e:
|
||||||
|
# If rtlamr fails, clean up rtl_tcp
|
||||||
|
with rtl_tcp_lock:
|
||||||
|
if rtl_tcp_process:
|
||||||
|
rtl_tcp_process.terminate()
|
||||||
|
rtl_tcp_process.wait(timeout=2)
|
||||||
|
rtl_tcp_process = None
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
@rtlamr_bp.route('/stop_rtlamr', methods=['POST'])
|
||||||
|
def stop_rtlamr() -> Response:
|
||||||
|
global rtl_tcp_process
|
||||||
|
|
||||||
|
with app_module.rtlamr_lock:
|
||||||
|
if app_module.rtlamr_process:
|
||||||
|
app_module.rtlamr_process.terminate()
|
||||||
|
try:
|
||||||
|
app_module.rtlamr_process.wait(timeout=2)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
app_module.rtlamr_process.kill()
|
||||||
|
app_module.rtlamr_process = None
|
||||||
|
|
||||||
|
# Also stop rtl_tcp
|
||||||
|
with rtl_tcp_lock:
|
||||||
|
if rtl_tcp_process:
|
||||||
|
rtl_tcp_process.terminate()
|
||||||
|
try:
|
||||||
|
rtl_tcp_process.wait(timeout=2)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
rtl_tcp_process.kill()
|
||||||
|
rtl_tcp_process = None
|
||||||
|
logger.info("rtl_tcp stopped")
|
||||||
|
|
||||||
|
return jsonify({'status': 'stopped'})
|
||||||
|
|
||||||
|
|
||||||
|
@rtlamr_bp.route('/stream_rtlamr')
|
||||||
|
def stream_rtlamr() -> Response:
|
||||||
|
def generate() -> Generator[str, None, None]:
|
||||||
|
last_keepalive = time.time()
|
||||||
|
keepalive_interval = 30.0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
msg = app_module.rtlamr_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
|
||||||
92
setup.sh
92
setup.sh
@@ -178,6 +178,17 @@ check_required() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_optional() {
|
||||||
|
local label="$1"; shift
|
||||||
|
local desc="$1"; shift
|
||||||
|
|
||||||
|
if have_any "$@"; then
|
||||||
|
ok "${label} - ${desc}"
|
||||||
|
else
|
||||||
|
warn "${label} - ${desc} (missing, optional)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
check_tools() {
|
check_tools() {
|
||||||
info "Checking required tools..."
|
info "Checking required tools..."
|
||||||
missing_required=()
|
missing_required=()
|
||||||
@@ -186,8 +197,10 @@ check_tools() {
|
|||||||
info "Core SDR:"
|
info "Core SDR:"
|
||||||
check_required "rtl_fm" "RTL-SDR FM demodulator" rtl_fm
|
check_required "rtl_fm" "RTL-SDR FM demodulator" rtl_fm
|
||||||
check_required "rtl_test" "RTL-SDR device detection" rtl_test
|
check_required "rtl_test" "RTL-SDR device detection" rtl_test
|
||||||
|
check_required "rtl_tcp" "RTL-SDR TCP server" rtl_tcp
|
||||||
check_required "multimon-ng" "Pager decoder" multimon-ng
|
check_required "multimon-ng" "Pager decoder" multimon-ng
|
||||||
check_required "rtl_433" "433MHz sensor decoder" rtl_433 rtl433
|
check_required "rtl_433" "433MHz sensor decoder" rtl_433 rtl433
|
||||||
|
check_optional "rtlamr" "Utility meter decoder (requires Go)" rtlamr
|
||||||
check_required "dump1090" "ADS-B decoder" dump1090
|
check_required "dump1090" "ADS-B decoder" dump1090
|
||||||
check_required "acarsdec" "ACARS decoder" acarsdec
|
check_required "acarsdec" "ACARS decoder" acarsdec
|
||||||
|
|
||||||
@@ -280,9 +293,9 @@ install_python_deps() {
|
|||||||
if ! python -m pip install -r requirements.txt 2>/dev/null; then
|
if ! python -m pip install -r requirements.txt 2>/dev/null; then
|
||||||
warn "Some pip packages failed - checking if apt packages cover them..."
|
warn "Some pip packages failed - checking if apt packages cover them..."
|
||||||
# Verify critical packages are available
|
# Verify critical packages are available
|
||||||
python -c "import flask; import requests" 2>/dev/null || {
|
python -c "import flask; import requests; from flask_limiter import Limiter" 2>/dev/null || {
|
||||||
fail "Critical Python packages (flask, requests) not installed"
|
fail "Critical Python packages (flask, requests, flask-limiter) not installed"
|
||||||
echo "Try: sudo apt install python3-flask python3-requests"
|
echo "Try: pip install flask requests flask-limiter"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
ok "Core Python dependencies available"
|
ok "Core Python dependencies available"
|
||||||
@@ -324,6 +337,49 @@ brew_install() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install_rtlamr_from_source() {
|
||||||
|
info "Installing rtlamr from source (requires Go)..."
|
||||||
|
|
||||||
|
# Check if Go is installed, install if needed
|
||||||
|
if ! cmd_exists go; then
|
||||||
|
if [[ "$OS" == "macos" ]]; then
|
||||||
|
info "Installing Go via Homebrew..."
|
||||||
|
brew_install go || { warn "Failed to install Go. Cannot install rtlamr."; return 1; }
|
||||||
|
else
|
||||||
|
info "Installing Go via apt..."
|
||||||
|
$SUDO apt-get install -y golang >/dev/null 2>&1 || { warn "Failed to install Go. Cannot install rtlamr."; return 1; }
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set up Go environment
|
||||||
|
export GOPATH="${GOPATH:-$HOME/go}"
|
||||||
|
export PATH="$GOPATH/bin:$PATH"
|
||||||
|
mkdir -p "$GOPATH/bin"
|
||||||
|
|
||||||
|
info "Building rtlamr..."
|
||||||
|
if go install github.com/bemasher/rtlamr@latest 2>/dev/null; then
|
||||||
|
# Link to system path
|
||||||
|
if [[ -f "$GOPATH/bin/rtlamr" ]]; then
|
||||||
|
if [[ "$OS" == "macos" ]]; then
|
||||||
|
if [[ -w /usr/local/bin ]]; then
|
||||||
|
ln -sf "$GOPATH/bin/rtlamr" /usr/local/bin/rtlamr
|
||||||
|
else
|
||||||
|
sudo ln -sf "$GOPATH/bin/rtlamr" /usr/local/bin/rtlamr
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
$SUDO ln -sf "$GOPATH/bin/rtlamr" /usr/local/bin/rtlamr
|
||||||
|
fi
|
||||||
|
ok "rtlamr installed successfully"
|
||||||
|
else
|
||||||
|
warn "rtlamr binary not found after build"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Failed to build rtlamr"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
install_multimon_ng_from_source_macos() {
|
install_multimon_ng_from_source_macos() {
|
||||||
info "multimon-ng not available via Homebrew. Building from source..."
|
info "multimon-ng not available via Homebrew. Building from source..."
|
||||||
|
|
||||||
@@ -382,6 +438,21 @@ install_macos_packages() {
|
|||||||
progress "Installing rtl_433"
|
progress "Installing rtl_433"
|
||||||
brew_install rtl_433
|
brew_install rtl_433
|
||||||
|
|
||||||
|
progress "Installing rtlamr (optional)"
|
||||||
|
# rtlamr is optional - used for utility meter monitoring
|
||||||
|
if ! cmd_exists rtlamr; then
|
||||||
|
echo
|
||||||
|
info "rtlamr is used for utility meter monitoring (electric/gas/water meters)."
|
||||||
|
read -r -p "Do you want to install rtlamr? [y/N] " install_rtlamr
|
||||||
|
if [[ "$install_rtlamr" =~ ^[Yy]$ ]]; then
|
||||||
|
install_rtlamr_from_source
|
||||||
|
else
|
||||||
|
warn "Skipping rtlamr installation. You can install it later if needed."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ok "rtlamr already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
progress "Installing dump1090"
|
progress "Installing dump1090"
|
||||||
(brew_install dump1090-mutability) || warn "dump1090 not available via Homebrew"
|
(brew_install dump1090-mutability) || warn "dump1090 not available via Homebrew"
|
||||||
|
|
||||||
@@ -686,6 +757,21 @@ install_debian_packages() {
|
|||||||
progress "Installing rtl_433"
|
progress "Installing rtl_433"
|
||||||
apt_try_install_any rtl-433 rtl433 || warn "rtl-433 not available"
|
apt_try_install_any rtl-433 rtl433 || warn "rtl-433 not available"
|
||||||
|
|
||||||
|
progress "Installing rtlamr (optional)"
|
||||||
|
# rtlamr is optional - used for utility meter monitoring
|
||||||
|
if ! cmd_exists rtlamr; then
|
||||||
|
echo
|
||||||
|
info "rtlamr is used for utility meter monitoring (electric/gas/water meters)."
|
||||||
|
read -r -p "Do you want to install rtlamr? [y/N] " install_rtlamr
|
||||||
|
if [[ "$install_rtlamr" =~ ^[Yy]$ ]]; then
|
||||||
|
install_rtlamr_from_source
|
||||||
|
else
|
||||||
|
warn "Skipping rtlamr installation. You can install it later if needed."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ok "rtlamr already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
progress "Installing aircrack-ng"
|
progress "Installing aircrack-ng"
|
||||||
apt_install aircrack-ng || true
|
apt_install aircrack-ng || true
|
||||||
|
|
||||||
|
|||||||
@@ -207,12 +207,16 @@ body {
|
|||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show ACARS sidebar on desktop */
|
/* Show ACARS sidebar on desktop */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.acars-sidebar {
|
.acars-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
max-height: calc(100dvh - 95px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,11 +268,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.acars-sidebar-content {
|
.acars-sidebar-content {
|
||||||
width: 250px;
|
width: 300px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: width 0.3s ease, opacity 0.2s ease;
|
transition: width 0.3s ease, opacity 0.2s ease;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.acars-sidebar.collapsed .acars-sidebar-content {
|
.acars-sidebar.collapsed .acars-sidebar-content {
|
||||||
@@ -283,12 +289,31 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.acars-sidebar .panel::before {
|
.acars-sidebar .panel::before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.acars-sidebar .panel-header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.acars-sidebar #acarsPanelContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.acars-sidebar .acars-info,
|
||||||
|
.acars-sidebar .acars-controls {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.acars-sidebar .acars-messages {
|
.acars-sidebar .acars-messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
34
static/js/core/login.js
Normal file
34
static/js/core/login.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Handles the visual transition and submission lock for the authorization terminal.
|
||||||
|
* @param {Event} event - The click event from the submission button.
|
||||||
|
*/
|
||||||
|
function login(event) {
|
||||||
|
const btn = event.currentTarget;
|
||||||
|
const form = btn.closest('form');
|
||||||
|
|
||||||
|
// Validate form requirements before triggering visual effects
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
return; // Allow the browser to handle native "required" field alerts
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Visual Feedback: Transition to "Processing" state
|
||||||
|
btn.style.color = "#ff4d4d";
|
||||||
|
btn.style.borderColor = "#ff4d4d";
|
||||||
|
btn.style.textShadow = "0 0 10px #ff4d4d";
|
||||||
|
btn.style.transform = "scale(0.95)";
|
||||||
|
|
||||||
|
// Update button text to reflect terminal status
|
||||||
|
const btnText = btn.querySelector('.btn-text');
|
||||||
|
if (btnText) {
|
||||||
|
btnText.innerText = "AUTHORIZING...";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Security Lock: Prevent redundant requests (Double-click spam)
|
||||||
|
// A 10ms delay ensures the browser successfully dispatches the POST request
|
||||||
|
// before the UI element becomes non-interactive.
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.style.pointerEvents = "none";
|
||||||
|
btn.style.opacity = "0.7";
|
||||||
|
btn.style.cursor = "not-allowed";
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
@@ -375,6 +375,8 @@
|
|||||||
class="nav-label">Pager</span></button>
|
class="nav-label">Pager</span></button>
|
||||||
<button class="mode-nav-btn" onclick="switchMode('sensor')"><span class="nav-icon">📡</span><span
|
<button class="mode-nav-btn" onclick="switchMode('sensor')"><span class="nav-icon">📡</span><span
|
||||||
class="nav-label">433MHz</span></button>
|
class="nav-label">433MHz</span></button>
|
||||||
|
<button class="mode-nav-btn" onclick="switchMode('rtlamr')"><span class="nav-icon">⚡</span><span
|
||||||
|
class="nav-label">Meters</span></button>
|
||||||
<a href="/adsb/dashboard" class="mode-nav-btn" style="text-decoration: none;"><span
|
<a href="/adsb/dashboard" class="mode-nav-btn" style="text-decoration: none;"><span
|
||||||
class="nav-icon">✈️</span><span class="nav-label">Aircraft</span></a>
|
class="nav-icon">✈️</span><span class="nav-label">Aircraft</span></a>
|
||||||
<button class="mode-nav-btn" onclick="switchMode('aprs')"><span class="nav-icon">📍</span><span
|
<button class="mode-nav-btn" onclick="switchMode('aprs')"><span class="nav-icon">📍</span><span
|
||||||
@@ -440,6 +442,7 @@
|
|||||||
<nav class="mobile-nav-bar" id="mobileNavBar">
|
<nav class="mobile-nav-bar" id="mobileNavBar">
|
||||||
<button class="mobile-nav-btn active" data-mode="pager" onclick="switchMode('pager')">📟 Pager</button>
|
<button class="mobile-nav-btn active" data-mode="pager" onclick="switchMode('pager')">📟 Pager</button>
|
||||||
<button class="mobile-nav-btn" data-mode="sensor" onclick="switchMode('sensor')">📡 433MHz</button>
|
<button class="mobile-nav-btn" data-mode="sensor" onclick="switchMode('sensor')">📡 433MHz</button>
|
||||||
|
<button class="mobile-nav-btn" data-mode="rtlamr" onclick="switchMode('rtlamr')">⚡ Meters</button>
|
||||||
<a href="/adsb/dashboard" class="mobile-nav-btn" style="text-decoration: none;">✈️ Aircraft</a>
|
<a href="/adsb/dashboard" class="mobile-nav-btn" style="text-decoration: none;">✈️ Aircraft</a>
|
||||||
<button class="mobile-nav-btn" data-mode="aprs" onclick="switchMode('aprs')">📍 APRS</button>
|
<button class="mobile-nav-btn" data-mode="aprs" onclick="switchMode('aprs')">📍 APRS</button>
|
||||||
<button class="mobile-nav-btn" data-mode="wifi" onclick="switchMode('wifi')">📶 WiFi</button>
|
<button class="mobile-nav-btn" data-mode="wifi" onclick="switchMode('wifi')">📶 WiFi</button>
|
||||||
@@ -543,6 +546,8 @@
|
|||||||
|
|
||||||
{% include 'partials/modes/sensor.html' %}
|
{% include 'partials/modes/sensor.html' %}
|
||||||
|
|
||||||
|
{% include 'partials/modes/rtlamr.html' %}
|
||||||
|
|
||||||
{% include 'partials/modes/wifi.html' %}
|
{% include 'partials/modes/wifi.html' %}
|
||||||
|
|
||||||
{% include 'partials/modes/bluetooth.html' %}
|
{% include 'partials/modes/bluetooth.html' %}
|
||||||
@@ -1974,6 +1979,7 @@
|
|||||||
});
|
});
|
||||||
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('rtlamrMode').classList.toggle('active', mode === 'rtlamr');
|
||||||
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');
|
||||||
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
|
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
|
||||||
@@ -2000,6 +2006,7 @@
|
|||||||
const modeNames = {
|
const modeNames = {
|
||||||
'pager': 'PAGER',
|
'pager': 'PAGER',
|
||||||
'sensor': '433MHZ',
|
'sensor': '433MHZ',
|
||||||
|
'rtlamr': 'METERS',
|
||||||
'satellite': 'SATELLITE',
|
'satellite': 'SATELLITE',
|
||||||
'wifi': 'WIFI',
|
'wifi': 'WIFI',
|
||||||
'bluetooth': 'BLUETOOTH',
|
'bluetooth': 'BLUETOOTH',
|
||||||
@@ -2019,6 +2026,7 @@
|
|||||||
const titles = {
|
const titles = {
|
||||||
'pager': 'Pager Decoder',
|
'pager': 'Pager Decoder',
|
||||||
'sensor': '433MHz Sensor Monitor',
|
'sensor': '433MHz Sensor Monitor',
|
||||||
|
'rtlamr': 'Utility Meter Monitor',
|
||||||
'satellite': 'Satellite Monitor',
|
'satellite': 'Satellite Monitor',
|
||||||
'wifi': 'WiFi Scanner',
|
'wifi': 'WiFi Scanner',
|
||||||
'bluetooth': 'Bluetooth Scanner',
|
'bluetooth': 'Bluetooth Scanner',
|
||||||
@@ -2051,7 +2059,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 === 'listening' || mode === 'aprs') ? 'block' : 'none';
|
document.getElementById('rtlDeviceSection').style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs') ? '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';
|
||||||
@@ -2276,6 +2284,183 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// RTLAMR Functions
|
||||||
|
// ========================================
|
||||||
|
let isRtlamrRunning = false;
|
||||||
|
|
||||||
|
function setRtlamrFreq(freq) {
|
||||||
|
document.getElementById('rtlamrFrequency').value = freq;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRtlamrDecoding() {
|
||||||
|
const freq = document.getElementById('rtlamrFrequency').value;
|
||||||
|
const gain = document.getElementById('rtlamrGain').value;
|
||||||
|
const ppm = document.getElementById('rtlamrPpm').value;
|
||||||
|
const device = getSelectedDevice();
|
||||||
|
const msgtype = document.getElementById('rtlamrMsgType').value;
|
||||||
|
const filterid = document.getElementById('rtlamrFilterId').value;
|
||||||
|
const unique = document.getElementById('rtlamrUnique').checked;
|
||||||
|
|
||||||
|
// Check if device is available
|
||||||
|
if (!checkDeviceAvailability('rtlamr')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
frequency: freq,
|
||||||
|
gain: gain,
|
||||||
|
ppm: ppm,
|
||||||
|
device: device,
|
||||||
|
msgtype: msgtype,
|
||||||
|
filterid: filterid,
|
||||||
|
unique: unique,
|
||||||
|
format: 'json'
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/start_rtlamr', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
}).then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'started') {
|
||||||
|
reserveDevice(parseInt(device), 'rtlamr');
|
||||||
|
setRtlamrRunning(true);
|
||||||
|
startRtlamrStream();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopRtlamrDecoding() {
|
||||||
|
fetch('/stop_rtlamr', { method: 'POST' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
releaseDevice('rtlamr');
|
||||||
|
setRtlamrRunning(false);
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
eventSource = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRtlamrRunning(running) {
|
||||||
|
isRtlamrRunning = running;
|
||||||
|
document.getElementById('statusDot').classList.toggle('running', running);
|
||||||
|
document.getElementById('statusText').textContent = running ? 'Listening...' : 'Idle';
|
||||||
|
document.getElementById('startRtlamrBtn').style.display = running ? 'none' : 'block';
|
||||||
|
document.getElementById('stopRtlamrBtn').style.display = running ? 'block' : 'none';
|
||||||
|
|
||||||
|
// Update mode indicator with frequency
|
||||||
|
if (running) {
|
||||||
|
const freq = document.getElementById('rtlamrFrequency').value;
|
||||||
|
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>METERS @ ' + freq + ' MHz';
|
||||||
|
} else {
|
||||||
|
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>METERS';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRtlamrStream() {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource = new EventSource('/stream_rtlamr');
|
||||||
|
|
||||||
|
eventSource.onopen = function () {
|
||||||
|
showInfo('RTLAMR stream connected...');
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onmessage = function (e) {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
if (data.type === 'rtlamr') {
|
||||||
|
addRtlamrReading(data);
|
||||||
|
} else if (data.type === 'status') {
|
||||||
|
if (data.text === 'stopped') {
|
||||||
|
setRtlamrRunning(false);
|
||||||
|
}
|
||||||
|
} else if (data.type === 'info' || data.type === 'raw') {
|
||||||
|
showInfo(data.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function (e) {
|
||||||
|
console.error('RTLAMR stream error');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRtlamrReading(data) {
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
const placeholder = output.querySelector('.placeholder');
|
||||||
|
if (placeholder) placeholder.remove();
|
||||||
|
|
||||||
|
// Store for export
|
||||||
|
allMessages.push(data);
|
||||||
|
playAlert();
|
||||||
|
pulseSignal();
|
||||||
|
|
||||||
|
sensorCount++;
|
||||||
|
document.getElementById('sensorCount').textContent = sensorCount;
|
||||||
|
|
||||||
|
// Track unique meters by ID
|
||||||
|
const meterId = data.Message?.ID || 'Unknown';
|
||||||
|
if (meterId !== 'Unknown') {
|
||||||
|
const deviceKey = 'METER_' + meterId;
|
||||||
|
if (!uniqueDevices.has(deviceKey)) {
|
||||||
|
uniqueDevices.add(deviceKey);
|
||||||
|
document.getElementById('deviceCount').textContent = uniqueDevices.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'sensor-card';
|
||||||
|
|
||||||
|
let dataItems = '';
|
||||||
|
const msg = data.Message || {};
|
||||||
|
|
||||||
|
// Build display from message data
|
||||||
|
for (const [key, value] of Object.entries(msg)) {
|
||||||
|
if (value !== null && value !== undefined) {
|
||||||
|
const label = key.replace(/_/g, ' ');
|
||||||
|
let displayValue = value;
|
||||||
|
if (key === 'Consumption') displayValue = value + ' units';
|
||||||
|
dataItems += `<div class="sensor-item"><span class="sensor-label">${label}:</span> <span class="sensor-value">${displayValue}</span></div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="sensor-header">
|
||||||
|
<span class="sensor-model">${data.Type || 'Meter'}</span>
|
||||||
|
<span class="sensor-time">${timestamp}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sensor-data">${dataItems}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
output.insertBefore(card, output.firstChild);
|
||||||
|
|
||||||
|
// Limit output to 50 cards
|
||||||
|
while (output.children.length > 50) {
|
||||||
|
output.removeChild(output.lastChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRtlamrUnique() {
|
||||||
|
// No action needed, value is read on start
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRtlamrLogging() {
|
||||||
|
const enabled = document.getElementById('rtlamrLogging').checked;
|
||||||
|
fetch('/logging', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ enabled: enabled, log_file: 'rtlamr_data.log' })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: Audio alert settings moved to static/js/core/audio.js
|
// NOTE: Audio alert settings moved to static/js/core/audio.js
|
||||||
|
|
||||||
// Message storage for export
|
// Message storage for export
|
||||||
@@ -10066,6 +10251,8 @@
|
|||||||
decoder</span></div>
|
decoder</span></div>
|
||||||
<div class="icon-item"><span class="icon">📡</span><span class="desc">433MHz - Sensor decoder</span>
|
<div class="icon-item"><span class="icon">📡</span><span class="desc">433MHz - Sensor decoder</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="icon-item"><span class="icon">⚡</span><span class="desc">Meters - Utility meter decoder</span>
|
||||||
|
</div>
|
||||||
<div class="icon-item"><span class="icon">✈️</span><span class="desc">Aircraft - Opens ADS-B
|
<div class="icon-item"><span class="icon">✈️</span><span class="desc">Aircraft - Opens ADS-B
|
||||||
Dashboard</span></div>
|
Dashboard</span></div>
|
||||||
<div class="icon-item"><span class="icon">📍</span><span class="desc">APRS - Amateur radio
|
<div class="icon-item"><span class="icon">📍</span><span class="desc">APRS - Amateur radio
|
||||||
@@ -10101,6 +10288,14 @@
|
|||||||
<li>Device intelligence builds profiles of recurring devices</li>
|
<li>Device intelligence builds profiles of recurring devices</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<h3>⚡ Utility Meter Mode</h3>
|
||||||
|
<ul class="tip-list">
|
||||||
|
<li>Decodes utility meter transmissions (water, gas, electric) using rtlamr</li>
|
||||||
|
<li>Supports ERT protocol on 912 MHz (North America) or 868 MHz (Europe)</li>
|
||||||
|
<li>Displays meter IDs and consumption data in real-time</li>
|
||||||
|
<li>Supports SCM, SCM+, IDM, NetIDM, and R900 message types</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h3>✈️ Aircraft (Dashboard)</h3>
|
<h3>✈️ Aircraft (Dashboard)</h3>
|
||||||
<ul class="tip-list">
|
<ul class="tip-list">
|
||||||
<li>Opens the dedicated ADS-B Dashboard for aircraft tracking</li>
|
<li>Opens the dedicated ADS-B Dashboard for aircraft tracking</li>
|
||||||
@@ -10233,6 +10428,7 @@
|
|||||||
<ul class="tip-list">
|
<ul class="tip-list">
|
||||||
<li><strong>Pager:</strong> RTL-SDR, rtl_fm, multimon-ng</li>
|
<li><strong>Pager:</strong> RTL-SDR, rtl_fm, multimon-ng</li>
|
||||||
<li><strong>433MHz Sensors:</strong> RTL-SDR, rtl_433</li>
|
<li><strong>433MHz Sensors:</strong> RTL-SDR, rtl_433</li>
|
||||||
|
<li><strong>Utility Meters:</strong> RTL-SDR, rtl_tcp, rtlamr</li>
|
||||||
<li><strong>Aircraft (ADS-B):</strong> RTL-SDR, dump1090 or rtl_adsb</li>
|
<li><strong>Aircraft (ADS-B):</strong> RTL-SDR, dump1090 or rtl_adsb</li>
|
||||||
<li><strong>Aircraft (ACARS):</strong> Second RTL-SDR, acarsdec</li>
|
<li><strong>Aircraft (ACARS):</strong> Second RTL-SDR, acarsdec</li>
|
||||||
<li><strong>APRS:</strong> RTL-SDR, direwolf or multimon-ng</li>
|
<li><strong>APRS:</strong> RTL-SDR, direwolf or multimon-ng</li>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>iNTERCEPT // Restricted Access</title>
|
<title>iNTERCEPT // Restricted Access</title>
|
||||||
|
<script src="{{ url_for('static', filename='js/core/login.js') }}"></script>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}" />
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}" />
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}" />
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}" />
|
||||||
</head>
|
</head>
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
<input type="text" name="username" placeholder="OPERATOR ID" class="form-input" required autofocus autocomplete="off" />
|
<input type="text" name="username" placeholder="OPERATOR ID" class="form-input" required autofocus autocomplete="off" />
|
||||||
<input type="password" name="password" placeholder="ENCRYPTION KEY" class="form-input" required />
|
<input type="password" name="password" placeholder="ENCRYPTION KEY" class="form-input" required />
|
||||||
|
|
||||||
<button type="submit" class="landing-enter-btn">
|
<button type="submit" class="landing-enter-btn" onclick="login(event)">
|
||||||
<span class="btn-text">INITIALIZE SESSION</span>
|
<span class="btn-text">INITIALIZE SESSION</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
67
templates/partials/modes/rtlamr.html
Normal file
67
templates/partials/modes/rtlamr.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<!-- RTLAMR UTILITY METER MODE -->
|
||||||
|
<div id="rtlamrMode" class="mode-content">
|
||||||
|
<div class="section">
|
||||||
|
<h3>Frequency</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Frequency (MHz)</label>
|
||||||
|
<input type="text" id="rtlamrFrequency" value="912.0" placeholder="e.g., 912.0">
|
||||||
|
</div>
|
||||||
|
<div class="preset-buttons">
|
||||||
|
<button class="preset-btn" onclick="setRtlamrFreq('912.0')">912.0 (NA)</button>
|
||||||
|
<button class="preset-btn" onclick="setRtlamrFreq('868.0')">868.0 (EU)</button>
|
||||||
|
<button class="preset-btn" onclick="setRtlamrFreq('915.0')">915.0</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Settings</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Gain (dB, 0 = auto)</label>
|
||||||
|
<input type="text" id="rtlamrGain" value="0" placeholder="0-49 or 0 for auto">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>PPM Correction</label>
|
||||||
|
<input type="text" id="rtlamrPpm" value="0" placeholder="Frequency correction">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Message Type</label>
|
||||||
|
<select id="rtlamrMsgType">
|
||||||
|
<option value="scm">SCM (Standard Consumption Message)</option>
|
||||||
|
<option value="scm+">SCM+ (Enhanced)</option>
|
||||||
|
<option value="idm">IDM (Interval Data Message)</option>
|
||||||
|
<option value="netidm">NetIDM (Network IDM)</option>
|
||||||
|
<option value="r900">R900 (Neptune)</option>
|
||||||
|
<option value="r900bcd">R900 BCD</option>
|
||||||
|
<option value="all">All Types</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Filter by Meter ID (optional, comma-separated)</label>
|
||||||
|
<input type="text" id="rtlamrFilterId" placeholder="e.g., 12345678,87654321">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Protocols</h3>
|
||||||
|
<div class="info-text" style="margin-bottom: 10px;">
|
||||||
|
rtlamr decodes utility meter transmissions (water, gas, electric) using ERT protocol.
|
||||||
|
</div>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="rtlamrUnique" checked onchange="toggleRtlamrUnique()">
|
||||||
|
Unique Messages Only
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="rtlamrLogging" onchange="toggleRtlamrLogging()">
|
||||||
|
Enable Logging
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="run-btn" id="startRtlamrBtn" onclick="startRtlamrDecoding()">
|
||||||
|
Start Listening
|
||||||
|
</button>
|
||||||
|
<button class="stop-btn" id="stopRtlamrBtn" onclick="stopRtlamrDecoding()" style="display: none;">
|
||||||
|
Stop Listening
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
109
tests/test_requirements.py
Normal file
109
tests/test_requirements.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
import importlib.metadata
|
||||||
|
import tomllib # Standard in Python 3.11+
|
||||||
|
|
||||||
|
def get_root_path():
|
||||||
|
return Path(__file__).parent.parent
|
||||||
|
|
||||||
|
def _clean_string(req):
|
||||||
|
"""Normalizes a requirement string (lowercase and removes spaces)."""
|
||||||
|
return req.strip().lower().replace(" ", "")
|
||||||
|
|
||||||
|
def parse_txt_requirements(file_path):
|
||||||
|
"""Extracts full requirement strings (name + version) from a .txt file."""
|
||||||
|
if not file_path.exists():
|
||||||
|
return set()
|
||||||
|
packages = set()
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
# Ignore empty lines, comments, and recursive/local flags
|
||||||
|
if not line or line.startswith(("#", "-e", "git+", "-r")):
|
||||||
|
continue
|
||||||
|
packages.add(_clean_string(line))
|
||||||
|
return packages
|
||||||
|
|
||||||
|
def parse_toml_section(data, section_type="main"):
|
||||||
|
"""Extracts full requirement strings from pyproject.toml."""
|
||||||
|
packages = set()
|
||||||
|
if section_type == "main":
|
||||||
|
deps = data.get("project", {}).get("dependencies", [])
|
||||||
|
else:
|
||||||
|
# Check optional-dependencies or dependency-groups
|
||||||
|
deps = data.get("project", {}).get("optional-dependencies", {}).get("dev", [])
|
||||||
|
if not deps:
|
||||||
|
deps = data.get("dependency-groups", {}).get("dev", [])
|
||||||
|
|
||||||
|
for req in deps:
|
||||||
|
packages.add(_clean_string(req))
|
||||||
|
return packages
|
||||||
|
|
||||||
|
def test_dependency_files_integrity():
|
||||||
|
"""1. Verifies that .txt files and pyproject.toml have identical names AND versions."""
|
||||||
|
root = get_root_path()
|
||||||
|
toml_path = root / "pyproject.toml"
|
||||||
|
assert toml_path.exists(), "Missing pyproject.toml"
|
||||||
|
|
||||||
|
with open(toml_path, "rb") as f:
|
||||||
|
toml_data = tomllib.load(f)
|
||||||
|
|
||||||
|
# Validate Production Sync
|
||||||
|
txt_main = parse_txt_requirements(root / "requirements.txt")
|
||||||
|
toml_main = parse_toml_section(toml_data, "main")
|
||||||
|
assert txt_main == toml_main, (
|
||||||
|
f"Production version mismatch!\n"
|
||||||
|
f"Only in TXT: {txt_main - toml_main}\n"
|
||||||
|
f"Only in TOML: {toml_main - txt_main}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate Development Sync
|
||||||
|
txt_dev = parse_txt_requirements(root / "requirements-dev.txt")
|
||||||
|
toml_dev = parse_toml_section(toml_data, "dev")
|
||||||
|
assert txt_dev == toml_dev, (
|
||||||
|
f"Development version mismatch!\n"
|
||||||
|
f"Only in TXT: {txt_dev - toml_dev}\n"
|
||||||
|
f"Only in TOML: {toml_dev - txt_dev}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_environment_vs_toml():
|
||||||
|
"""2. Verifies that installed packages satisfy TOML requirements."""
|
||||||
|
root = get_root_path()
|
||||||
|
with open(root / "pyproject.toml", "rb") as f:
|
||||||
|
data = tomllib.load(f)
|
||||||
|
|
||||||
|
all_declared = parse_toml_section(data, "main") | parse_toml_section(data, "dev")
|
||||||
|
_verify_installation(all_declared, "TOML")
|
||||||
|
|
||||||
|
def test_environment_vs_requirements():
|
||||||
|
"""3. Verifies that installed packages satisfy .txt requirements."""
|
||||||
|
root = get_root_path()
|
||||||
|
all_txt_deps = (
|
||||||
|
parse_txt_requirements(root / "requirements.txt") |
|
||||||
|
parse_txt_requirements(root / "requirements-dev.txt")
|
||||||
|
)
|
||||||
|
_verify_installation(all_txt_deps, "requirements.txt")
|
||||||
|
|
||||||
|
def _verify_installation(package_set, source_name):
|
||||||
|
"""Helper to check if declared versions match installed versions."""
|
||||||
|
missing_or_wrong = []
|
||||||
|
|
||||||
|
for req in package_set:
|
||||||
|
# Split name from version to check installation status
|
||||||
|
# handles ==, >=, ~=, <=, > , <
|
||||||
|
import re
|
||||||
|
parts = re.split(r'==|>=|~=|<=|>|<', req)
|
||||||
|
name = parts[0].strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
installed_ver = importlib.metadata.version(name)
|
||||||
|
# If the config uses exact versioning '==', we can do a strict check
|
||||||
|
if "==" in req:
|
||||||
|
expected_ver = req.split("==")[1].strip()
|
||||||
|
if installed_ver != expected_ver:
|
||||||
|
missing_or_wrong.append(f"{name} (Installed: {installed_ver}, Expected: {expected_ver})")
|
||||||
|
except importlib.metadata.PackageNotFoundError:
|
||||||
|
missing_or_wrong.append(f"{name} (Not installed)")
|
||||||
|
|
||||||
|
if missing_or_wrong:
|
||||||
|
pytest.fail(f"Environment out of sync with {source_name}:\n" + "\n".join(missing_or_wrong))
|
||||||
Reference in New Issue
Block a user