Add RTL-SDR mode switcher, DVB-T to Kodi pipeline, FM radio

Mode switcher (rtl_mode_switch.sh) handles exclusive dongle access
between DVB-T, FM, SDR scanner, ADS-B, spectrum, and HackRF modes.
DVB-T pipeline: RTL-SDR -> GNU Radio demod -> MPEG-TS -> HTTP -> Kodi.
Kodi setup script generates M3U playlist for PVR IPTV Simple Client.
Includes dvbt_rx.py and sdr_tv.py from dvbt-rx project.
WebUI updated with mode switcher, channel selector, and Kodi controls.
This commit is contained in:
sssnake
2026-03-31 07:38:02 -07:00
parent 7be3a8e183
commit 6e027b2c1b
7 changed files with 1544 additions and 0 deletions

View File

@@ -21,6 +21,7 @@ FILES=(
system.prop system.prop
system/ system/
webroot/ webroot/
scripts/
BUILDING_MODULES.md BUILDING_MODULES.md
) )

Binary file not shown.

295
scripts/dvbt_rx.py Executable file
View File

@@ -0,0 +1,295 @@
#!/usr/bin/env python3
"""
DVB-T Digital TV Receiver using RTL-SDR and GNU Radio
Receives DVB-T signals and outputs MPEG-TS stream to stdout.
Usage:
python3 dvbt_rx.py --freq 506000000 | mpv -
python3 dvbt_rx.py --freq 506e6 --input-file recording.raw | vlc -
"""
import argparse
import signal
import sys
import os
from gnuradio import gr, blocks, fft, dtv
import osmosdr
# DVB-T parameter tables
BANDWIDTH_MAP = {
8: (64.0 / 7e6, dtv.BANDWIDTH_8_0_MHZ), # 9142857.14 Hz
7: (1.0 / 8e6, dtv.BANDWIDTH_7_0_MHZ), # 8000000 Hz
6: (7.0 / 48e6, dtv.BANDWIDTH_6_0_MHZ), # 6857142.86 Hz
}
TRANSMISSION_MODE_MAP = {
'2k': (dtv.T2k, 2048, 1705),
'8k': (dtv.T8k, 8192, 6817),
}
CONSTELLATION_MAP = {
'qpsk': dtv.MOD_QPSK,
'16qam': dtv.MOD_16QAM,
'64qam': dtv.MOD_64QAM,
}
CODE_RATE_MAP = {
'1/2': dtv.C1_2,
'2/3': dtv.C2_3,
'3/4': dtv.C3_4,
'5/6': dtv.C5_6,
'7/8': dtv.C7_8,
}
GUARD_INTERVAL_MAP = {
'1/4': (dtv.GI_1_4, 4),
'1/8': (dtv.GI_1_8, 8),
'1/16': (dtv.GI_1_16, 16),
'1/32': (dtv.GI_1_32, 32),
}
def parse_freq(s):
"""Parse frequency string, supporting scientific notation like 506e6."""
return int(float(s))
class DVBTReceiver(gr.top_block):
def __init__(self, args):
gr.top_block.__init__(self, "DVB-T Receiver")
# Resolve parameters
tx_mode_enum, fft_length, active_carriers = TRANSMISSION_MODE_MAP[args.transmission_mode]
constellation = CONSTELLATION_MAP[args.constellation]
code_rate = CODE_RATE_MAP[args.code_rate]
gi_enum, gi_divisor = GUARD_INTERVAL_MAP[args.guard_interval]
cp_length = fft_length // gi_divisor
# Sample rate from bandwidth
# DVB-T sample rate = bandwidth * 8/7 (for 8 MHz: 64/7 MHz)
bw_mhz = args.bandwidth
if bw_mhz == 8:
samp_rate = 64e6 / 7.0 # 9142857.14 Hz
elif bw_mhz == 7:
samp_rate = 8e6
else:
samp_rate = 48e6 / 7.0 # 6857142.86 Hz
print(f"--- DVB-T Receiver ---", file=sys.stderr)
print(f"Frequency: {args.freq / 1e6:.3f} MHz", file=sys.stderr)
print(f"Bandwidth: {bw_mhz} MHz", file=sys.stderr)
print(f"Sample rate: {samp_rate:.0f} Hz", file=sys.stderr)
print(f"Mode: {args.transmission_mode} (FFT={fft_length})", file=sys.stderr)
print(f"Constellation: {args.constellation}", file=sys.stderr)
print(f"Code rate: {args.code_rate}", file=sys.stderr)
print(f"Guard interval:{args.guard_interval} (CP={cp_length})", file=sys.stderr)
print(f"", file=sys.stderr)
# --- Source ---
if args.input_file:
print(f"Source: file '{args.input_file}'", file=sys.stderr)
self.source = blocks.file_source(
gr.sizeof_gr_complex, args.input_file, repeat=False
)
else:
print(f"Source: RTL-SDR (live)", file=sys.stderr)
self.source = osmosdr.source(args="numchan=1")
self.source.set_sample_rate(samp_rate)
self.source.set_center_freq(args.freq)
self.source.set_freq_corr(args.ppm)
self.source.set_gain_mode(True)
self.source.set_gain(args.gain)
self.source.set_if_gain(args.if_gain)
self.source.set_bb_gain(0)
actual_rate = self.source.get_sample_rate()
if abs(actual_rate - samp_rate) > 1000:
print(f"WARNING: Requested sample rate {samp_rate:.0f} Hz but got {actual_rate:.0f} Hz",
file=sys.stderr)
print(f"WARNING: RTL-SDR max is ~2.4 Msps. DVB-T {bw_mhz}MHz needs {samp_rate:.0f} Hz.",
file=sys.stderr)
print(f"WARNING: Signal decoding will likely fail. Use --input-file for testing.",
file=sys.stderr)
# --- OFDM Demodulation Chain ---
# Stream -> Vector for OFDM processing
self.s2v_ofdm = blocks.stream_to_vector(gr.sizeof_gr_complex, fft_length)
# OFDM symbol acquisition (timing sync, CP removal)
self.ofdm_acq = dtv.dvbt_ofdm_sym_acquisition(
blocks=1,
fft_length=fft_length,
occupied_tones=active_carriers,
cp_length=cp_length,
snr=100.0
)
# FFT: time domain -> frequency domain
self.fft = fft.fft_vcc(
fft_size=fft_length,
forward=True,
window=[],
shift=False
)
# Channel estimation and equalization using reference signals
self.demod_ref = dtv.dvbt_demod_reference_signals(
itemsize=gr.sizeof_gr_complex,
ninput=fft_length,
noutput=active_carriers,
constellation=constellation,
hierarchy=dtv.NH,
code_rate_HP=code_rate,
code_rate_LP=dtv.C1_2,
guard_interval=gi_enum,
transmission_mode=tx_mode_enum,
include_cell_id=0,
cell_id=0
)
# --- Demapping and Decoding Chain ---
# Vector -> Stream for demapper
self.v2s_demod = blocks.vector_to_stream(gr.sizeof_gr_complex, active_carriers)
# Constellation demapping (soft decision)
self.demap = dtv.dvbt_demap(
nsize=active_carriers,
constellation=constellation,
hierarchy=dtv.NH,
transmission=tx_mode_enum,
gain=1.0
)
# Bit-level inner deinterleaver
self.bit_deintlv = dtv.dvbt_bit_inner_deinterleaver(
nsize=active_carriers,
constellation=constellation,
hierarchy=dtv.NH,
transmission=tx_mode_enum
)
# Viterbi decoder (inner FEC)
self.viterbi = dtv.dvbt_viterbi_decoder(
constellation=constellation,
hierarchy=dtv.NH,
coderate=code_rate,
bsize=active_carriers
)
# Byte-level convolutional deinterleaver
self.conv_deintlv = dtv.dvbt_convolutional_deinterleaver(
nsize=136,
I=12,
M=17
)
# Reed-Solomon decoder (outer FEC) - RS(204,188,t=8) shortened from RS(255,239)
self.rs_dec = dtv.dvbt_reed_solomon_dec(
p=2,
m=8,
gfpoly=0x11d,
n=255,
k=239,
t=8,
s=51,
blocks=8
)
# Energy descrambler - restores MPEG-TS sync bytes
self.descramble = dtv.dvbt_energy_descramble(nblocks=8)
# --- Output: MPEG-TS to stdout ---
self.sink = blocks.file_descriptor_sink(gr.sizeof_char, 1) # fd=1 = stdout
# --- Wire the flowgraph ---
self.connect(
self.source,
self.s2v_ofdm,
self.ofdm_acq,
self.fft,
self.demod_ref,
self.v2s_demod,
self.demap,
self.bit_deintlv,
self.viterbi,
self.conv_deintlv,
self.rs_dec,
self.descramble,
self.sink
)
print("Flowgraph built. Starting...", file=sys.stderr)
def main():
parser = argparse.ArgumentParser(
description='DVB-T Digital TV Receiver',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
Live reception (requires SDR with sufficient sample rate):
python3 dvbt_rx.py --freq 506000000 | mpv -
From recorded IQ file:
python3 dvbt_rx.py --freq 506e6 --input-file recording.cf32 | vlc -
Record IQ samples first (if you have a capable SDR):
rtl_sdr -f 506000000 -s 9142857 -g 40 recording.raw
"""
)
parser.add_argument('--freq', type=parse_freq, required=True,
help='Center frequency in Hz (e.g., 506000000 or 506e6)')
parser.add_argument('--bandwidth', type=int, default=8, choices=[6, 7, 8],
help='Channel bandwidth in MHz (default: 8)')
parser.add_argument('--transmission-mode', default='8k', choices=['2k', '8k'],
help='Transmission mode (default: 8k)')
parser.add_argument('--constellation', default='64qam',
choices=['qpsk', '16qam', '64qam'],
help='Constellation (default: 64qam)')
parser.add_argument('--code-rate', default='2/3',
choices=['1/2', '2/3', '3/4', '5/6', '7/8'],
help='Code rate (default: 2/3)')
parser.add_argument('--guard-interval', default='1/32',
choices=['1/4', '1/8', '1/16', '1/32'],
help='Guard interval (default: 1/32)')
parser.add_argument('--gain', type=float, default=40.0,
help='RF gain in dB (default: 40)')
parser.add_argument('--if-gain', type=float, default=40.0,
help='IF gain in dB (default: 40)')
parser.add_argument('--ppm', type=float, default=0.0,
help='Frequency correction in PPM (default: 0)')
parser.add_argument('--input-file', type=str, default=None,
help='Read IQ from file instead of RTL-SDR (complex float32)')
args = parser.parse_args()
# Make stdout binary for MPEG-TS output
if hasattr(sys.stdout, 'buffer'):
sys.stdout = sys.stdout.buffer
tb = DVBTReceiver(args)
def signal_handler(sig, frame):
print("\nStopping...", file=sys.stderr)
tb.stop()
tb.wait()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
tb.start()
try:
tb.wait()
except KeyboardInterrupt:
tb.stop()
tb.wait()
if __name__ == '__main__':
main()

168
scripts/kodi_dvbt_setup.sh Executable file
View File

@@ -0,0 +1,168 @@
#!/system/bin/sh
# Kodi DVB-T Live TV Setup
#
# Sets up the pipeline: RTL-SDR -> DVB-T demod -> MPEG-TS -> HTTP -> Kodi
#
# Kodi PVR connection:
# 1. Install "PVR IPTV Simple Client" addon in Kodi
# 2. Configure M3U playlist URL: http://127.0.0.1:8554/dvbt.m3u
# 3. Or add single channel: http://127.0.0.1:8554
#
# This script generates the M3U playlist from configured channels
# and starts the DVB-T HTTP stream server.
MODDIR="/data/adb/modules/driver-manager"
CONFDIR="$MODDIR/config"
STREAMDIR="$MODDIR/streams"
LOGFILE="$MODDIR/driver-manager.log"
KODI_PORT=$(cat "$CONFDIR/kodi_stream_port" 2>/dev/null || echo "8554")
CHANNELS_FILE="$CONFDIR/dvbt_channels.conf"
mkdir -p "$STREAMDIR"
mlog() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [kodi] $1" >> "$LOGFILE"
}
# Generate M3U playlist from channel config
# Format of dvbt_channels.conf:
# # Channel Name | Frequency (Hz) | Bandwidth | TX Mode | Constellation | Code Rate | Guard
# BBC One|506000000|8|8k|64qam|2/3|1/32
# ITV|514000000|8|8k|64qam|2/3|1/32
generate_m3u() {
M3U="$STREAMDIR/dvbt.m3u"
echo '#EXTM3U' > "$M3U"
if [ ! -f "$CHANNELS_FILE" ]; then
# Create default channel config
cat > "$CHANNELS_FILE" << 'CHANNELS'
# DVB-T Channel List
# Format: Name|Frequency(Hz)|Bandwidth(MHz)|TxMode|Constellation|CodeRate|Guard
# Edit this file to add your local channels
# Find channels with: rtl_mode_switch.sh spectrum (then check results)
#
# Example channels (UK Freeview):
Channel 1|506000000|8|8k|64qam|2/3|1/32
Channel 2|514000000|8|8k|64qam|2/3|1/32
Channel 3|522000000|8|8k|64qam|2/3|1/32
CHANNELS
mlog "Created default channel config at $CHANNELS_FILE"
fi
CH_NUM=1
while IFS='|' read -r NAME FREQ BW TXMODE CONST CR GUARD; do
# Skip comments and empty lines
case "$NAME" in
\#*|"") continue ;;
esac
# Each channel gets its own stream port
CH_PORT=$((KODI_PORT + CH_NUM))
echo "#EXTINF:-1,$NAME" >> "$M3U"
echo "http://127.0.0.1:$CH_PORT" >> "$M3U"
CH_NUM=$((CH_NUM + 1))
done < "$CHANNELS_FILE"
mlog "Generated M3U playlist: $M3U ($((CH_NUM - 1)) channels)"
}
# Start a stream server for a specific channel
# Called when Kodi connects to a channel URL
start_channel_stream() {
CH_PORT=$1
FREQ=$2
BW=$3
TXMODE=$4
CONST=$5
CR=$6
GUARD=$7
CH_NAME=$8
TERMUX="/data/data/com.termux/files/usr/bin"
SDR_TV="$MODDIR/scripts/sdr_tv.py"
FIFO="$STREAMDIR/ch_${CH_PORT}.ts"
rm -f "$FIFO"
mkfifo "$FIFO" 2>/dev/null
if [ -x "$TERMUX/python3" ] && [ -f "$SDR_TV" ]; then
# Start DVB-T demodulator
"$TERMUX/python3" "$SDR_TV" dvbt \
--freq "$FREQ" \
--bandwidth "$BW" \
--transmission-mode "$TXMODE" \
--constellation "$CONST" \
--code-rate "$CR" \
--guard-interval "$GUARD" \
> "$FIFO" 2>>"$LOGFILE" &
DVB_PID=$!
# HTTP stream server for this channel
(while true; do
(echo -e "HTTP/1.1 200 OK\r\nContent-Type: video/mp2t\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r"; cat "$FIFO") | \
nc -l -p "$CH_PORT" 2>/dev/null
# If Kodi disconnects, kill the demodulator
kill "$DVB_PID" 2>/dev/null
break
done) &
mlog "Channel stream started: $CH_NAME on port $CH_PORT (freq=$FREQ)"
else
mlog "ERROR: python3 or sdr_tv.py not found for channel stream"
fi
}
# Serve M3U playlist on the base port
serve_m3u() {
M3U="$STREAMDIR/dvbt.m3u"
(while true; do
M3U_CONTENT=$(cat "$M3U" 2>/dev/null)
RESPONSE="HTTP/1.1 200 OK\r\nContent-Type: audio/x-mpegurl\r\nContent-Length: ${#M3U_CONTENT}\r\nConnection: close\r\n\r\n${M3U_CONTENT}"
echo -e "$RESPONSE" | nc -l -p "$KODI_PORT" 2>/dev/null
done) &
mlog "M3U server on port $KODI_PORT"
}
case "$1" in
setup)
generate_m3u
serve_m3u
mlog "Kodi DVB-T setup complete"
mlog "Add to Kodi PVR IPTV Simple Client:"
mlog " M3U URL: http://127.0.0.1:$KODI_PORT/dvbt.m3u"
echo "Kodi setup complete."
echo "In Kodi: Settings > PVR & Live TV > PVR IPTV Simple Client"
echo "M3U URL: http://127.0.0.1:$KODI_PORT/dvbt.m3u"
;;
scan)
# Scan for DVB-T channels using spectrum analysis
echo "Scanning for DVB-T channels..."
echo "Common DVB-T frequency ranges:"
echo " VHF Band III: 174-230 MHz"
echo " UHF Band IV: 470-582 MHz"
echo " UHF Band V: 582-862 MHz"
echo ""
echo "Starting spectrum scan on UHF band..."
"$MODDIR/scripts/rtl_mode_switch.sh" spectrum
echo "Check $MODDIR/streams/spectrum_data.csv for results"
;;
channels)
# List configured channels
if [ -f "$CHANNELS_FILE" ]; then
echo "Configured DVB-T channels:"
grep -v "^#" "$CHANNELS_FILE" | grep -v "^$" | while IFS='|' read -r NAME FREQ BW TXMODE CONST CR GUARD; do
FREQ_MHZ=$(echo "scale=1; $FREQ / 1000000" | bc 2>/dev/null || echo "$FREQ")
echo " $NAME${FREQ_MHZ} MHz (${BW}MHz BW, $TXMODE, $CONST, $CR, $GUARD)"
done
else
echo "No channels configured. Edit: $CHANNELS_FILE"
fi
;;
*)
echo "Usage: kodi_dvbt_setup.sh {setup|scan|channels}"
echo ""
echo " setup - Generate M3U and start stream servers"
echo " scan - Scan spectrum for DVB-T channels"
echo " channels - List configured channels"
;;
esac

235
scripts/rtl_mode_switch.sh Executable file
View File

@@ -0,0 +1,235 @@
#!/system/bin/sh
# RTL-SDR Mode Switcher
# Switches between DVB-T (digital TV), FM Radio, and SDR scanner modes
# Manages the userspace process that controls the RTL-SDR dongle
#
# Only one mode can use the dongle at a time. This script kills
# the active process before starting a new one.
MODDIR="/data/adb/modules/driver-manager"
CONFDIR="$MODDIR/config"
LOGFILE="$MODDIR/driver-manager.log"
PIDDIR="$MODDIR/run"
TERMUX="/data/data/com.termux/files/usr/bin"
STREAMDIR="$MODDIR/streams"
mkdir -p "$PIDDIR" "$STREAMDIR"
mlog() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [rtl_switch] $1" >> "$LOGFILE"
}
# Kill any running RTL process that holds the dongle
kill_rtl() {
for pidfile in "$PIDDIR"/rtl_*.pid; do
[ -f "$pidfile" ] || continue
PID=$(cat "$pidfile")
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
kill "$PID" 2>/dev/null
sleep 1
kill -9 "$PID" 2>/dev/null
mlog "Killed PID $PID ($(basename "$pidfile" .pid))"
fi
rm -f "$pidfile"
done
# Also catch any strays
pkill -f rtl_tcp 2>/dev/null
pkill -f rtl_fm 2>/dev/null
pkill -f rtl_adsb 2>/dev/null
pkill -f rtl_power 2>/dev/null
pkill -f dvbt_rx 2>/dev/null
pkill -f sdr_tv 2>/dev/null
sleep 1
}
# Start rtl_tcp server — base layer for all modes
# Apps connect to localhost:1234 for I/Q data
start_rtl_tcp() {
PORT=$(cat "$CONFDIR/rtl_tcp_port" 2>/dev/null || echo "1234")
GAIN=$(cat "$CONFDIR/rtl_gain" 2>/dev/null || echo "0")
SRATE=$(cat "$CONFDIR/rtl_samplerate" 2>/dev/null || echo "2048000")
FREQ=$(cat "$CONFDIR/rtl_freq" 2>/dev/null || echo "100000000")
if [ -x "$TERMUX/rtl_tcp" ]; then
"$TERMUX/rtl_tcp" -a 127.0.0.1 -p "$PORT" -f "$FREQ" -s "$SRATE" -g "$GAIN" &
echo $! > "$PIDDIR/rtl_tcp.pid"
mlog "rtl_tcp started on port $PORT (freq=$FREQ srate=$SRATE gain=$GAIN)"
else
mlog "ERROR: rtl_tcp not found — install in Termux: pkg install rtl-sdr"
return 1
fi
}
MODE="$1"
[ -z "$MODE" ] && MODE=$(cat "$CONFDIR/rtl_mode" 2>/dev/null || echo "off")
case "$MODE" in
# =================================================================
# DVB-T Digital TV Mode
# =================================================================
# Receives DVB-T signal, outputs MPEG-TS stream
# Stream is served via HTTP on port 8554 for Kodi to consume
dvbt)
kill_rtl
FREQ=$(cat "$CONFDIR/dvbt_freq" 2>/dev/null || echo "506000000")
BW=$(cat "$CONFDIR/dvbt_bandwidth" 2>/dev/null || echo "8")
TXMODE=$(cat "$CONFDIR/dvbt_txmode" 2>/dev/null || echo "8k")
CONSTELLATION=$(cat "$CONFDIR/dvbt_constellation" 2>/dev/null || echo "64qam")
CODERATE=$(cat "$CONFDIR/dvbt_coderate" 2>/dev/null || echo "2/3")
GUARD=$(cat "$CONFDIR/dvbt_guard" 2>/dev/null || echo "1/32")
KODI_PORT=$(cat "$CONFDIR/kodi_stream_port" 2>/dev/null || echo "8554")
# Use the sdr_tv.py DVB-T receiver if available
SDR_TV="$MODDIR/scripts/sdr_tv.py"
DVBT_RX="$MODDIR/scripts/dvbt_rx.py"
if [ -x "$TERMUX/python3" ] && [ -f "$SDR_TV" ]; then
# Pipe MPEG-TS to a local HTTP server for Kodi
# Using socat or a simple netcat HTTP wrapper
FIFO="$STREAMDIR/dvbt.ts"
rm -f "$FIFO"
mkfifo "$FIFO" 2>/dev/null
# Start DVB-T receiver, output MPEG-TS to FIFO
"$TERMUX/python3" "$SDR_TV" dvbt \
--freq "$FREQ" \
--bandwidth "$BW" \
--transmission-mode "$TXMODE" \
--constellation "$CONSTELLATION" \
--code-rate "$CODERATE" \
--guard-interval "$GUARD" \
> "$FIFO" 2>>"$LOGFILE" &
echo $! > "$PIDDIR/rtl_dvbt.pid"
# HTTP stream server — serves MPEG-TS on port for Kodi
# Kodi connects to http://127.0.0.1:8554/dvbt.ts
(while true; do
cat "$FIFO" | {
read -r _ # wait for connection
echo -e "HTTP/1.1 200 OK\r\nContent-Type: video/mp2t\r\nConnection: close\r\n\r"
cat
} | nc -l -p "$KODI_PORT" 2>/dev/null
done) &
echo $! > "$PIDDIR/rtl_dvbt_http.pid"
mlog "DVB-T started: freq=$FREQ bw=${BW}MHz mode=$TXMODE"
mlog "Kodi stream: http://127.0.0.1:$KODI_PORT"
elif [ -x "$TERMUX/rtl_tcp" ]; then
# Fallback: just start rtl_tcp, let an Android DVB app handle it
echo "$FREQ" > "$CONFDIR/rtl_freq"
start_rtl_tcp
mlog "DVB-T fallback: rtl_tcp on port 1234, use SDR Touch or rtl_tcp_andro"
else
mlog "ERROR: no DVB-T tools found"
fi
echo "dvbt" > "$CONFDIR/rtl_mode"
;;
# =================================================================
# FM Radio Mode
# =================================================================
fm)
kill_rtl
FREQ=$(cat "$CONFDIR/fm_freq" 2>/dev/null || echo "100000000")
GAIN=$(cat "$CONFDIR/rtl_gain" 2>/dev/null || echo "0")
if [ -x "$TERMUX/rtl_fm" ]; then
# Demodulate FM and pipe to audio output
"$TERMUX/rtl_fm" -f "$FREQ" -M wbfm -s 200000 -r 48000 -g "$GAIN" - 2>>"$LOGFILE" | \
"$TERMUX/play" -r 48000 -b 16 -c 1 -e signed-integer -t raw - 2>/dev/null &
echo $! > "$PIDDIR/rtl_fm.pid"
mlog "FM radio started: freq=$FREQ gain=$GAIN"
else
mlog "ERROR: rtl_fm not found — install in Termux: pkg install rtl-sdr"
fi
echo "fm" > "$CONFDIR/rtl_mode"
;;
# =================================================================
# SDR Scanner Mode
# =================================================================
# Starts rtl_tcp server for any SDR app to connect
sdr)
kill_rtl
start_rtl_tcp
echo "sdr" > "$CONFDIR/rtl_mode"
;;
# =================================================================
# ADS-B Aircraft Tracking
# =================================================================
adsb)
kill_rtl
if [ -x "$TERMUX/rtl_adsb" ]; then
"$TERMUX/rtl_adsb" -g 50 > "$STREAMDIR/adsb_output.txt" 2>>"$LOGFILE" &
echo $! > "$PIDDIR/rtl_adsb.pid"
mlog "ADS-B tracking started (1090 MHz)"
else
mlog "ERROR: rtl_adsb not found"
fi
echo "adsb" > "$CONFDIR/rtl_mode"
;;
# =================================================================
# Spectrum Scanner
# =================================================================
spectrum)
kill_rtl
RANGE=$(cat "$CONFDIR/spectrum_range" 2>/dev/null || echo "24M:1800M")
if [ -x "$TERMUX/rtl_power" ]; then
"$TERMUX/rtl_power" -f "$RANGE" -g 50 -i 1 "$STREAMDIR/spectrum_data.csv" 2>>"$LOGFILE" &
echo $! > "$PIDDIR/rtl_spectrum.pid"
mlog "Spectrum scan started: $RANGE"
else
mlog "ERROR: rtl_power not found"
fi
echo "spectrum" > "$CONFDIR/rtl_mode"
;;
# =================================================================
# HackRF TX/RX Mode
# =================================================================
hackrf)
kill_rtl
# HackRF uses its own USB interface, doesn't conflict with RTL-SDR
# Just set the mode flag — apps handle the rest
mlog "HackRF mode set — use hackrf_transfer or rtl_tcp_andro"
echo "hackrf" > "$CONFDIR/rtl_mode"
;;
# =================================================================
# Off
# =================================================================
off)
kill_rtl
mlog "All SDR processes stopped"
echo "off" > "$CONFDIR/rtl_mode"
;;
# =================================================================
# Status
# =================================================================
status)
CURRENT=$(cat "$CONFDIR/rtl_mode" 2>/dev/null || echo "off")
echo "Mode: $CURRENT"
for pidfile in "$PIDDIR"/rtl_*.pid; do
[ -f "$pidfile" ] || continue
PID=$(cat "$pidfile")
NAME=$(basename "$pidfile" .pid)
if kill -0 "$PID" 2>/dev/null; then
echo " $NAME: running (PID $PID)"
else
echo " $NAME: dead"
fi
done
;;
*)
echo "Usage: rtl_mode_switch.sh {dvbt|fm|sdr|adsb|spectrum|hackrf|off|status}"
exit 1
;;
esac

714
scripts/sdr_tv.py Executable file
View File

@@ -0,0 +1,714 @@
#!/usr/bin/env python3
"""
SDR TV & RF Analysis Tool
Multi-mode SDR application for RTL-SDR V4:
- DVB-T digital TV reception (MPEG-TS output)
- Analog TV reception (PAL/NTSC via FM demod)
- Spectrum analyzer (terminal-based power display)
- Wireless camera scanner (common security camera frequencies)
Designed for authorized RF security testing and research.
Usage:
python3 sdr_tv.py dvbt --freq 506e6 | mpv -
python3 sdr_tv.py analog --freq 175.25e6 --standard pal | mpv --demuxer=rawvideo --rawvideo=w=768:h=576:format=gray -
python3 sdr_tv.py spectrum --freq 500e6 --span 20e6
python3 sdr_tv.py camera --preset 1.2ghz
"""
import argparse
import signal
import sys
import time
import struct
import threading
import numpy as np
from gnuradio import gr, blocks, fft, dtv, analog, filter
import osmosdr
# =============================================================================
# Constants & Presets
# =============================================================================
# DVB-T parameters
TRANSMISSION_MODE_MAP = {
'2k': (dtv.T2k, 2048, 1705),
'8k': (dtv.T8k, 8192, 6817),
}
CONSTELLATION_MAP = {
'qpsk': dtv.MOD_QPSK,
'16qam': dtv.MOD_16QAM,
'64qam': dtv.MOD_64QAM,
}
CODE_RATE_MAP = {
'1/2': dtv.C1_2,
'2/3': dtv.C2_3,
'3/4': dtv.C3_4,
'5/6': dtv.C5_6,
'7/8': dtv.C7_8,
}
GUARD_INTERVAL_MAP = {
'1/4': (dtv.GI_1_4, 4),
'1/8': (dtv.GI_1_8, 8),
'1/16': (dtv.GI_1_16, 16),
'1/32': (dtv.GI_1_32, 32),
}
# Wireless camera frequency presets for authorized security testing
# These are commonly documented frequencies used by analog wireless cameras
CAMERA_PRESETS = {
'900mhz': {
'name': '900 MHz ISM Band',
'channels': [
(910.0e6, '910 MHz Ch1'),
(920.0e6, '920 MHz Ch2'),
(930.0e6, '930 MHz Ch3'),
(940.0e6, '940 MHz Ch4'),
],
'bandwidth': 6.0e6,
'modulation': 'fm',
},
'1.2ghz': {
'name': '1.2 GHz Band',
'channels': [
(1080.0e6, '1080 MHz Ch1'),
(1120.0e6, '1120 MHz Ch2'),
(1160.0e6, '1160 MHz Ch3'),
(1200.0e6, '1200 MHz Ch4'),
(1240.0e6, '1240 MHz Ch5'),
(1280.0e6, '1280 MHz Ch6'),
(1320.0e6, '1320 MHz Ch7'),
],
'bandwidth': 10.0e6,
'modulation': 'fm',
},
'2.4ghz': {
'name': '2.4 GHz ISM Band',
'channels': [
(2411.0e6, '2411 MHz Ch1'),
(2431.0e6, '2431 MHz Ch2'),
(2451.0e6, '2451 MHz Ch3'),
(2473.0e6, '2473 MHz Ch4'),
],
'bandwidth': 10.0e6,
'modulation': 'fm',
},
'fpv': {
'name': 'FPV/Drone Video (5.8 GHz — RTL-SDR cannot tune here)',
'channels': [
(5740.0e6, '5740 MHz R1'),
(5760.0e6, '5760 MHz R2'),
(5780.0e6, '5780 MHz R3'),
(5800.0e6, '5800 MHz R4'),
(5820.0e6, '5820 MHz R5'),
(5840.0e6, '5840 MHz R6'),
(5860.0e6, '5860 MHz R7'),
(5880.0e6, '5880 MHz R8'),
],
'bandwidth': 20.0e6,
'modulation': 'fm',
},
}
def parse_freq(s):
"""Parse frequency string, supporting scientific notation like 506e6."""
return int(float(s))
def log(msg):
"""Print to stderr so stdout stays clean for data output."""
print(msg, file=sys.stderr)
# =============================================================================
# RTL-SDR Source Helper
# =============================================================================
def create_rtl_source(freq, samp_rate, gain=40.0, if_gain=40.0, ppm=0.0):
"""Create and configure an osmosdr RTL-SDR source."""
source = osmosdr.source(args="numchan=1")
source.set_sample_rate(samp_rate)
source.set_center_freq(freq)
source.set_freq_corr(ppm)
source.set_gain_mode(False)
source.set_gain(gain)
source.set_if_gain(if_gain)
source.set_bb_gain(0)
return source
# =============================================================================
# Mode 1: DVB-T Digital TV Receiver
# =============================================================================
class DVBTReceiver(gr.top_block):
def __init__(self, args):
gr.top_block.__init__(self, "DVB-T Receiver")
tx_mode_enum, fft_length, active_carriers = TRANSMISSION_MODE_MAP[args.transmission_mode]
constellation = CONSTELLATION_MAP[args.constellation]
code_rate = CODE_RATE_MAP[args.code_rate]
gi_enum, gi_divisor = GUARD_INTERVAL_MAP[args.guard_interval]
cp_length = fft_length // gi_divisor
if args.bandwidth == 8:
samp_rate = 64e6 / 7.0
elif args.bandwidth == 7:
samp_rate = 8e6
else:
samp_rate = 48e6 / 7.0
log(f"=== DVB-T Digital Receiver ===")
log(f"Frequency: {args.freq / 1e6:.3f} MHz")
log(f"Bandwidth: {args.bandwidth} MHz")
log(f"Sample rate: {samp_rate:.0f} Hz")
log(f"Mode: {args.transmission_mode} (FFT={fft_length})")
log(f"Constellation: {args.constellation}")
log(f"Code rate: {args.code_rate}")
log(f"Guard interval: {args.guard_interval} (CP={cp_length})")
# Source
if args.input_file:
log(f"Source: file '{args.input_file}'")
self.source = blocks.file_source(gr.sizeof_gr_complex, args.input_file, repeat=False)
else:
log(f"Source: RTL-SDR (live)")
self.source = create_rtl_source(args.freq, samp_rate, args.gain, args.if_gain, args.ppm)
actual = self.source.get_sample_rate()
if abs(actual - samp_rate) > 1000:
log(f"WARNING: Got {actual:.0f} Hz instead of {samp_rate:.0f} Hz")
log(f"WARNING: RTL-SDR can't sample fast enough for DVB-T.")
log(f"WARNING: Use --input-file with a pre-recorded IQ file.")
# OFDM demodulation chain
self.s2v = blocks.stream_to_vector(gr.sizeof_gr_complex, fft_length)
self.ofdm_acq = dtv.dvbt_ofdm_sym_acquisition(
blocks=1, fft_length=fft_length,
occupied_tones=active_carriers, cp_length=cp_length, snr=100.0
)
self.fft_block = fft.fft_vcc(fft_length, True, [], False)
self.demod_ref = dtv.dvbt_demod_reference_signals(
gr.sizeof_gr_complex, fft_length, active_carriers,
constellation, dtv.NH, code_rate, dtv.C1_2,
gi_enum, tx_mode_enum, 0, 0
)
self.v2s = blocks.vector_to_stream(gr.sizeof_gr_complex, active_carriers)
self.demap = dtv.dvbt_demap(active_carriers, constellation, dtv.NH, tx_mode_enum, 1.0)
self.bit_deintlv = dtv.dvbt_bit_inner_deinterleaver(
active_carriers, constellation, dtv.NH, tx_mode_enum
)
self.viterbi = dtv.dvbt_viterbi_decoder(constellation, dtv.NH, code_rate, active_carriers)
self.conv_deintlv = dtv.dvbt_convolutional_deinterleaver(136, 12, 17)
self.rs_dec = dtv.dvbt_reed_solomon_dec(2, 8, 0x11d, 255, 239, 8, 51, 8)
self.descramble = dtv.dvbt_energy_descramble(8)
self.sink = blocks.file_descriptor_sink(gr.sizeof_char, 1)
# Wire it up
self.connect(
self.source, self.s2v, self.ofdm_acq, self.fft_block,
self.demod_ref, self.v2s, self.demap, self.bit_deintlv,
self.viterbi, self.conv_deintlv, self.rs_dec,
self.descramble, self.sink
)
log("Flowgraph ready. MPEG-TS output on stdout.")
# =============================================================================
# Mode 2: Analog TV Receiver (PAL/NTSC via Wideband FM Demod)
# =============================================================================
class AnalogTVReceiver(gr.top_block):
"""
Analog TV receiver using wideband FM demodulation.
Outputs raw baseband video as unsigned 8-bit samples to stdout.
Analog TV transmits composite video via AM on the picture carrier,
with FM audio offset +4.5 MHz (NTSC) or +5.5 MHz (PAL).
This receiver demodulates the composite video baseband signal.
"""
def __init__(self, args):
gr.top_block.__init__(self, "Analog TV Receiver")
samp_rate = 2.4e6 # RTL-SDR max stable rate
audio_rate = 48000
if args.standard == 'pal':
fm_deviation = 5.5e6
video_bw = 5.0e6
else: # ntsc
fm_deviation = 4.5e6
video_bw = 4.2e6
log(f"=== Analog TV Receiver ({args.standard.upper()}) ===")
log(f"Frequency: {args.freq / 1e6:.3f} MHz")
log(f"Sample rate: {samp_rate:.0f} Hz")
log(f"Standard: {args.standard.upper()}")
# Source
if args.input_file:
log(f"Source: file '{args.input_file}'")
self.source = blocks.file_source(gr.sizeof_gr_complex, args.input_file, repeat=False)
else:
log(f"Source: RTL-SDR (live)")
self.source = create_rtl_source(args.freq, samp_rate, args.gain, args.if_gain, args.ppm)
# FM demodulator for composite video
self.fm_demod = analog.wfm_rcv(
quad_rate=samp_rate,
audio_decimation=1,
)
# Low-pass filter to isolate video baseband
video_taps = filter.firdes.low_pass(
1.0, samp_rate, min(video_bw, samp_rate / 2 - 100e3), 100e3
)
self.video_lpf = filter.fir_filter_fff(1, video_taps)
# Scale float [-1,1] to unsigned byte [0,255] for raw video output
self.scale = blocks.multiply_const_ff(127.0)
self.offset = blocks.add_const_ff(128.0)
self.f2c = blocks.float_to_uchar()
# Output to stdout
self.sink = blocks.file_descriptor_sink(gr.sizeof_char, 1)
self.connect(
self.source, self.fm_demod, self.video_lpf,
self.scale, self.offset, self.f2c, self.sink
)
log("Flowgraph ready. Raw video on stdout.")
log(f"Play with: python3 sdr_tv.py analog ... | mpv --demuxer=rawvideo --rawvideo=w=720:h=576:format=gray -")
# =============================================================================
# Mode 3: Spectrum Analyzer (Terminal-based)
# =============================================================================
class SpectrumAnalyzer(gr.top_block):
"""
Real-time terminal spectrum analyzer.
Captures IQ, computes FFT, and displays power spectrum in the terminal.
"""
def __init__(self, args):
gr.top_block.__init__(self, "Spectrum Analyzer")
self.fft_size = args.fft_size
self.samp_rate = min(args.span, 2.4e6) # RTL-SDR limited
self.center_freq = args.freq
self.args = args
log(f"=== Spectrum Analyzer ===")
log(f"Center: {args.freq / 1e6:.3f} MHz")
log(f"Span: {self.samp_rate / 1e6:.1f} MHz")
log(f"FFT size: {self.fft_size}")
log(f"Bin width: {self.samp_rate / self.fft_size:.0f} Hz")
# Source
if args.input_file:
self.source = blocks.file_source(gr.sizeof_gr_complex, args.input_file, repeat=True)
else:
self.source = create_rtl_source(args.freq, self.samp_rate, args.gain, args.if_gain, args.ppm)
# FFT -> magnitude squared -> log
self.s2v = blocks.stream_to_vector(gr.sizeof_gr_complex, self.fft_size)
self.fft_block = fft.fft_vcc(self.fft_size, True,
fft.window.blackmanharris(self.fft_size), True)
self.mag = blocks.complex_to_mag_squared(self.fft_size)
# Probe to read FFT data from Python
self.probe = blocks.probe_signal_vf(self.fft_size)
self.connect(self.source, self.s2v, self.fft_block, self.mag, self.probe)
def display_loop(self):
"""Terminal display loop — runs in main thread."""
cols = 80
try:
cols = os.get_terminal_size().columns
except Exception:
pass
log("")
while True:
try:
data = self.probe.level()
if len(data) == 0:
time.sleep(0.1)
continue
power = np.array(data)
# Convert to dB, avoiding log(0)
power_db = 10.0 * np.log10(np.maximum(power, 1e-12))
# Bin down to terminal width
num_bins = min(cols - 12, self.fft_size)
if num_bins < self.fft_size:
binned = np.mean(power_db.reshape(num_bins, -1), axis=1)
else:
binned = power_db[:num_bins]
db_min = self.args.db_min
db_max = self.args.db_max
height = 20
# Clear screen and draw
sys.stderr.write('\033[2J\033[H')
freq_start = (self.center_freq - self.samp_rate / 2) / 1e6
freq_end = (self.center_freq + self.samp_rate / 2) / 1e6
sys.stderr.write(f' Spectrum: {freq_start:.2f} - {freq_end:.2f} MHz '
f'(center {self.center_freq/1e6:.3f} MHz)\n')
sys.stderr.write(f' Range: {db_min} to {db_max} dB\n\n')
# Draw waterfall-style rows
for row in range(height - 1, -1, -1):
db_level = db_min + (db_max - db_min) * row / height
label = f'{db_level:6.0f}|'
line = []
for b in range(num_bins):
if binned[b] >= db_level:
intensity = min(1.0, (binned[b] - db_level) / ((db_max - db_min) / height))
if intensity > 0.7:
line.append('\033[91m\u2588\033[0m') # red = strong
elif intensity > 0.3:
line.append('\033[93m\u2593\033[0m') # yellow = medium
else:
line.append('\033[92m\u2591\033[0m') # green = weak
else:
line.append(' ')
sys.stderr.write(label + ''.join(line) + '\n')
# Frequency axis
sys.stderr.write(' ' * 7 + '\u2514' + '\u2500' * num_bins + '\n')
axis = ' ' * 7
axis += f'{freq_start:.1f}'
mid_pos = num_bins // 2 - 4
axis += ' ' * (mid_pos - len(f'{freq_start:.1f}'))
axis += f'{self.center_freq/1e6:.1f}'
end_pos = num_bins - len(f'{freq_end:.1f}') - mid_pos
axis += ' ' * max(1, end_pos)
axis += f'{freq_end:.1f}'
sys.stderr.write(axis + ' MHz\n')
# Peak info
peak_bin = np.argmax(binned)
peak_freq = freq_start + (freq_end - freq_start) * peak_bin / num_bins
sys.stderr.write(f'\n Peak: {binned[peak_bin]:.1f} dB @ {peak_freq:.3f} MHz\n')
sys.stderr.write(f' [Ctrl+C to quit]\n')
time.sleep(0.1)
except (ValueError, RuntimeError):
time.sleep(0.2)
except KeyboardInterrupt:
break
# =============================================================================
# Mode 4: Wireless Camera Scanner
# =============================================================================
class CameraScanner(gr.top_block):
"""
Scans known wireless camera frequencies and reports signal strength.
For authorized security testing only.
"""
def __init__(self, args):
gr.top_block.__init__(self, "Camera Scanner")
self.samp_rate = 2.4e6
self.fft_size = 1024
self.args = args
# Start with a dummy frequency, we'll retune
self.source = create_rtl_source(100e6, self.samp_rate, args.gain, args.if_gain, args.ppm)
self.s2v = blocks.stream_to_vector(gr.sizeof_gr_complex, self.fft_size)
self.fft_block = fft.fft_vcc(self.fft_size, True,
fft.window.blackmanharris(self.fft_size), True)
self.mag = blocks.complex_to_mag_squared(self.fft_size)
self.probe = blocks.probe_signal_vf(self.fft_size)
self.connect(self.source, self.s2v, self.fft_block, self.mag, self.probe)
def measure_power(self, freq, dwell=0.5):
"""Tune to frequency and measure average power."""
self.source.set_center_freq(freq)
time.sleep(dwell)
try:
data = np.array(self.probe.level())
if len(data) == 0:
return -100.0
power = 10.0 * np.log10(np.maximum(np.mean(data), 1e-12))
return power
except (ValueError, RuntimeError):
return -100.0
def scan_loop(self):
"""Scan all camera presets and display results."""
presets = self.args.preset
if presets == 'all':
scan_presets = list(CAMERA_PRESETS.keys())
else:
scan_presets = [presets]
log("\n=== Wireless Camera Scanner ===")
log("For authorized security testing only.\n")
# RTL-SDR V4 tunes roughly 24 MHz - 1766 MHz
max_freq = 1766e6
while True:
try:
sys.stderr.write('\033[2J\033[H')
sys.stderr.write('=== Wireless Camera Scanner ===\n')
sys.stderr.write('Authorized security testing only.\n\n')
for preset_name in scan_presets:
preset = CAMERA_PRESETS[preset_name]
sys.stderr.write(f'--- {preset["name"]} ---\n')
for freq, label in preset['channels']:
if freq > max_freq:
sys.stderr.write(f' {label:25s} [OUT OF RANGE for RTL-SDR]\n')
continue
power = self.measure_power(freq, dwell=0.3)
# Signal strength bar
bar_len = max(0, int((power + 60) / 2))
bar = '\u2588' * bar_len
if power > -20:
color = '\033[91m' # red = strong signal
status = 'STRONG'
elif power > -35:
color = '\033[93m' # yellow = moderate
status = 'MODERATE'
elif power > -50:
color = '\033[92m' # green = weak
status = 'WEAK'
else:
color = '\033[90m' # gray = noise floor
status = 'noise'
sys.stderr.write(
f' {label:25s} {power:7.1f} dB '
f'{color}{bar:20s} {status}\033[0m\n'
)
sys.stderr.write('\n')
sys.stderr.write('[Scanning... Ctrl+C to quit]\n')
time.sleep(1.0)
except KeyboardInterrupt:
break
# =============================================================================
# Mode 5: Camera Video Receiver (tune to a specific camera freq and demod)
# =============================================================================
class CameraVideoReceiver(gr.top_block):
"""
Receives analog wireless camera video via wideband FM demodulation.
Most analog wireless cameras use FM modulated composite video.
"""
def __init__(self, args):
gr.top_block.__init__(self, "Camera Video Receiver")
samp_rate = 2.4e6
log(f"=== Camera Video Receiver ===")
log(f"Frequency: {args.freq / 1e6:.3f} MHz")
log(f"Demodulation: Wideband FM")
self.source = create_rtl_source(args.freq, samp_rate, args.gain, args.if_gain, args.ppm)
# Wideband FM demod (analog cameras typically use ~5 MHz deviation)
self.fm_demod = analog.wfm_rcv(quad_rate=samp_rate, audio_decimation=1)
# Low-pass to get composite video
video_taps = filter.firdes.low_pass(1.0, samp_rate, 1.0e6, 200e3)
self.video_lpf = filter.fir_filter_fff(1, video_taps)
# Float to byte for output
self.scale = blocks.multiply_const_ff(127.0)
self.offset = blocks.add_const_ff(128.0)
self.f2c = blocks.float_to_uchar()
self.sink = blocks.file_descriptor_sink(gr.sizeof_char, 1)
self.connect(self.source, self.fm_demod, self.video_lpf,
self.scale, self.offset, self.f2c, self.sink)
log("Raw composite video on stdout.")
log("View with: python3 sdr_tv.py camera-rx ... | ffplay -f rawvideo -pix_fmt gray -video_size 720x576 -")
# =============================================================================
# CLI
# =============================================================================
import os
def main():
parser = argparse.ArgumentParser(
description='SDR TV & RF Analysis Tool for RTL-SDR V4',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Modes:
dvbt DVB-T digital TV receiver (outputs MPEG-TS to stdout)
analog Analog TV receiver PAL/NTSC (outputs raw video to stdout)
spectrum Real-time terminal spectrum analyzer
camera Scan wireless camera frequencies for signals
camera-rx Receive/demodulate a wireless camera signal
Examples:
python3 sdr_tv.py dvbt --freq 506e6 | mpv -
python3 sdr_tv.py analog --freq 175.25e6 | ffplay -f rawvideo -pix_fmt gray -video_size 720x576 -
python3 sdr_tv.py spectrum --freq 500e6
python3 sdr_tv.py camera --preset all
python3 sdr_tv.py camera --preset 1.2ghz
python3 sdr_tv.py camera-rx --freq 1200e6 | ffplay -f rawvideo -pix_fmt gray -video_size 720x576 -
"""
)
subparsers = parser.add_subparsers(dest='mode', required=True)
# Common arguments for all modes
def add_common_args(p):
p.add_argument('--gain', type=float, default=40.0, help='RF gain dB (default: 40)')
p.add_argument('--if-gain', type=float, default=40.0, help='IF gain dB (default: 40)')
p.add_argument('--ppm', type=float, default=0.0, help='Freq correction PPM (default: 0)')
p.add_argument('--input-file', type=str, default=None, help='IQ file instead of live SDR')
# --- DVB-T ---
p_dvbt = subparsers.add_parser('dvbt', help='DVB-T digital TV receiver')
p_dvbt.add_argument('--freq', type=parse_freq, required=True, help='Center freq Hz')
p_dvbt.add_argument('--bandwidth', type=int, default=8, choices=[6, 7, 8], help='BW MHz')
p_dvbt.add_argument('--transmission-mode', default='8k', choices=['2k', '8k'])
p_dvbt.add_argument('--constellation', default='64qam', choices=['qpsk', '16qam', '64qam'])
p_dvbt.add_argument('--code-rate', default='2/3', choices=['1/2', '2/3', '3/4', '5/6', '7/8'])
p_dvbt.add_argument('--guard-interval', default='1/32', choices=['1/4', '1/8', '1/16', '1/32'])
add_common_args(p_dvbt)
# --- Analog TV ---
p_analog = subparsers.add_parser('analog', help='Analog TV receiver (PAL/NTSC)')
p_analog.add_argument('--freq', type=parse_freq, required=True, help='Picture carrier freq Hz')
p_analog.add_argument('--standard', default='pal', choices=['pal', 'ntsc'], help='TV standard')
add_common_args(p_analog)
# --- Spectrum Analyzer ---
p_spec = subparsers.add_parser('spectrum', help='Terminal spectrum analyzer')
p_spec.add_argument('--freq', type=parse_freq, required=True, help='Center freq Hz')
p_spec.add_argument('--span', type=float, default=2.4e6, help='Span Hz (max 2.4M for RTL-SDR)')
p_spec.add_argument('--fft-size', type=int, default=1024, help='FFT size (default: 1024)')
p_spec.add_argument('--db-min', type=float, default=-60.0, help='Min dB for display')
p_spec.add_argument('--db-max', type=float, default=0.0, help='Max dB for display')
add_common_args(p_spec)
# --- Camera Scanner ---
p_cam = subparsers.add_parser('camera', help='Scan wireless camera frequencies')
p_cam.add_argument('--preset', default='all',
choices=list(CAMERA_PRESETS.keys()) + ['all'],
help='Camera freq preset to scan')
add_common_args(p_cam)
# --- Camera Video Receiver ---
p_camrx = subparsers.add_parser('camera-rx', help='Receive wireless camera video')
p_camrx.add_argument('--freq', type=parse_freq, required=True, help='Camera freq Hz')
add_common_args(p_camrx)
args = parser.parse_args()
# Signal handling
def signal_handler(sig, frame):
log("\nStopping...")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
# Dispatch
if args.mode == 'dvbt':
if hasattr(sys.stdout, 'buffer'):
sys.stdout = sys.stdout.buffer
tb = DVBTReceiver(args)
tb.start()
try:
tb.wait()
except KeyboardInterrupt:
tb.stop()
tb.wait()
elif args.mode == 'analog':
if hasattr(sys.stdout, 'buffer'):
sys.stdout = sys.stdout.buffer
tb = AnalogTVReceiver(args)
tb.start()
try:
tb.wait()
except KeyboardInterrupt:
tb.stop()
tb.wait()
elif args.mode == 'spectrum':
tb = SpectrumAnalyzer(args)
tb.start()
try:
tb.display_loop()
except KeyboardInterrupt:
pass
tb.stop()
tb.wait()
elif args.mode == 'camera':
tb = CameraScanner(args)
tb.start()
try:
tb.scan_loop()
except KeyboardInterrupt:
pass
tb.stop()
tb.wait()
elif args.mode == 'camera-rx':
if hasattr(sys.stdout, 'buffer'):
sys.stdout = sys.stdout.buffer
tb = CameraVideoReceiver(args)
tb.start()
try:
tb.wait()
except KeyboardInterrupt:
tb.stop()
tb.wait()
if __name__ == '__main__':
main()

View File

@@ -241,6 +241,100 @@
</div> </div>
</div> </div>
<!-- RTL Mode Switcher -->
<div class="card">
<div class="card-title"><span class="dot" id="rtlDot"></span> RTL-SDR Mode</div>
<div class="row">
<div>
<div class="row-label">Active Mode</div>
<div class="row-desc">Only one mode can use the dongle at a time</div>
</div>
<select class="sel" id="rtlMode" onchange="switchRtlMode(this.value)">
<option value="off">Off</option>
<option value="dvbt">DVB-T (Live TV)</option>
<option value="fm">FM Radio</option>
<option value="sdr">SDR Scanner (rtl_tcp)</option>
<option value="adsb">ADS-B Tracking</option>
<option value="spectrum">Spectrum Scan</option>
<option value="hackrf">HackRF TX/RX</option>
</select>
</div>
<div class="row">
<div><div class="row-label">Process Status</div></div>
<div class="row-value" id="rtlStatus"></div>
</div>
</div>
<!-- DVB-T / Kodi -->
<div class="card">
<div class="card-title"><span class="dot" id="kodiDot"></span> DVB-T Live TV / Kodi</div>
<div class="row">
<div>
<div class="row-label">DVB-T Frequency</div>
<div class="row-desc">Channel center frequency (Hz)</div>
</div>
<select class="sel" id="dvbtFreq" onchange="setConf('dvbt_freq', this.value)">
<option value="474000000">474 MHz (Ch 21)</option>
<option value="482000000">482 MHz (Ch 22)</option>
<option value="490000000">490 MHz (Ch 23)</option>
<option value="498000000">498 MHz (Ch 24)</option>
<option value="506000000">506 MHz (Ch 25)</option>
<option value="514000000">514 MHz (Ch 26)</option>
<option value="522000000">522 MHz (Ch 27)</option>
<option value="530000000">530 MHz (Ch 28)</option>
<option value="538000000">538 MHz (Ch 29)</option>
<option value="546000000">546 MHz (Ch 30)</option>
<option value="554000000">554 MHz (Ch 31)</option>
<option value="562000000">562 MHz (Ch 32)</option>
<option value="570000000">570 MHz (Ch 33)</option>
<option value="578000000">578 MHz (Ch 34)</option>
<option value="586000000">586 MHz (Ch 35)</option>
<option value="594000000">594 MHz (Ch 36)</option>
<option value="602000000">602 MHz (Ch 37)</option>
<option value="610000000">610 MHz (Ch 38)</option>
<option value="618000000">618 MHz (Ch 39)</option>
<option value="626000000">626 MHz (Ch 40)</option>
</select>
</div>
<div class="row">
<div>
<div class="row-label">Kodi Stream</div>
<div class="row-desc">Add to Kodi PVR IPTV Simple Client</div>
</div>
<div class="row-value" id="kodiUrl">http://127.0.0.1:8554</div>
</div>
<div class="btn-row">
<button class="btn" onclick="exec('sh /data/adb/modules/driver-manager/scripts/kodi_dvbt_setup.sh setup'); log('Kodi DVB-T setup started')">Setup Kodi</button>
<button class="btn" onclick="exec('sh /data/adb/modules/driver-manager/scripts/kodi_dvbt_setup.sh scan'); log('Channel scan started')">Scan Channels</button>
</div>
</div>
<!-- FM Radio -->
<div class="card">
<div class="card-title"><span class="dot" id="fmDot"></span> FM Radio</div>
<div class="row">
<div>
<div class="row-label">Frequency (MHz)</div>
<div class="row-desc">FM broadcast frequency</div>
</div>
<select class="sel" id="fmFreq" onchange="setConf('fm_freq', this.value)">
<option value="88100000">88.1</option>
<option value="89900000">89.9</option>
<option value="91500000">91.5</option>
<option value="93100000">93.1</option>
<option value="95500000">95.5</option>
<option value="97100000">97.1</option>
<option value="98500000">98.5</option>
<option value="100000000">100.0</option>
<option value="101500000">101.5</option>
<option value="103100000">103.1</option>
<option value="104700000">104.7</option>
<option value="106300000">106.3</option>
<option value="107900000">107.9</option>
</select>
</div>
</div>
<!-- Game Controllers --> <!-- Game Controllers -->
<div class="card"> <div class="card">
<div class="card-title"><span class="dot" id="padDot"></span> Game Controllers</div> <div class="card-title"><span class="dot" id="padDot"></span> Game Controllers</div>
@@ -301,6 +395,42 @@
catch (e) { return ''; } catch (e) { return ''; }
} }
async function switchRtlMode(mode) {
log('Switching RTL mode to: ' + mode);
await exec('sh ' + MODDIR + '/scripts/rtl_mode_switch.sh ' + mode);
log('RTL mode switched to: ' + mode);
await loadRtlStatus();
}
async function setConf(file, value) {
await exec('echo "' + value + '" > ' + MODDIR + '/config/' + file);
log('Set ' + file + ' = ' + value);
}
async function loadRtlStatus() {
const mode = (await exec('cat ' + MODDIR + '/config/rtl_mode 2>/dev/null')).stdout.trim();
if (mode) document.getElementById('rtlMode').value = mode;
const status = (await exec('sh ' + MODDIR + '/scripts/rtl_mode_switch.sh status 2>/dev/null')).stdout.trim();
document.getElementById('rtlStatus').textContent = status || 'off';
const dot = document.getElementById('rtlDot');
dot.className = 'dot' + (mode && mode !== 'off' ? '' : ' off');
const kodiDot = document.getElementById('kodiDot');
kodiDot.className = 'dot' + (mode === 'dvbt' ? '' : ' off');
const fmDot = document.getElementById('fmDot');
fmDot.className = 'dot' + (mode === 'fm' ? '' : ' off');
// Load saved frequencies
const dvbtFreq = (await exec('cat ' + MODDIR + '/config/dvbt_freq 2>/dev/null')).stdout.trim();
if (dvbtFreq) document.getElementById('dvbtFreq').value = dvbtFreq;
const fmFreq = (await exec('cat ' + MODDIR + '/config/fm_freq 2>/dev/null')).stdout.trim();
if (fmFreq) document.getElementById('fmFreq').value = fmFreq;
}
async function setMode(file, value) { async function setMode(file, value) {
log('Setting ' + file + ' = ' + value); log('Setting ' + file + ' = ' + value);
await exec('echo "' + value + '" > ' + MODDIR + '/config/' + file); await exec('echo "' + value + '" > ' + MODDIR + '/config/' + file);
@@ -388,6 +518,7 @@
await loadWifiInfo(); await loadWifiInfo();
await loadSdrInfo(); await loadSdrInfo();
await loadControllerInfo(); await loadControllerInfo();
await loadRtlStatus();
log('Done'); log('Done');
} }