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:
SarahRose
2026-01-19 21:42:01 -05:00
parent df025f0409
commit ecc8dad2e2
6 changed files with 557 additions and 2 deletions
+7 -1
View File
@@ -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)
+2
View File
@@ -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)
+250
View 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
+34
View File
@@ -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
View File
@@ -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>
+67
View 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>