Files
intercept/routes/rtlamr.py
2026-02-19 20:46:28 +00:00

308 lines
11 KiB
Python

"""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 sse_stream_fanout
from utils.event_pipeline import process_event
from utils.process import safe_terminate, register_process, unregister_process
rtlamr_bp = Blueprint('rtlamr', __name__)
# Store rtl_tcp process separately
rtl_tcp_process = None
rtl_tcp_lock = threading.Lock()
# Track which device is being used
rtlamr_active_device: int | None = None
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:
global rtl_tcp_process, rtlamr_active_device
# Ensure rtlamr process is terminated
try:
process.terminate()
process.wait(timeout=2)
except Exception:
try:
process.kill()
except Exception:
pass
unregister_process(process)
# Kill companion rtl_tcp process
with rtl_tcp_lock:
if rtl_tcp_process:
try:
rtl_tcp_process.terminate()
rtl_tcp_process.wait(timeout=2)
except Exception:
try:
rtl_tcp_process.kill()
except Exception:
pass
unregister_process(rtl_tcp_process)
rtl_tcp_process = None
app_module.rtlamr_queue.put({'type': 'status', 'text': 'stopped'})
with app_module.rtlamr_lock:
app_module.rtlamr_process = None
# Release SDR device
if rtlamr_active_device is not None:
app_module.release_sdr_device(rtlamr_active_device)
rtlamr_active_device = None
@rtlamr_bp.route('/start_rtlamr', methods=['POST'])
def start_rtlamr() -> Response:
global rtl_tcp_process, rtlamr_active_device
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
# Check if device is available
device_int = int(device)
error = app_module.claim_sdr_device(device_int, 'rtlamr')
if error:
return jsonify({
'status': 'error',
'error_type': 'DEVICE_BUSY',
'message': error
}), 409
rtlamr_active_device = device_int
# 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
)
register_process(rtl_tcp_process)
# Wait a moment for rtl_tcp to start
time.sleep(3)
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}")
# Release SDR device on rtl_tcp failure
if rtlamr_active_device is not None:
app_module.release_sdr_device(rtlamr_active_device)
rtlamr_active_device = None
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
)
register_process(app_module.rtlamr_process)
# 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 and release device
with rtl_tcp_lock:
if rtl_tcp_process:
rtl_tcp_process.terminate()
rtl_tcp_process.wait(timeout=2)
rtl_tcp_process = None
if rtlamr_active_device is not None:
app_module.release_sdr_device(rtlamr_active_device)
rtlamr_active_device = 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 and release device
with rtl_tcp_lock:
if rtl_tcp_process:
rtl_tcp_process.terminate()
rtl_tcp_process.wait(timeout=2)
rtl_tcp_process = None
if rtlamr_active_device is not None:
app_module.release_sdr_device(rtlamr_active_device)
rtlamr_active_device = None
return jsonify({'status': 'error', 'message': str(e)})
@rtlamr_bp.route('/stop_rtlamr', methods=['POST'])
def stop_rtlamr() -> Response:
global rtl_tcp_process, rtlamr_active_device
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")
# Release device from registry
if rtlamr_active_device is not None:
app_module.release_sdr_device(rtlamr_active_device)
rtlamr_active_device = None
return jsonify({'status': 'stopped'})
@rtlamr_bp.route('/stream_rtlamr')
def stream_rtlamr() -> Response:
def _on_msg(msg: dict[str, Any]) -> None:
process_event('rtlamr', msg, msg.get('type'))
response = Response(
sse_stream_fanout(
source_queue=app_module.rtlamr_queue,
channel_key='rtlamr',
timeout=1.0,
keepalive_interval=30.0,
on_message=_on_msg,
),
mimetype='text/event-stream',
)
response.headers['Cache-Control'] = 'no-cache'
response.headers['X-Accel-Buffering'] = 'no'
response.headers['Connection'] = 'keep-alive'
return response