Add rtl_433 sensor decoding and UI improvements

Features:
- Add 433MHz sensor decoding via rtl_433 integration
- Add mode tabs to switch between Pager and 433MHz modes
- Add sensor data display with device model, readings
- Add separate stats for sensors (readings count, unique devices)
- Add favicon
- Add audio alerts, export (CSV/JSON), signal meter, waterfall display
- Add auto-restart on frequency change
- Add relative timestamps

UI:
- Add author credit (smittix) to header
- Fix tool status alignment with grid layout
- Update stats display to switch with mode tabs

Docs:
- Update README with rtl_433 installation and usage
- Add "What is INTERCEPT?" section explaining front-end purpose
- Add author section and badge
- Update LICENSE copyright

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
James Smith
2025-12-19 15:25:48 +00:00
parent 9cefaed0f0
commit 63b12e7b43
4 changed files with 662 additions and 83 deletions

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025
Copyright (c) 2025 smittix
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -4,6 +4,7 @@
<img src="https://img.shields.io/badge/python-3.7+-blue.svg" alt="Python 3.7+">
<img src="https://img.shields.io/badge/license-MIT-green.svg" alt="MIT License">
<img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-lightgrey.svg" alt="Platform">
<img src="https://img.shields.io/badge/author-smittix-cyan.svg" alt="Author">
</p>
<p align="center">
@@ -11,8 +12,8 @@
</p>
<p align="center">
A sleek, modern web-based pager decoder using RTL-SDR and multimon-ng.<br>
Decode POCSAG and FLEX pager signals with a futuristic SpaceX-inspired interface.
A sleek, modern web-based front-end for RTL-SDR signal decoding tools.<br>
Provides a unified interface for multimon-ng (POCSAG/FLEX) and rtl_433 (433MHz sensors).
</p>
## Screenshot
@@ -20,15 +21,40 @@
---
## What is INTERCEPT?
INTERCEPT is a **web-based front-end** that provides a unified, modern interface for popular RTL-SDR signal decoding tools:
- **rtl_fm + multimon-ng** - For decoding POCSAG and FLEX pager signals
- **rtl_433** - For decoding 433MHz ISM band devices (weather stations, sensors, etc.)
Instead of running command-line tools manually, INTERCEPT handles the process management, output parsing, and presents decoded data in a clean, real-time web interface.
---
## Features
### Pager Decoding
- **Real-time decoding** of POCSAG (512/1200/2400) and FLEX protocols
- **Customizable frequency presets** stored in browser
- **Auto-restart** on frequency change while decoding
### 433MHz Sensor Decoding
- **200+ device protocols** supported via rtl_433
- **Weather stations** - temperature, humidity, wind, rain
- **TPMS** - Tire pressure monitoring sensors
- **Doorbells, remotes, and IoT devices**
- **Smart meters** and utility monitors
### General
- **Web-based interface** - no desktop app needed
- **Live message streaming** via Server-Sent Events (SSE)
- **Audio alerts** with mute toggle
- **Message export** to CSV/JSON
- **Signal activity meter** and waterfall display
- **Message logging** to file with timestamps
- **Customizable frequency presets** stored in browser
- **RTL-SDR device detection** and selection
- **Configurable gain, squelch, and PPM correction**
- **Configurable gain and PPM correction**
## Requirements
@@ -40,7 +66,8 @@
- Python 3.7+
- Flask
- rtl-sdr tools (`rtl_fm`)
- multimon-ng
- multimon-ng (for pager decoding)
- rtl_433 (for 433MHz sensor decoding)
## Installation
@@ -75,7 +102,7 @@ sudo apt-get install multimon-ng
**From source:**
```bash
git clone https://github.com/EliasOewortal/multimon-ng.git
git clone https://github.com/EliasOenal/multimon-ng.git
cd multimon-ng
mkdir build && cd build
cmake ..
@@ -83,13 +110,35 @@ make
sudo make install
```
### 3. Install Python dependencies
### 3. Install rtl_433 (optional, for 433MHz sensors)
**macOS (Homebrew):**
```bash
brew install rtl_433
```
**Ubuntu/Debian:**
```bash
sudo apt-get install rtl-433
```
**From source:**
```bash
git clone https://github.com/merbanan/rtl_433.git
cd rtl_433
mkdir build && cd build
cmake ..
make
sudo make install
```
### 4. Install Python dependencies
```bash
pip install -r requirements.txt
```
### 4. Clone and run
### 5. Clone and run
```bash
git clone https://github.com/yourusername/intercept.git
@@ -119,12 +168,19 @@ Open your browser to `http://localhost:5050`
Enable logging in the Logging section to save decoded messages to a file. Messages are saved with timestamp, protocol, address, and content.
## Default Frequencies (UK)
## Default Frequencies
### Pager (UK)
- **153.350 MHz** - UK pager frequency
- **153.025 MHz** - UK pager frequency
You can customize these presets in the web interface.
### 433MHz Sensors
- **433.92 MHz** - EU/UK ISM band (most common)
- **315.00 MHz** - US ISM band
- **868.00 MHz** - EU ISM band
- **915.00 MHz** - US ISM band
You can customize pager presets in the web interface.
## API Endpoints
@@ -132,10 +188,13 @@ You can customize these presets in the web interface.
|----------|--------|-------------|
| `/` | GET | Main web interface |
| `/devices` | GET | List RTL-SDR devices |
| `/start` | POST | Start decoding |
| `/stop` | POST | Stop decoding |
| `/start` | POST | Start pager decoding |
| `/stop` | POST | Stop pager decoding |
| `/start_sensor` | POST | Start 433MHz sensor listening |
| `/stop_sensor` | POST | Stop 433MHz sensor listening |
| `/status` | GET | Get decoder status |
| `/stream` | GET | SSE stream for messages |
| `/stream` | GET | SSE stream for pager messages |
| `/stream_sensor` | GET | SSE stream for sensor data |
| `/logging` | POST | Toggle message logging |
| `/killall` | POST | Kill all decoder processes |
@@ -160,10 +219,15 @@ You can customize these presets in the web interface.
MIT License - see [LICENSE](LICENSE) for details.
## Author
Created by **smittix** - [GitHub](https://github.com/smittix)
## Acknowledgments
- [rtl-sdr](https://osmocom.org/projects/rtl-sdr/wiki) - RTL-SDR drivers
- [multimon-ng](https://github.com/EliasOenal/multimon-ng) - Multi-protocol decoder
- [multimon-ng](https://github.com/EliasOenal/multimon-ng) - Multi-protocol pager decoder
- [rtl_433](https://github.com/merbanan/rtl_433) - 433MHz sensor decoder
- Inspired by the SpaceX mission control aesthetic
## Disclaimer

8
favicon.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" fill="#000"/>
<path d="M50 5 L90 27.5 L90 72.5 L50 95 L10 72.5 L10 27.5 Z" stroke="#00d4ff" stroke-width="3" fill="none"/>
<path d="M30 50 Q40 35, 50 50 Q60 65, 70 50" stroke="#00d4ff" stroke-width="4" fill="none" stroke-linecap="round"/>
<path d="M35 50 Q42 40, 50 50 Q58 60, 65 50" stroke="#00ff88" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M40 50 Q45 45, 50 50 Q55 55, 60 50" stroke="#ffffff" stroke-width="2" fill="none" stroke-linecap="round"/>
<circle cx="50" cy="50" r="4" fill="#00d4ff"/>
</svg>

After

Width:  |  Height:  |  Size: 639 B

View File

@@ -11,14 +11,17 @@ import queue
import pty
import os
import select
from flask import Flask, render_template_string, jsonify, request, Response
from flask import Flask, render_template_string, jsonify, request, Response, send_file
app = Flask(__name__)
# Global process management
current_process = None
sensor_process = None
output_queue = queue.Queue()
sensor_queue = queue.Queue()
process_lock = threading.Lock()
sensor_lock = threading.Lock()
# Logging settings
logging_enabled = False
@@ -32,6 +35,7 @@ HTML_TEMPLATE = '''
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>INTERCEPT // Signal Intelligence</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Rajdhani:wght@400;500;600;700&display=swap');
@@ -644,6 +648,92 @@ HTML_TEMPLATE = '''
color: var(--accent-red);
}
/* Mode tabs */
.mode-tabs {
display: flex;
gap: 0;
margin-bottom: 20px;
border: 1px solid var(--border-color);
}
.mode-tab {
flex: 1;
padding: 12px 16px;
background: var(--bg-primary);
border: none;
color: var(--text-secondary);
cursor: pointer;
font-family: 'Rajdhani', sans-serif;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
transition: all 0.2s ease;
}
.mode-tab:not(:last-child) {
border-right: 1px solid var(--border-color);
}
.mode-tab:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.mode-tab.active {
background: var(--accent-cyan);
color: var(--bg-primary);
}
.mode-content {
display: none;
}
.mode-content.active {
display: block;
}
/* Sensor card styling */
.sensor-card {
padding: 15px;
margin-bottom: 10px;
border: 1px solid var(--border-color);
border-left: 3px solid var(--accent-green);
background: var(--bg-secondary);
}
.sensor-card .device-name {
color: var(--accent-green);
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
}
.sensor-card .sensor-data {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 8px;
}
.sensor-card .data-item {
background: var(--bg-primary);
padding: 8px 10px;
border: 1px solid var(--border-color);
}
.sensor-card .data-label {
font-size: 9px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
}
.sensor-card .data-value {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
color: var(--accent-cyan);
}
/* Scanline effect overlay */
body::before {
content: '';
@@ -682,12 +772,18 @@ HTML_TEMPLATE = '''
</svg>
</div>
<h1>INTERCEPT</h1>
<p>Signal Intelligence // POCSAG & FLEX Decoder</p>
<p>Signal Intelligence // by smittix</p>
</header>
<div class="container">
<div class="main-content">
<div class="sidebar">
<!-- Mode Tabs -->
<div class="mode-tabs">
<button class="mode-tab active" onclick="switchMode('pager')">Pager</button>
<button class="mode-tab" onclick="switchMode('sensor')">433MHz</button>
</div>
<div class="section">
<h3>Device</h3>
<div class="form-group">
@@ -704,76 +800,130 @@ HTML_TEMPLATE = '''
<button class="preset-btn" onclick="refreshDevices()" style="width: 100%;">
Refresh Devices
</button>
<div class="info-text">
rtl_fm: <span class="tool-status {{ 'ok' if tools.rtl_fm else 'missing' }}">{{ 'OK' if tools.rtl_fm else 'Missing' }}</span>
multimon-ng: <span class="tool-status {{ 'ok' if tools.multimon else 'missing' }}">{{ 'OK' if tools.multimon else 'Missing' }}</span>
<div class="info-text" style="display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;">
<span>rtl_fm:</span><span class="tool-status {{ 'ok' if tools.rtl_fm else 'missing' }}">{{ 'OK' if tools.rtl_fm else 'Missing' }}</span>
<span>multimon-ng:</span><span class="tool-status {{ 'ok' if tools.multimon else 'missing' }}">{{ 'OK' if tools.multimon else 'Missing' }}</span>
<span>rtl_433:</span><span class="tool-status {{ 'ok' if tools.rtl_433 else 'missing' }}">{{ 'OK' if tools.rtl_433 else 'Missing' }}</span>
</div>
</div>
<div class="section">
<h3>Frequency</h3>
<div class="form-group">
<label>Frequency (MHz)</label>
<input type="text" id="frequency" value="153.350" placeholder="e.g., 153.350">
<!-- PAGER MODE -->
<div id="pagerMode" class="mode-content active">
<div class="section">
<h3>Frequency</h3>
<div class="form-group">
<label>Frequency (MHz)</label>
<input type="text" id="frequency" value="153.350" placeholder="e.g., 153.350">
</div>
<div class="preset-buttons" id="presetButtons">
<!-- Populated by JavaScript -->
</div>
<div style="margin-top: 8px; display: flex; gap: 5px;">
<input type="text" id="newPresetFreq" placeholder="New freq (MHz)" style="flex: 1; padding: 6px; background: #0f3460; border: 1px solid #1a1a2e; color: #fff; border-radius: 4px; font-size: 12px;">
<button class="preset-btn" onclick="addPreset()" style="background: #2ecc71;">Add</button>
</div>
<div style="margin-top: 5px;">
<button class="preset-btn" onclick="resetPresets()" style="font-size: 11px;">Reset to Defaults</button>
</div>
</div>
<div class="preset-buttons" id="presetButtons">
<!-- Populated by JavaScript -->
<div class="section">
<h3>Protocols</h3>
<div class="checkbox-group">
<label><input type="checkbox" id="proto_pocsag512" checked> POCSAG-512</label>
<label><input type="checkbox" id="proto_pocsag1200" checked> POCSAG-1200</label>
<label><input type="checkbox" id="proto_pocsag2400" checked> POCSAG-2400</label>
<label><input type="checkbox" id="proto_flex" checked> FLEX</label>
</div>
</div>
<div style="margin-top: 8px; display: flex; gap: 5px;">
<input type="text" id="newPresetFreq" placeholder="New freq (MHz)" style="flex: 1; padding: 6px; background: #0f3460; border: 1px solid #1a1a2e; color: #fff; border-radius: 4px; font-size: 12px;">
<button class="preset-btn" onclick="addPreset()" style="background: #2ecc71;">Add</button>
<div class="section">
<h3>Settings</h3>
<div class="form-group">
<label>Gain (dB, 0 = auto)</label>
<input type="text" id="gain" value="0" placeholder="0-49 or 0 for auto">
</div>
<div class="form-group">
<label>Squelch Level</label>
<input type="text" id="squelch" value="0" placeholder="0 = off">
</div>
<div class="form-group">
<label>PPM Correction</label>
<input type="text" id="ppm" value="0" placeholder="Frequency correction">
</div>
</div>
<div style="margin-top: 5px;">
<button class="preset-btn" onclick="resetPresets()" style="font-size: 11px;">Reset to Defaults</button>
<div class="section">
<h3>Logging</h3>
<div class="checkbox-group" style="margin-bottom: 15px;">
<label>
<input type="checkbox" id="loggingEnabled" onchange="toggleLogging()">
Enable Logging
</label>
</div>
<div class="form-group">
<label>Log file path</label>
<input type="text" id="logFilePath" value="pager_messages.log" placeholder="pager_messages.log">
</div>
</div>
<button class="run-btn" id="startBtn" onclick="startDecoding()">
Start Decoding
</button>
<button class="stop-btn" id="stopBtn" onclick="stopDecoding()" style="display: none;">
Stop Decoding
</button>
</div>
<div class="section">
<h3>Protocols</h3>
<div class="checkbox-group">
<label><input type="checkbox" id="proto_pocsag512" checked> POCSAG-512</label>
<label><input type="checkbox" id="proto_pocsag1200" checked> POCSAG-1200</label>
<label><input type="checkbox" id="proto_pocsag2400" checked> POCSAG-2400</label>
<label><input type="checkbox" id="proto_flex" checked> FLEX</label>
<!-- 433MHz SENSOR MODE -->
<div id="sensorMode" class="mode-content">
<div class="section">
<h3>Frequency</h3>
<div class="form-group">
<label>Frequency (MHz)</label>
<input type="text" id="sensorFrequency" value="433.92" placeholder="e.g., 433.92">
</div>
<div class="preset-buttons">
<button class="preset-btn" onclick="setSensorFreq('433.92')">433.92</button>
<button class="preset-btn" onclick="setSensorFreq('315.00')">315.00</button>
<button class="preset-btn" onclick="setSensorFreq('868.00')">868.00</button>
<button class="preset-btn" onclick="setSensorFreq('915.00')">915.00</button>
</div>
</div>
<div class="section">
<h3>Settings</h3>
<div class="form-group">
<label>Gain (dB, 0 = auto)</label>
<input type="text" id="sensorGain" value="0" placeholder="0-49 or 0 for auto">
</div>
<div class="form-group">
<label>PPM Correction</label>
<input type="text" id="sensorPpm" value="0" placeholder="Frequency correction">
</div>
</div>
<div class="section">
<h3>Protocols</h3>
<div class="info-text" style="margin-bottom: 10px;">
rtl_433 auto-detects 200+ device protocols including weather stations, TPMS, doorbells, and more.
</div>
<div class="checkbox-group">
<label>
<input type="checkbox" id="sensorLogging" onchange="toggleSensorLogging()">
Enable Logging
</label>
</div>
</div>
<button class="run-btn" id="startSensorBtn" onclick="startSensorDecoding()">
Start Listening
</button>
<button class="stop-btn" id="stopSensorBtn" onclick="stopSensorDecoding()" style="display: none;">
Stop Listening
</button>
</div>
<div class="section">
<h3>Settings</h3>
<div class="form-group">
<label>Gain (dB, 0 = auto)</label>
<input type="text" id="gain" value="0" placeholder="0-49 or 0 for auto">
</div>
<div class="form-group">
<label>Squelch Level</label>
<input type="text" id="squelch" value="0" placeholder="0 = off">
</div>
<div class="form-group">
<label>PPM Correction</label>
<input type="text" id="ppm" value="0" placeholder="Frequency correction">
</div>
</div>
<div class="section">
<h3>Logging</h3>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="loggingEnabled" onchange="toggleLogging()">
Enable message logging
</label>
</div>
<div class="form-group">
<label>Log file path</label>
<input type="text" id="logFilePath" value="pager_messages.log" placeholder="pager_messages.log">
</div>
</div>
<button class="run-btn" id="startBtn" onclick="startDecoding()">
Start Decoding
</button>
<button class="stop-btn" id="stopBtn" onclick="stopDecoding()" style="display: none;">
Stop Decoding
</button>
<button class="preset-btn" onclick="killAll()" style="width: 100%; margin-top: 10px; border-color: #ff3366; color: #ff3366;">
Kill All Processes
</button>
@@ -790,11 +940,15 @@ HTML_TEMPLATE = '''
<div class="signal-bar"></div>
<div class="signal-bar"></div>
</div>
<div class="stats">
<div class="stats" id="pagerStats">
<div>MSG: <span id="msgCount">0</span></div>
<div>POCSAG: <span id="pocsagCount">0</span></div>
<div>FLEX: <span id="flexCount">0</span></div>
</div>
<div class="stats" id="sensorStats" style="display: none;">
<div>SENSORS: <span id="sensorCount">0</span></div>
<div>DEVICES: <span id="deviceCount">0</span></div>
</div>
</div>
</div>
@@ -828,11 +982,185 @@ HTML_TEMPLATE = '''
<script>
let eventSource = null;
let isRunning = false;
let isSensorRunning = false;
let currentMode = 'pager';
let msgCount = 0;
let pocsagCount = 0;
let flexCount = 0;
let sensorCount = 0;
let deviceList = {{ devices | tojson | safe }};
// Mode switching
function switchMode(mode) {
currentMode = mode;
document.querySelectorAll('.mode-tab').forEach(tab => {
tab.classList.toggle('active', tab.textContent.toLowerCase().includes(mode === 'pager' ? 'pager' : '433'));
});
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor');
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none';
}
// Track unique sensor devices
let uniqueDevices = new Set();
// Sensor frequency
function setSensorFreq(freq) {
document.getElementById('sensorFrequency').value = freq;
if (isSensorRunning) {
fetch('/stop_sensor', {method: 'POST'})
.then(() => setTimeout(() => startSensorDecoding(), 500));
}
}
// Start sensor decoding
function startSensorDecoding() {
const freq = document.getElementById('sensorFrequency').value;
const gain = document.getElementById('sensorGain').value;
const ppm = document.getElementById('sensorPpm').value;
const device = getSelectedDevice();
const config = {
frequency: freq,
gain: gain,
ppm: ppm,
device: device
};
fetch('/start_sensor', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
}).then(r => r.json())
.then(data => {
if (data.status === 'started') {
setSensorRunning(true);
startSensorStream();
} else {
alert('Error: ' + data.message);
}
});
}
// Stop sensor decoding
function stopSensorDecoding() {
fetch('/stop_sensor', {method: 'POST'})
.then(r => r.json())
.then(data => {
setSensorRunning(false);
if (eventSource) {
eventSource.close();
eventSource = null;
}
});
}
function setSensorRunning(running) {
isSensorRunning = running;
document.getElementById('statusDot').classList.toggle('running', running);
document.getElementById('statusText').textContent = running ? 'Listening...' : 'Idle';
document.getElementById('startSensorBtn').style.display = running ? 'none' : 'block';
document.getElementById('stopSensorBtn').style.display = running ? 'block' : 'none';
}
function startSensorStream() {
if (eventSource) {
eventSource.close();
}
eventSource = new EventSource('/stream_sensor');
eventSource.onopen = function() {
showInfo('Sensor stream connected...');
};
eventSource.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.type === 'sensor') {
addSensorReading(data);
} else if (data.type === 'status') {
if (data.text === 'stopped') {
setSensorRunning(false);
}
} else if (data.type === 'info' || data.type === 'raw') {
showInfo(data.text);
}
};
eventSource.onerror = function(e) {
console.error('Sensor stream error');
};
}
function addSensorReading(data) {
const output = document.getElementById('output');
const placeholder = output.querySelector('.placeholder');
if (placeholder) placeholder.remove();
// Store for export
allMessages.push(data);
playAlert();
pulseSignal();
addWaterfallPoint(Date.now(), 0.8);
sensorCount++;
document.getElementById('sensorCount').textContent = sensorCount;
// Track unique devices by model + id
const deviceKey = (data.model || 'Unknown') + '_' + (data.id || data.channel || '0');
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 skipKeys = ['type', 'time', 'model', 'raw'];
for (const [key, value] of Object.entries(data)) {
if (!skipKeys.includes(key) && value !== null && value !== undefined) {
const label = key.replace(/_/g, ' ');
let displayValue = value;
if (key === 'temperature_C') displayValue = value + ' °C';
else if (key === 'temperature_F') displayValue = value + ' °F';
else if (key === 'humidity') displayValue = value + ' %';
else if (key === 'pressure_hPa') displayValue = value + ' hPa';
else if (key === 'wind_avg_km_h') displayValue = value + ' km/h';
else if (key === 'rain_mm') displayValue = value + ' mm';
else if (key === 'battery_ok') displayValue = value ? 'OK' : 'Low';
dataItems += '<div class="data-item"><div class="data-label">' + label + '</div><div class="data-value">' + displayValue + '</div></div>';
}
}
const relTime = data.time ? getRelativeTime(data.time.split(' ')[1] || data.time) : 'now';
card.innerHTML =
'<div class="header" style="display: flex; justify-content: space-between; margin-bottom: 8px;">' +
'<span class="device-name">' + (data.model || 'Unknown Device') + '</span>' +
'<span class="msg-time" data-timestamp="' + (data.time || '') + '" style="color: #444; font-size: 10px;">' + relTime + '</span>' +
'</div>' +
'<div class="sensor-data">' + dataItems + '</div>';
output.insertBefore(card, output.firstChild);
if (autoScroll) output.scrollTop = 0;
while (output.children.length > 100) {
output.removeChild(output.lastChild);
}
}
function toggleSensorLogging() {
const enabled = document.getElementById('sensorLogging').checked;
fetch('/logging', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({enabled: enabled, log_file: 'sensor_data.log'})
});
}
// Audio alert settings
let audioMuted = localStorage.getItem('audioMuted') === 'true';
let audioContext = null;
@@ -892,7 +1220,7 @@ HTML_TEMPLATE = '''
];
csv.push(row.join(','));
});
downloadFile(csv.join('\n'), 'intercept_messages.csv', 'text/csv');
downloadFile(csv.join('\\n'), 'intercept_messages.csv', 'text/csv');
}
function exportJSON() {
@@ -980,7 +1308,7 @@ HTML_TEMPLATE = '''
function renderWaterfall() {
const canvas = document.getElementById('waterfallCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const width = canvas.width;
const height = canvas.height;
@@ -1097,6 +1425,13 @@ HTML_TEMPLATE = '''
function setFreq(freq) {
document.getElementById('frequency').value = freq;
// Auto-restart decoder with new frequency if currently running
if (isRunning) {
fetch('/stop', {method: 'POST'})
.then(() => {
setTimeout(() => startDecoding(), 500);
});
}
}
function refreshDevices() {
@@ -1351,15 +1686,19 @@ HTML_TEMPLATE = '''
function clearMessages() {
document.getElementById('output').innerHTML = `
<div class="placeholder" style="color: #888; text-align: center; padding: 50px;">
Messages cleared. ${isRunning ? 'Waiting for new messages...' : 'Start decoding to receive messages.'}
Messages cleared. ${isRunning || isSensorRunning ? 'Waiting for new messages...' : 'Start decoding to receive messages.'}
</div>
`;
msgCount = 0;
pocsagCount = 0;
flexCount = 0;
sensorCount = 0;
uniqueDevices.clear();
document.getElementById('msgCount').textContent = '0';
document.getElementById('pocsagCount').textContent = '0';
document.getElementById('flexCount').textContent = '0';
document.getElementById('sensorCount').textContent = '0';
document.getElementById('deviceCount').textContent = '0';
}
</script>
</body>
@@ -1544,12 +1883,18 @@ def stream_decoder(master_fd, process):
def index():
tools = {
'rtl_fm': check_tool('rtl_fm'),
'multimon': check_tool('multimon-ng')
'multimon': check_tool('multimon-ng'),
'rtl_433': check_tool('rtl_433')
}
devices = detect_devices()
return render_template_string(HTML_TEMPLATE, tools=tools, devices=devices)
@app.route('/favicon.svg')
def favicon():
return send_file('favicon.svg', mimetype='image/svg+xml')
@app.route('/devices')
def get_devices():
return jsonify(detect_devices())
@@ -1747,8 +2092,8 @@ def log_message(msg):
@app.route('/killall', methods=['POST'])
def kill_all():
"""Kill all rtl_fm and multimon-ng processes."""
global current_process
"""Kill all rtl_fm, multimon-ng, and rtl_433 processes."""
global current_process, sensor_process
killed = []
try:
@@ -1765,9 +2110,19 @@ def kill_all():
except:
pass
try:
result = subprocess.run(['pkill', '-f', 'rtl_433'], capture_output=True)
if result.returncode == 0:
killed.append('rtl_433')
except:
pass
with process_lock:
current_process = None
with sensor_lock:
sensor_process = None
return jsonify({'status': 'killed', 'processes': killed})
@@ -1789,10 +2144,162 @@ def stream():
return response
# ============== RTL_433 SENSOR ROUTES ==============
def stream_sensor_output(process):
"""Stream rtl_433 JSON output to queue."""
global sensor_process
import json as json_module
try:
sensor_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:
# rtl_433 outputs JSON objects, one per line
data = json_module.loads(line)
data['type'] = 'sensor'
sensor_queue.put(data)
# Log if enabled
if logging_enabled:
try:
with open(log_file_path, 'a') as f:
from datetime import datetime
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
f.write(f"{timestamp} | {data.get('model', 'Unknown')} | {json_module.dumps(data)}\n")
except Exception:
pass
except json_module.JSONDecodeError:
# Not JSON, send as raw
sensor_queue.put({'type': 'raw', 'text': line})
except Exception as e:
sensor_queue.put({'type': 'error', 'text': str(e)})
finally:
process.wait()
sensor_queue.put({'type': 'status', 'text': 'stopped'})
with sensor_lock:
sensor_process = None
@app.route('/start_sensor', methods=['POST'])
def start_sensor():
global sensor_process
with sensor_lock:
if sensor_process:
return jsonify({'status': 'error', 'message': 'Sensor already running'})
data = request.json
freq = data.get('frequency', '433.92')
gain = data.get('gain', '0')
ppm = data.get('ppm', '0')
device = data.get('device', '0')
# Clear queue
while not sensor_queue.empty():
try:
sensor_queue.get_nowait()
except:
break
# Build rtl_433 command
# rtl_433 -d <device> -f <freq>M -g <gain> -p <ppm> -F json
cmd = [
'rtl_433',
'-d', str(device),
'-f', f'{freq}M',
'-F', 'json'
]
if gain and gain != '0':
cmd.extend(['-g', str(gain)])
if ppm and ppm != '0':
cmd.extend(['-p', str(ppm)])
full_cmd = ' '.join(cmd)
print(f"Running: {full_cmd}")
try:
sensor_process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=1
)
# Start output thread
thread = threading.Thread(target=stream_sensor_output, args=(sensor_process,))
thread.daemon = True
thread.start()
# Monitor stderr
def monitor_stderr():
for line in sensor_process.stderr:
err = line.decode('utf-8', errors='replace').strip()
if err:
print(f"[rtl_433] {err}")
sensor_queue.put({'type': 'info', 'text': f'[rtl_433] {err}'})
stderr_thread = threading.Thread(target=monitor_stderr)
stderr_thread.daemon = True
stderr_thread.start()
sensor_queue.put({'type': 'info', 'text': f'Command: {full_cmd}'})
return jsonify({'status': 'started', 'command': full_cmd})
except FileNotFoundError:
return jsonify({'status': 'error', 'message': 'rtl_433 not found. Install with: brew install rtl_433'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)})
@app.route('/stop_sensor', methods=['POST'])
def stop_sensor():
global sensor_process
with sensor_lock:
if sensor_process:
sensor_process.terminate()
try:
sensor_process.wait(timeout=2)
except subprocess.TimeoutExpired:
sensor_process.kill()
sensor_process = None
return jsonify({'status': 'stopped'})
return jsonify({'status': 'not_running'})
@app.route('/stream_sensor')
def stream_sensor():
def generate():
import json
while True:
try:
msg = sensor_queue.get(timeout=1)
yield f"data: {json.dumps(msg)}\n\n"
except queue.Empty:
yield f"data: {json.dumps({'type': 'keepalive'})}\n\n"
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
def main():
print("=" * 50)
print(" INTERCEPT // Signal Intelligence")
print(" POCSAG / FLEX using RTL-SDR + multimon-ng")
print(" POCSAG / FLEX / 433MHz Sensors")
print("=" * 50)
print()
print("Open http://localhost:5050 in your browser")