examples: build FM receiver programmatically via GRC Platform API
Replace template-based prepare_flowgraph() with build_fm_receiver() that constructs the entire flowgraph in Python code: - Initialize Platform (same as gr-mcp main.py) - Create variables: samp_rate, freq - Create blocks: osmosdr_source, low_pass_filter, analog_wfm_rcv, audio_sink - Add xmlrpc_server for runtime frequency control - Connect signal chain: source → LPF → demod → audio - Save and compile with grcc This proves gr-mcp's middleware approach works end-to-end: parameters set via set_params() serialize correctly after the lru_cache fix. No longer needs examples/fm_receiver.grc template file.
This commit is contained in:
parent
27f651307e
commit
2a5e73e9a7
@ -1,16 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""FM Band Scanner — scan 87.5–108.0 MHz using rtl_power, rank stations by signal strength.
|
"""FM Band Scanner — scan 87.5–108.0 MHz using rtl_power, rank stations by signal strength.
|
||||||
|
|
||||||
Tuning uses a GNU Radio flowgraph built from the included GRC template,
|
Tuning builds a GNU Radio flowgraph programmatically using the same GRC
|
||||||
compiled with grcc, and controlled at runtime via XML-RPC — the same
|
Platform API that gr-mcp uses, compiles it with grcc, and controls it at
|
||||||
protocol that gr-mcp uses for live parameter control.
|
runtime via XML-RPC for live frequency changes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -255,40 +254,116 @@ def pick_station(stations: list[dict]) -> float | None:
|
|||||||
|
|
||||||
|
|
||||||
XMLRPC_PORT = 8090
|
XMLRPC_PORT = 8090
|
||||||
GRC_TEMPLATE = Path(__file__).parent / "fm_receiver.grc"
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_flowgraph(freq_mhz: float, gain: int = 10) -> Path:
|
def build_fm_receiver(freq_mhz: float, gain: int = 10) -> Path:
|
||||||
"""Create a tuned FM receiver GRC file from the template.
|
"""Build an FM receiver flowgraph programmatically — no GRC template needed.
|
||||||
|
|
||||||
Patches the template with the requested frequency and gain, then
|
Creates all blocks, sets parameters, connects the signal chain, saves to
|
||||||
compiles it with grcc. Returns the path to the compiled .py file.
|
.grc, and compiles with grcc. This uses the same middleware that gr-mcp's
|
||||||
|
MCP tools use, proving end-to-end programmatic flowgraph construction.
|
||||||
|
|
||||||
|
Signal chain:
|
||||||
|
RTL-SDR (2.4 MHz) → LPF (decim 5) → WBFM Demod (decim 10) → Audio (48 kHz)
|
||||||
"""
|
"""
|
||||||
grc_text = GRC_TEMPLATE.read_text()
|
# Late import to avoid dependency when just scanning (no --tune)
|
||||||
|
try:
|
||||||
|
from gnuradio import gr
|
||||||
|
from gnuradio.grc.core.platform import Platform
|
||||||
|
except ImportError:
|
||||||
|
print("Error: GNU Radio not found. Install gnuradio.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Patch frequency variable (value line under the freq block)
|
# Initialize platform (same as gr-mcp main.py)
|
||||||
grc_text = re.sub(
|
platform = Platform(
|
||||||
r"(- name: freq\n id: variable\n parameters:\n comment: ''\n value: )[\d.eE+]+",
|
version=gr.version(),
|
||||||
rf"\g<1>{freq_mhz}e6",
|
version_parts=(gr.major_version(), gr.api_version(), gr.minor_version()),
|
||||||
grc_text,
|
prefs=gr.prefs(),
|
||||||
)
|
)
|
||||||
# Patch osmosdr RF gain (only the top-level gain0, not bb_gain0/if_gain0)
|
platform.build_library()
|
||||||
grc_text = re.sub(r"((?<![_a-z])gain0: ')(\d+)(')", rf"\g<1>{gain}\3", grc_text)
|
|
||||||
|
|
||||||
work_dir = Path(tempfile.mkdtemp(prefix="fm_scanner_"))
|
# Create flowgraph
|
||||||
|
fg = platform.make_flow_graph()
|
||||||
|
|
||||||
|
# Configure options block (flowgraph metadata)
|
||||||
|
options = next(b for b in fg.blocks if b.key == "options")
|
||||||
|
options.params["id"].set_value("fm_receiver")
|
||||||
|
options.params["title"].set_value("FM Receiver")
|
||||||
|
options.params["generate_options"].set_value("no_gui")
|
||||||
|
options.params["run_options"].set_value("run")
|
||||||
|
|
||||||
|
# Add samp_rate variable (not included by default, unlike GRC GUI)
|
||||||
|
samp_rate = fg.new_block("variable")
|
||||||
|
samp_rate.params["id"].set_value("samp_rate")
|
||||||
|
samp_rate.params["value"].set_value("int(2.4e6)")
|
||||||
|
|
||||||
|
# Add freq variable
|
||||||
|
freq_block = fg.new_block("variable")
|
||||||
|
freq_block.params["id"].set_value("freq")
|
||||||
|
freq_block.params["value"].set_value(f"{freq_mhz}e6")
|
||||||
|
|
||||||
|
# RTL-SDR source
|
||||||
|
source = fg.new_block("osmosdr_source")
|
||||||
|
source.params["id"].set_value("osmosdr_source_0")
|
||||||
|
source.params["sample_rate"].set_value("samp_rate")
|
||||||
|
source.params["freq0"].set_value("freq") # Reference the variable
|
||||||
|
source.params["gain0"].set_value(str(gain))
|
||||||
|
source.params["if_gain0"].set_value("20")
|
||||||
|
source.params["bb_gain0"].set_value("20")
|
||||||
|
source.params["args"].set_value('"rtl=0"')
|
||||||
|
|
||||||
|
# Low-pass filter: 2.4 MHz → 480 kHz (decim 5)
|
||||||
|
lpf = fg.new_block("low_pass_filter")
|
||||||
|
lpf.params["id"].set_value("low_pass_filter_0")
|
||||||
|
lpf.params["type"].set_value("fir_filter_ccf")
|
||||||
|
lpf.params["decim"].set_value("5")
|
||||||
|
lpf.params["gain"].set_value("1")
|
||||||
|
lpf.params["samp_rate"].set_value("samp_rate")
|
||||||
|
lpf.params["cutoff_freq"].set_value("100e3")
|
||||||
|
lpf.params["width"].set_value("10e3")
|
||||||
|
lpf.params["win"].set_value("window.WIN_HAMMING")
|
||||||
|
lpf.params["beta"].set_value("6.76")
|
||||||
|
|
||||||
|
# WBFM demodulator: 480 kHz → 48 kHz (decim 10)
|
||||||
|
wfm = fg.new_block("analog_wfm_rcv")
|
||||||
|
wfm.params["id"].set_value("analog_wfm_rcv_0")
|
||||||
|
wfm.params["quad_rate"].set_value("480e3")
|
||||||
|
wfm.params["audio_decimation"].set_value("10")
|
||||||
|
|
||||||
|
# Audio sink
|
||||||
|
audio = fg.new_block("audio_sink")
|
||||||
|
audio.params["id"].set_value("audio_sink_0")
|
||||||
|
audio.params["samp_rate"].set_value("48000")
|
||||||
|
audio.params["ok_to_block"].set_value("True")
|
||||||
|
|
||||||
|
# XML-RPC server for runtime control
|
||||||
|
xmlrpc = fg.new_block("xmlrpc_server")
|
||||||
|
xmlrpc.params["id"].set_value("xmlrpc_server_0")
|
||||||
|
xmlrpc.params["addr"].set_value("0.0.0.0")
|
||||||
|
xmlrpc.params["port"].set_value(str(XMLRPC_PORT))
|
||||||
|
|
||||||
|
# Connect signal chain
|
||||||
|
# source:0 → lpf:0
|
||||||
|
fg.connect(source.sources[0], lpf.sinks[0])
|
||||||
|
# lpf:0 → wfm:0
|
||||||
|
fg.connect(lpf.sources[0], wfm.sinks[0])
|
||||||
|
# wfm:0 → audio:0
|
||||||
|
fg.connect(wfm.sources[0], audio.sinks[0])
|
||||||
|
|
||||||
|
# Save and compile
|
||||||
|
work_dir = Path(tempfile.mkdtemp(prefix="fm_receiver_"))
|
||||||
grc_path = work_dir / "fm_receiver.grc"
|
grc_path = work_dir / "fm_receiver.grc"
|
||||||
grc_path.write_text(grc_text)
|
platform.save_flow_graph(str(grc_path), fg)
|
||||||
|
|
||||||
# Compile GRC → Python
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["grcc", "-o", str(work_dir), str(grc_path)],
|
["grcc", "-o", str(work_dir), str(grc_path)],
|
||||||
capture_output=True, text=True,
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print(f"Error: grcc compilation failed:\n{result.stderr}", file=sys.stderr)
|
print(f"Error: grcc compilation failed:\n{result.stderr}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# grcc uses the flowgraph's id as the filename (default.py for id: default)
|
|
||||||
py_files = list(work_dir.glob("*.py"))
|
py_files = list(work_dir.glob("*.py"))
|
||||||
if not py_files:
|
if not py_files:
|
||||||
print("Error: grcc produced no Python output.", file=sys.stderr)
|
print("Error: grcc produced no Python output.", file=sys.stderr)
|
||||||
@ -317,13 +392,13 @@ def wait_for_xmlrpc(url: str, timeout: float = 10.0) -> xmlrpc.client.ServerProx
|
|||||||
def tune_station(freq_mhz: float, gain: int = 10):
|
def tune_station(freq_mhz: float, gain: int = 10):
|
||||||
"""Launch a GNU Radio FM receiver and tune via XML-RPC.
|
"""Launch a GNU Radio FM receiver and tune via XML-RPC.
|
||||||
|
|
||||||
Builds a flowgraph from the GRC template, compiles it with grcc,
|
Builds a flowgraph programmatically using the GRC Platform API (the same
|
||||||
launches the Python flowgraph as a subprocess, and connects to its
|
approach gr-mcp uses), compiles it with grcc, launches the Python flowgraph
|
||||||
XML-RPC server for live frequency control — the same mechanism that
|
as a subprocess, and connects to its XML-RPC server for live frequency
|
||||||
gr-mcp uses for runtime parameter changes.
|
control.
|
||||||
"""
|
"""
|
||||||
print(f"\n Building FM receiver for {freq_mhz:.1f} MHz...")
|
print(f"\n Building FM receiver for {freq_mhz:.1f} MHz...")
|
||||||
py_path = prepare_flowgraph(freq_mhz, gain)
|
py_path = build_fm_receiver(freq_mhz, gain)
|
||||||
|
|
||||||
url = f"http://localhost:{XMLRPC_PORT}"
|
url = f"http://localhost:{XMLRPC_PORT}"
|
||||||
print(f" Launching flowgraph ({py_path.name})...")
|
print(f" Launching flowgraph ({py_path.name})...")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user