mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
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:
2
LICENSE
2
LICENSE
@@ -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
|
||||
|
||||
92
README.md
92
README.md
@@ -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
8
favicon.svg
Normal 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 |
643
intercept.py
643
intercept.py
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user