mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -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_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_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||
tscm_lock = threading.Lock()
|
||||
@@ -225,7 +230,8 @@ def index() -> str:
|
||||
tools = {
|
||||
'rtl_fm': check_tool('rtl_fm'),
|
||||
'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()]
|
||||
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."""
|
||||
from .pager import pager_bp
|
||||
from .sensor import sensor_bp
|
||||
from .rtlamr import rtlamr_bp
|
||||
from .wifi import wifi_bp
|
||||
from .bluetooth import bluetooth_bp
|
||||
from .adsb import adsb_bp
|
||||
@@ -18,6 +19,7 @@ def register_blueprints(app):
|
||||
|
||||
app.register_blueprint(pager_bp)
|
||||
app.register_blueprint(sensor_bp)
|
||||
app.register_blueprint(rtlamr_bp)
|
||||
app.register_blueprint(wifi_bp)
|
||||
app.register_blueprint(bluetooth_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:"
|
||||
check_required "rtl_fm" "RTL-SDR FM demodulator" rtl_fm
|
||||
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 "rtl_433" "433MHz sensor decoder" rtl_433 rtl433
|
||||
check_required "rtlamr" "Utility meter decoder" rtlamr
|
||||
check_required "dump1090" "ADS-B decoder" dump1090
|
||||
check_required "acarsdec" "ACARS decoder" acarsdec
|
||||
|
||||
@@ -332,6 +334,24 @@ install_macos_packages() {
|
||||
progress "Installing 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"
|
||||
(brew_install dump1090-mutability) || warn "dump1090 not available via Homebrew"
|
||||
|
||||
@@ -602,6 +622,20 @@ install_debian_packages() {
|
||||
progress "Installing rtl_433"
|
||||
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"
|
||||
apt_install aircrack-ng || true
|
||||
|
||||
|
||||
+197
-1
@@ -375,6 +375,8 @@
|
||||
class="nav-label">Pager</span></button>
|
||||
<button class="mode-nav-btn" onclick="switchMode('sensor')"><span class="nav-icon">📡</span><span
|
||||
class="nav-label">433MHz</span></button>
|
||||
<button class="mode-nav-btn" onclick="switchMode('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
|
||||
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
|
||||
@@ -440,6 +442,7 @@
|
||||
<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" 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>
|
||||
<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>
|
||||
@@ -542,6 +545,8 @@
|
||||
{% include 'partials/modes/pager.html' %}
|
||||
|
||||
{% include 'partials/modes/sensor.html' %}
|
||||
|
||||
{% include 'partials/modes/rtlamr.html' %}
|
||||
|
||||
{% include 'partials/modes/wifi.html' %}
|
||||
|
||||
@@ -1974,6 +1979,7 @@
|
||||
});
|
||||
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
|
||||
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('wifiMode').classList.toggle('active', mode === 'wifi');
|
||||
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
|
||||
@@ -2000,6 +2006,7 @@
|
||||
const modeNames = {
|
||||
'pager': 'PAGER',
|
||||
'sensor': '433MHZ',
|
||||
'rtlamr': 'METERS',
|
||||
'satellite': 'SATELLITE',
|
||||
'wifi': 'WIFI',
|
||||
'bluetooth': 'BLUETOOTH',
|
||||
@@ -2019,6 +2026,7 @@
|
||||
const titles = {
|
||||
'pager': 'Pager Decoder',
|
||||
'sensor': '433MHz Sensor Monitor',
|
||||
'rtlamr': 'Utility Meter Monitor',
|
||||
'satellite': 'Satellite Monitor',
|
||||
'wifi': 'WiFi Scanner',
|
||||
'bluetooth': 'Bluetooth Scanner',
|
||||
@@ -2051,7 +2059,7 @@
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
|
||||
// Message storage for export
|
||||
@@ -10064,6 +10249,8 @@
|
||||
decoder</span></div>
|
||||
<div class="icon-item"><span class="icon">📡</span><span class="desc">433MHz - Sensor decoder</span>
|
||||
</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
|
||||
Dashboard</span></div>
|
||||
<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>
|
||||
</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>
|
||||
<ul class="tip-list">
|
||||
<li>Opens the dedicated ADS-B Dashboard for aircraft tracking</li>
|
||||
@@ -10231,6 +10426,7 @@
|
||||
<ul class="tip-list">
|
||||
<li><strong>Pager:</strong> RTL-SDR, rtl_fm, multimon-ng</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 (ACARS):</strong> Second RTL-SDR, acarsdec</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