mirror of
https://github.com/smittix/intercept.git
synced 2026-07-08 01:28:13 -07:00
Add rtlamr utility meter monitoring support
- Added rtlamr mode for decoding utility meters (water, gas, electric) - Starts rtl_tcp server first, then connects rtlamr to it - Supports multiple message types: SCM, SCM+, IDM, NetIDM, R900, R900 BCD - Added frequency presets for 912 MHz (NA) and 868 MHz (EU) - Includes meter ID filtering and unique message options - Updated setup.sh to check and install rtlamr and rtl_tcp - Added UI components: navigation button, mode template, JavaScript functions - Integrated into SDR/RF dropdown menu with lightning bolt icon - Updates mode indicator with frequency when listening - Added help documentation and requirements section
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -136,8 +136,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_required "rtlamr" "Utility meter decoder" 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
|
||||||
|
|
||||||
@@ -332,6 +334,24 @@ install_macos_packages() {
|
|||||||
progress "Installing rtl_433"
|
progress "Installing rtl_433"
|
||||||
brew_install rtl_433
|
brew_install rtl_433
|
||||||
|
|
||||||
|
progress "Installing rtlamr"
|
||||||
|
# rtlamr needs to be installed via go or binary
|
||||||
|
if ! cmd_exists rtlamr; then
|
||||||
|
if [[ -f "/home/rose/Compiled/rtlamr/rtlamr" ]]; then
|
||||||
|
info "Found rtlamr binary, linking to /usr/local/bin..."
|
||||||
|
if [[ -w /usr/local/bin ]]; then
|
||||||
|
ln -sf /home/rose/Compiled/rtlamr/rtlamr /usr/local/bin/rtlamr
|
||||||
|
else
|
||||||
|
sudo ln -sf /home/rose/Compiled/rtlamr/rtlamr /usr/local/bin/rtlamr
|
||||||
|
fi
|
||||||
|
ok "rtlamr linked successfully"
|
||||||
|
else
|
||||||
|
warn "rtlamr not found. Download from https://github.com/bemasher/rtlamr"
|
||||||
|
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"
|
||||||
|
|
||||||
@@ -602,6 +622,20 @@ 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"
|
||||||
|
# rtlamr needs to be installed via go or binary
|
||||||
|
if ! cmd_exists rtlamr; then
|
||||||
|
if [[ -f "/home/rose/Compiled/rtlamr/rtlamr" ]]; then
|
||||||
|
info "Found rtlamr binary, installing to /usr/local/bin..."
|
||||||
|
$SUDO install -m 0755 /home/rose/Compiled/rtlamr/rtlamr /usr/local/bin/rtlamr
|
||||||
|
ok "rtlamr installed successfully"
|
||||||
|
else
|
||||||
|
warn "rtlamr not found. Download from https://github.com/bemasher/rtlamr"
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
+197
-1
@@ -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
|
||||||
@@ -10064,6 +10249,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
|
||||||
@@ -10099,6 +10286,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>
|
||||||
@@ -10231,6 +10426,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user