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:
1
build.sh
1
build.sh
@@ -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
295
scripts/dvbt_rx.py
Executable 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
168
scripts/kodi_dvbt_setup.sh
Executable 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
235
scripts/rtl_mode_switch.sh
Executable 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
714
scripts/sdr_tv.py
Executable 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()
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user