chore: commit all pending changes

This commit is contained in:
Smittix
2026-02-23 16:51:32 +00:00
parent 38f6b74e8d
commit ddb9f06550
19 changed files with 946 additions and 308 deletions
+44 -29
View File
@@ -96,7 +96,7 @@ def parse_multimon_output(line: str) -> dict[str, str] | None:
return None
def log_message(msg: dict[str, Any]) -> None:
def log_message(msg: dict[str, Any]) -> None:
"""Log a message to file if logging is enabled."""
if not app_module.logging_enabled:
return
@@ -104,25 +104,39 @@ def log_message(msg: dict[str, Any]) -> None:
with open(app_module.log_file_path, 'a') as f:
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
f.write(f"{timestamp} | {msg.get('protocol', 'UNKNOWN')} | {msg.get('address', '')} | {msg.get('message', '')}\n")
except Exception as e:
logger.error(f"Failed to log message: {e}")
def audio_relay_thread(
rtl_stdout,
multimon_stdin,
output_queue: queue.Queue,
stop_event: threading.Event,
) -> None:
"""Relay audio from rtl_fm to multimon-ng while computing signal levels.
Reads raw 16-bit LE PCM from *rtl_stdout*, writes every chunk straight
through to *multimon_stdin*, and every ~100 ms pushes an RMS / peak scope
event onto *output_queue*.
"""
CHUNK = 4096 # bytes 2048 samples at 16-bit mono
INTERVAL = 0.1 # seconds between scope updates
last_scope = time.monotonic()
except Exception as e:
logger.error(f"Failed to log message: {e}")
def _encode_scope_waveform(samples: tuple[int, ...], window_size: int = 256) -> list[int]:
"""Compress recent PCM samples into a signed 8-bit waveform for SSE."""
if not samples:
return []
window = samples[-window_size:] if len(samples) > window_size else samples
waveform: list[int] = []
for sample in window:
# Convert int16 PCM to int8 range for lightweight transport.
packed = int(round(sample / 256))
waveform.append(max(-127, min(127, packed)))
return waveform
def audio_relay_thread(
rtl_stdout,
multimon_stdin,
output_queue: queue.Queue,
stop_event: threading.Event,
) -> None:
"""Relay audio from rtl_fm to multimon-ng while computing signal levels.
Reads raw 16-bit LE PCM from *rtl_stdout*, writes every chunk straight
through to *multimon_stdin*, and every ~100 ms pushes an RMS / peak scope
event plus a compact waveform sample onto *output_queue*.
"""
CHUNK = 4096 # bytes 2048 samples at 16-bit mono
INTERVAL = 0.1 # seconds between scope updates
last_scope = time.monotonic()
try:
while not stop_event.is_set():
@@ -146,15 +160,16 @@ def audio_relay_thread(
if n_samples == 0:
continue
samples = struct.unpack(f'<{n_samples}h', data[:n_samples * 2])
peak = max(abs(s) for s in samples)
rms = int(math.sqrt(sum(s * s for s in samples) / n_samples))
output_queue.put_nowait({
'type': 'scope',
'rms': rms,
'peak': peak,
})
except (struct.error, ValueError, queue.Full):
pass
peak = max(abs(s) for s in samples)
rms = int(math.sqrt(sum(s * s for s in samples) / n_samples))
output_queue.put_nowait({
'type': 'scope',
'rms': rms,
'peak': peak,
'waveform': _encode_scope_waveform(samples),
})
except (struct.error, ValueError, queue.Full):
pass
except Exception as e:
logger.debug(f"Audio relay error: {e}")
finally:
+64 -25
View File
@@ -1,14 +1,15 @@
"""RTL_433 sensor 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 __future__ import annotations
import json
import math
import queue
import subprocess
import threading
import time
from datetime import datetime
from typing import Any, Generator
from flask import Blueprint, jsonify, request, Response
@@ -28,12 +29,42 @@ sensor_bp = Blueprint('sensor', __name__)
# Track which device is being used
sensor_active_device: int | None = None
# RSSI history per device (model_id -> list of (timestamp, rssi))
sensor_rssi_history: dict[str, list[tuple[float, float]]] = {}
_MAX_RSSI_HISTORY = 60
def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
# RSSI history per device (model_id -> list of (timestamp, rssi))
sensor_rssi_history: dict[str, list[tuple[float, float]]] = {}
_MAX_RSSI_HISTORY = 60
def _build_scope_waveform(rssi: float, snr: float, noise: float, points: int = 256) -> list[int]:
"""Synthesize a compact waveform from rtl_433 level metrics."""
points = max(32, min(points, 512))
# rssi is usually negative; stronger signals are closer to 0 dBm.
rssi_norm = min(max(abs(rssi) / 40.0, 0.0), 1.0)
snr_norm = min(max((snr + 5.0) / 35.0, 0.0), 1.0)
noise_norm = min(max(abs(noise) / 40.0, 0.0), 1.0)
amplitude = max(0.06, min(1.0, (0.6 * rssi_norm + 0.4 * snr_norm) - (0.22 * noise_norm)))
cycles = 3.0 + (snr_norm * 8.0)
harmonic = 0.25 + (0.35 * snr_norm)
hiss = 0.08 + (0.18 * noise_norm)
phase = (time.monotonic() * (1.4 + (snr_norm * 2.2))) % (2.0 * math.pi)
waveform: list[int] = []
for i in range(points):
t = i / (points - 1)
base = math.sin((2.0 * math.pi * cycles * t) + phase)
overtone = math.sin((2.0 * math.pi * (cycles * 2.4) * t) + (phase * 0.7))
noise_wobble = math.sin((2.0 * math.pi * (cycles * 7.0) * t) + (phase * 2.1))
sample = amplitude * (base + (harmonic * overtone) + (hiss * noise_wobble))
sample /= (1.0 + harmonic + hiss)
packed = int(round(max(-1.0, min(1.0, sample)) * 127.0))
waveform.append(max(-127, min(127, packed)))
return waveform
def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
"""Stream rtl_433 JSON output to queue."""
try:
app_module.sensor_queue.put({'type': 'status', 'text': 'started'})
@@ -64,16 +95,24 @@ def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
rssi = data.get('rssi')
snr = data.get('snr')
noise = data.get('noise')
if rssi is not None or snr is not None:
try:
app_module.sensor_queue.put_nowait({
'type': 'scope',
'rssi': rssi if rssi is not None else 0,
'snr': snr if snr is not None else 0,
'noise': noise if noise is not None else 0,
})
except queue.Full:
pass
if rssi is not None or snr is not None:
try:
rssi_value = float(rssi) if rssi is not None else 0.0
snr_value = float(snr) if snr is not None else 0.0
noise_value = float(noise) if noise is not None else 0.0
app_module.sensor_queue.put_nowait({
'type': 'scope',
'rssi': rssi_value,
'snr': snr_value,
'noise': noise_value,
'waveform': _build_scope_waveform(
rssi=rssi_value,
snr=snr_value,
noise=noise_value,
),
})
except (TypeError, ValueError, queue.Full):
pass
# Log if enabled
if app_module.logging_enabled: