mirror of
https://github.com/smittix/intercept.git
synced 2026-04-25 07:10:00 -07:00
308 lines
11 KiB
Python
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
|