add Starlight docs site, LoRa examples, and clean up .gitignore

- Starlight docs: 28 pages covering getting started, guides, tool
  reference, concepts (architecture, dynamic tools, runtime comms)
- LoRa examples: channel scanner, quality analyzer, multi-SF receiver
  with both .grc and .py forms, plus ADSB+LoRa combo test
- .gitignore: exclude generated artifacts (*_patched_*.py, *.wav,
  docs build cache, tests/scratch/)
- Add .mcp.json for local MCP server config
- Sync uv.lock with date-based version
This commit is contained in:
Ryan Malloy 2026-02-24 09:34:50 -07:00
parent 41dcebbf6d
commit 8800d35fd4
42 changed files with 15280 additions and 1 deletions

10
.gitignore vendored
View File

@ -172,3 +172,13 @@ cython_debug/
# PyPI configuration file
.pypirc
# GR-MCP project-specific
examples/*_patched_*.py
examples/*.wav
tests/scratch/
# Astro/Starlight docs site
docs/.astro/
docs/node_modules/
docs/dist/

15
.mcp.json Normal file
View File

@ -0,0 +1,15 @@
{
"mcpServers": {
"gnuradio-mcp": {
"type": "stdio",
"command": "uv",
"args": [
"run",
"--directory",
"/home/rpm/claude/sdr/gr-mcp",
"gnuradio-mcp"
],
"env": {}
}
}
}

48
docs/astro.config.mjs Normal file
View File

@ -0,0 +1,48 @@
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
export default defineConfig({
site: 'https://gr-mcp.supported.systems',
integrations: [
starlight({
title: 'GR-MCP',
description: 'GNU Radio MCP Server for programmatic flowgraph control',
social: {
github: 'https://git.supported.systems/MCP/gr-mcp',
},
editLink: {
baseUrl: 'https://git.supported.systems/MCP/gr-mcp/src/branch/main/docs/',
},
customCss: ['./src/styles/custom.css'],
sidebar: [
{
label: 'Getting Started',
autogenerate: { directory: 'getting-started' },
},
{
label: 'Guides',
autogenerate: { directory: 'guides' },
},
{
label: 'Reference',
items: [
{ label: 'Tools Overview', link: '/reference/tools-overview/' },
{
label: 'Tool Reference',
collapsed: true,
autogenerate: { directory: 'reference/tools' },
},
{ label: 'Docker Images', link: '/reference/docker-images/' },
{ label: 'OOT Catalog', link: '/reference/oot-catalog/' },
],
},
{
label: 'Concepts',
autogenerate: { directory: 'concepts' },
},
],
}),
],
telemetry: false,
devToolbar: { enabled: false },
});

6964
docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
docs/package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "gr-mcp-docs",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"generate-api-docs": "python scripts/generate-api-docs.py"
},
"dependencies": {
"@astrojs/starlight": "^0.32.3",
"astro": "^5.2.5",
"sharp": "^0.33.5"
}
}

View File

@ -0,0 +1,355 @@
#!/usr/bin/env python3
"""Generate MDX documentation from GR-MCP tool docstrings.
Usage:
cd docs
python scripts/generate-api-docs.py
This script introspects the PlatformProvider and RuntimeProvider classes
to extract tool signatures and docstrings, then generates MDX files for
the Starlight documentation site.
"""
import inspect
import sys
from pathlib import Path
from typing import get_type_hints
# Add project root to path
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# Import providers
from gnuradio_mcp.providers.base import PlatformProvider
from gnuradio_mcp.providers.runtime import RuntimeProvider
OUTPUT_DIR = Path(__file__).parent.parent / "src/content/docs/reference/tools"
# Tool categorization
TOOL_CATEGORIES = {
"flowgraph": {
"title": "Flowgraph Tools",
"description": "Tools for managing flowgraph structure: blocks, connections, save/load.",
"tools": [
"get_blocks",
"make_block",
"remove_block",
"save_flowgraph",
"load_flowgraph",
"get_flowgraph_options",
"set_flowgraph_options",
"export_flowgraph_data",
"import_flowgraph_data",
],
},
"blocks": {
"title": "Block Tools",
"description": "Tools for block parameter and port management.",
"tools": [
"get_block_params",
"set_block_params",
"get_block_sources",
"get_block_sinks",
"bypass_block",
"unbypass_block",
],
},
"connections": {
"title": "Connection Tools",
"description": "Tools for wiring blocks together.",
"tools": [
"get_connections",
"connect_blocks",
"disconnect_blocks",
],
},
"validation": {
"title": "Validation Tools",
"description": "Tools for checking flowgraph validity.",
"tools": [
"validate_block",
"validate_flowgraph",
"get_all_errors",
],
},
"platform": {
"title": "Platform Tools",
"description": "Tools for discovering available blocks and managing OOT paths.",
"tools": [
"get_all_available_blocks",
"search_blocks",
"get_block_categories",
"load_oot_blocks",
"add_block_path",
"get_block_paths",
],
},
"codegen": {
"title": "Code Generation",
"description": "Tools for generating Python code and evaluating expressions.",
"tools": [
"generate_code",
"evaluate_expression",
"create_embedded_python_block",
],
},
"runtime-mode": {
"title": "Runtime Mode",
"description": "Tools for enabling/disabling runtime features and checking client capabilities.",
"tools": [
"get_runtime_mode",
"enable_runtime_mode",
"disable_runtime_mode",
"get_client_capabilities",
"list_client_roots",
],
},
"docker": {
"title": "Docker Tools",
"description": "Tools for container lifecycle management.",
"tools": [
"launch_flowgraph",
"list_containers",
"stop_flowgraph",
"remove_flowgraph",
"capture_screenshot",
"get_container_logs",
],
},
"xmlrpc": {
"title": "XML-RPC Tools",
"description": "Tools for XML-RPC connection and variable control.",
"tools": [
"connect",
"connect_to_container",
"disconnect",
"get_status",
"list_variables",
"get_variable",
"set_variable",
"start",
"stop",
"lock",
"unlock",
],
},
"controlport": {
"title": "ControlPort Tools",
"description": "Tools for ControlPort/Thrift connection and monitoring.",
"tools": [
"connect_controlport",
"connect_to_container_controlport",
"disconnect_controlport",
"get_knobs",
"set_knobs",
"get_knob_properties",
"get_performance_counters",
"post_message",
],
},
"coverage": {
"title": "Coverage Tools",
"description": "Tools for collecting Python code coverage from containers.",
"tools": [
"collect_coverage",
"generate_coverage_report",
"combine_coverage",
"delete_coverage",
],
},
"oot": {
"title": "OOT Tools",
"description": "Tools for OOT module detection and installation.",
"tools": [
"detect_oot_modules",
"install_oot_module",
"list_oot_images",
"remove_oot_image",
"build_multi_oot_image",
"list_combo_images",
"remove_combo_image",
],
},
}
def get_method_info(method):
"""Extract signature and docstring from a method."""
sig = inspect.signature(method)
doc = inspect.getdoc(method) or "No description available."
# Parse docstring sections
lines = doc.split("\n")
description = []
args = []
returns = ""
example = []
section = "description"
for line in lines:
stripped = line.strip()
if stripped.startswith("Args:"):
section = "args"
continue
elif stripped.startswith("Returns:"):
section = "returns"
continue
elif stripped.startswith("Example:") or stripped.startswith("Examples:"):
section = "example"
continue
if section == "description":
description.append(line)
elif section == "args":
args.append(line)
elif section == "returns":
returns += line + "\n"
elif section == "example":
example.append(line)
# Get parameter info from signature
params = []
for name, param in sig.parameters.items():
if name == "self":
continue
param_type = ""
if param.annotation != inspect.Parameter.empty:
param_type = str(param.annotation).replace("typing.", "")
default = ""
if param.default != inspect.Parameter.empty:
default = repr(param.default)
params.append({
"name": name,
"type": param_type,
"default": default,
})
# Get return type
return_type = ""
try:
hints = get_type_hints(method)
if "return" in hints:
return_type = str(hints["return"]).replace("typing.", "")
except Exception:
pass
return {
"name": method.__name__,
"description": "\n".join(description).strip(),
"params": params,
"returns": returns.strip(),
"return_type": return_type,
"args_doc": "\n".join(args).strip(),
"example": "\n".join(example).strip(),
}
def get_all_methods():
"""Get all tool methods from both providers."""
methods = {}
# Get PlatformProvider methods
for name, method in inspect.getmembers(PlatformProvider, predicate=inspect.isfunction):
if not name.startswith("_"):
methods[name] = get_method_info(method)
# Get RuntimeProvider methods
for name, method in inspect.getmembers(RuntimeProvider, predicate=inspect.isfunction):
if not name.startswith("_"):
methods[name] = get_method_info(method)
return methods
def generate_mdx(category_key: str, category: dict, methods: dict) -> str:
"""Generate MDX content for a category."""
lines = [
"---",
f'title: {category["title"]}',
f'description: {category["description"]}',
"---",
"",
f'{category["description"]}',
"",
]
for tool_name in category["tools"]:
if tool_name not in methods:
lines.append(f"## `{tool_name}`")
lines.append("")
lines.append("*Documentation pending.*")
lines.append("")
continue
info = methods[tool_name]
lines.append(f"## `{tool_name}`")
lines.append("")
lines.append(info["description"])
lines.append("")
# Parameters table
if info["params"]:
lines.append("### Parameters")
lines.append("")
lines.append("| Name | Type | Default | Description |")
lines.append("|------|------|---------|-------------|")
for param in info["params"]:
default = param["default"] if param["default"] else "-"
ptype = param["type"] if param["type"] else "-"
# Extract description from args_doc if available
desc = "-"
if info["args_doc"]:
for arg_line in info["args_doc"].split("\n"):
if arg_line.strip().startswith(f"{param['name']}:"):
desc = arg_line.split(":", 1)[1].strip()
break
lines.append(f"| `{param['name']}` | `{ptype}` | `{default}` | {desc} |")
lines.append("")
# Returns
if info["returns"] or info["return_type"]:
lines.append("### Returns")
lines.append("")
if info["return_type"]:
lines.append(f"**Type:** `{info['return_type']}`")
lines.append("")
if info["returns"]:
lines.append(info["returns"])
lines.append("")
# Example
if info["example"]:
lines.append("### Example")
lines.append("")
lines.append("```python")
lines.append(info["example"])
lines.append("```")
lines.append("")
lines.append("---")
lines.append("")
return "\n".join(lines)
def main():
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
print("Extracting methods from providers...")
methods = get_all_methods()
print(f"Found {len(methods)} methods")
for category_key, category in TOOL_CATEGORIES.items():
print(f"Generating {category_key}.mdx...")
content = generate_mdx(category_key, category, methods)
output_path = OUTPUT_DIR / f"{category_key}.mdx"
output_path.write_text(content)
print(f" Wrote {output_path}")
print("\nDone! Generated MDX files in:")
print(f" {OUTPUT_DIR}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,10 @@
import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({
loader: docsLoader(),
schema: docsSchema(),
}),
};

View File

@ -0,0 +1,237 @@
---
title: Architecture
description: GR-MCP system architecture and design principles
draft: false
---
import { Aside } from '@astrojs/starlight/components';
GR-MCP follows a **Middleware + Provider** pattern that abstracts GNU Radio's internal
objects into clean, serializable models suitable for the MCP protocol.
## High-Level Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ MCP Client │
│ (Claude, Cursor, etc.) │
└─────────────────────────────────────────────────────────────────┘
│ MCP Protocol (stdio/SSE)
┌─────────────────────────────────────────────────────────────────┐
│ FastMCP App │
│ (main.py) │
└─────────────────────────────────────────────────────────────────┘
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ McpPlatformProvider │ │ McpRuntimeProvider │
│ (29 tools) │ │ (5 always + ~40 dynamic) │
│ │ │ │
│ • get_blocks │ │ Always: │
│ • make_block │ │ • get_runtime_mode │
│ • connect_blocks │ │ • enable_runtime_mode │
│ • validate_flowgraph │ │ • disable_runtime_mode │
│ • generate_code │ │ • get_client_capabilities │
│ • ... │ │ • list_client_roots │
│ │ │ │
│ │ │ Dynamic (when enabled): │
│ │ │ • launch_flowgraph │
│ │ │ • connect_to_container │
│ │ │ • set_variable │
│ │ │ • ... │
└─────────────────────────┘ └─────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ PlatformProvider │ │ RuntimeProvider │
│ (Business Logic) │ │ (Business Logic) │
└─────────────────────────┘ └─────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ Middlewares │ │ Middlewares │
│ │ │ │
│ • PlatformMiddleware │ │ • DockerMiddleware │
│ • FlowGraphMiddleware │ │ • XmlRpcMiddleware │
│ • BlockMiddleware │ │ • ThriftMiddleware │
│ │ │ • OOTInstallerMiddleware │
└─────────────────────────┘ └─────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ GNU Radio Objects │ │ External Services │
│ │ │ │
│ • Platform │ │ • Docker daemon │
│ • FlowGraph │ │ • XML-RPC servers │
│ • Block │ │ • Thrift servers │
│ • Connections │ │ • Container processes │
└─────────────────────────┘ └─────────────────────────────────┘
```
## Layer Responsibilities
### MCP Layer (FastMCP)
- Handles MCP protocol communication
- Registers tools and resources
- Manages the server lifecycle
### Provider Layer
Business logic that doesn't know about MCP:
- **PlatformProvider**: All flowgraph operations (create, edit, validate, save)
- **RuntimeProvider**: All runtime operations (launch, connect, control)
### Middleware Layer
Wraps external systems with validation and normalization:
- **PlatformMiddleware**: Wraps GNU Radio's `Platform` class
- **FlowGraphMiddleware**: Wraps `FlowGraph` with block/connection management
- **BlockMiddleware**: Wraps `Block` with parameter/port access
- **DockerMiddleware**: Wraps Docker SDK operations
- **XmlRpcMiddleware**: Wraps XML-RPC client
- **ThriftMiddleware**: Wraps ControlPort Thrift client
- **OOTInstallerMiddleware**: Manages OOT module builds
### Models Layer
Pydantic models for serialization:
```python
# Examples
class BlockModel(BaseModel):
name: str
key: str
state: str
class ParamModel(BaseModel):
key: str
value: str
name: str
dtype: str
class PortModel(BaseModel):
parent: str
key: str
name: str
dtype: str
direction: str
```
## Data Flow
### Flowgraph Operations
```
Tool Call: make_block(block_type="osmosdr_source")
McpPlatformProvider.make_block()
PlatformProvider.make_block(block_name="osmosdr_source")
FlowGraphMiddleware.add_block("osmosdr_source")
│ Creates GNU Radio Block object
│ Generates unique name
BlockMiddleware(block)
Return: "osmosdr_source_0" (str)
```
### Runtime Operations
```
Tool Call: set_variable(name="freq", value=101.1e6)
McpRuntimeProvider._runtime_tools["set_variable"]
RuntimeProvider.set_variable(name="freq", value=101.1e6)
XmlRpcMiddleware.set_variable("freq", 101.1e6)
│ XML-RPC call to running flowgraph
Return: True
```
## Dynamic Tool Registration
Runtime tools are registered dynamically to minimize context usage:
```python
# At startup: only 5 mode control tools
get_runtime_mode() # Check status
enable_runtime_mode() # Register all runtime tools
disable_runtime_mode() # Unregister runtime tools
get_client_capabilities() # Debug client info
list_client_roots() # Debug client roots
# After enable_runtime_mode(): ~40 additional tools
launch_flowgraph()
connect_to_container()
set_variable()
# ...etc
```
<Aside type="tip">
This design reduces the tool list from ~70 to ~35 when only doing flowgraph design,
saving context tokens in LLM applications.
</Aside>
## Key Design Decisions
### Why Middlewares?
GNU Radio objects have complex internal state and non-serializable references.
Middlewares:
1. Provide a clean interface with Pydantic models
2. Handle validation and error formatting
3. Manage resource lifecycle (connections, processes)
### Why Providers?
Providers separate business logic from MCP registration:
1. Testable without MCP infrastructure
2. Reusable in non-MCP contexts
3. Clear dependency injection
### Why Dynamic Registration?
Many MCP clients include all tool descriptions in the system prompt.
With 70+ tools, this wastes significant context. Dynamic registration:
1. Starts with minimal tools for flowgraph design
2. Expands when runtime control is needed
3. Can contract back when done
## File Organization
```
src/gnuradio_mcp/
├── models.py # All Pydantic models
├── utils.py # Helper functions
├── oot_catalog.py # OOT module catalog
├── middlewares/
│ ├── platform.py # PlatformMiddleware
│ ├── flowgraph.py # FlowGraphMiddleware
│ ├── block.py # BlockMiddleware
│ ├── docker.py # DockerMiddleware
│ ├── xmlrpc.py # XmlRpcMiddleware
│ ├── thrift.py # ThriftMiddleware
│ └── oot.py # OOTInstallerMiddleware
└── providers/
├── base.py # PlatformProvider
├── mcp.py # McpPlatformProvider
├── runtime.py # RuntimeProvider
└── mcp_runtime.py # McpRuntimeProvider
```

View File

@ -0,0 +1,196 @@
---
title: Dynamic Tools
description: How GR-MCP dynamically registers runtime tools
draft: false
---
import { Aside, Steps } from '@astrojs/starlight/components';
GR-MCP uses **dynamic tool registration** to minimize context usage. Runtime tools
are only registered when needed, keeping the tool list small during flowgraph design.
## The Problem
Many MCP clients include tool descriptions in the system prompt. With 70+ tools,
this can consume significant context:
```
70 tools × ~100 tokens/tool = ~7,000 tokens
```
For LLM applications, this is wasteful when you only need flowgraph design tools.
## The Solution
GR-MCP splits tools into two groups:
### Always Available (34 tools)
Platform tools for flowgraph design:
- `get_blocks`, `make_block`, `remove_block`
- `connect_blocks`, `disconnect_blocks`
- `validate_flowgraph`, `generate_code`
- etc.
Plus 5 runtime mode control tools:
- `get_runtime_mode`
- `enable_runtime_mode`
- `disable_runtime_mode`
- `get_client_capabilities`
- `list_client_roots`
### Dynamically Registered (~40 tools)
Runtime tools loaded via `enable_runtime_mode()`:
- Container lifecycle: `launch_flowgraph`, `stop_flowgraph`
- XML-RPC: `connect`, `set_variable`, `get_variable`
- ControlPort: `connect_controlport`, `get_knobs`, `get_performance_counters`
- Coverage: `collect_coverage`, `generate_coverage_report`
- OOT: `install_oot_module`, `detect_oot_modules`
## Usage Pattern
<Steps>
1. **Flowgraph Design** (29 platform tools + 5 mode tools = 34 total)
```python
make_block(block_type="osmosdr_source")
connect_blocks(...)
validate_flowgraph()
generate_code(output_dir="/tmp")
```
2. **Enable Runtime** (adds ~40 tools = ~74 total)
```python
enable_runtime_mode()
# Now runtime tools are available
```
3. **Runtime Control**
```python
launch_flowgraph(flowgraph_path="/tmp/fm.py")
connect_to_container(name="gr-fm")
set_variable(name="freq", value=101.1e6)
capture_screenshot()
```
4. **Disable Runtime** (back to 34 tools)
```python
disable_runtime_mode()
# Runtime tools removed, back to design mode
```
</Steps>
## Implementation
### Mode Control Tools
```python
@self._mcp.tool
def enable_runtime_mode() -> RuntimeModeStatus:
"""Enable runtime mode, registering all runtime control tools."""
if self._runtime_enabled:
return RuntimeModeStatus(enabled=True, ...)
self._register_runtime_tools()
self._runtime_enabled = True
return RuntimeModeStatus(enabled=True, tools_registered=[...])
```
### Dynamic Registration
```python
def _register_runtime_tools(self):
"""Dynamically register all runtime tools."""
p = self._provider
# Connection management
self._add_tool("connect", p.connect)
self._add_tool("disconnect", p.disconnect)
self._add_tool("get_status", p.get_status)
# Variable control
self._add_tool("list_variables", p.list_variables)
self._add_tool("get_variable", p.get_variable)
self._add_tool("set_variable", p.set_variable)
# ... more tools ...
# Docker-dependent tools (only if Docker available)
if p._has_docker:
self._add_tool("launch_flowgraph", p.launch_flowgraph)
# ...
def _add_tool(self, name: str, func: Callable):
"""Add a tool and track it for later removal."""
self._mcp.add_tool(func)
self._runtime_tools[name] = func
```
### Dynamic Unregistration
```python
def _unregister_runtime_tools(self):
"""Remove all dynamically registered runtime tools."""
for name in list(self._runtime_tools.keys()):
self._mcp.remove_tool(name)
self._runtime_tools.clear()
```
## Conditional Registration
Some tools depend on external services:
```python
# Docker-dependent tools
if p._has_docker:
self._add_tool("launch_flowgraph", p.launch_flowgraph)
self._add_tool("capture_screenshot", p.capture_screenshot)
# OOT tools require Docker for building
if p._has_oot:
self._add_tool("install_oot_module", p.install_oot_module)
```
<Aside>
If Docker is unavailable, container-related tools are simply not registered.
The `get_runtime_mode()` tool reports which capabilities are available.
</Aside>
## Checking Status
```python
status = get_runtime_mode()
# Returns: RuntimeModeStatus(
# enabled=False,
# tools_registered=[],
# docker_available=True,
# oot_available=True
# )
enable_runtime_mode()
status = get_runtime_mode()
# Returns: RuntimeModeStatus(
# enabled=True,
# tools_registered=[
# "connect", "disconnect", "get_status",
# "list_variables", "get_variable", "set_variable",
# "start", "stop", "lock", "unlock",
# "launch_flowgraph", "list_containers",
# ...
# ],
# docker_available=True,
# oot_available=True
# )
```
## Benefits
1. **Reduced Context** — ~35 tools instead of ~75 during design
2. **Faster Responses** — Less prompt processing for simple queries
3. **Clear Separation** — Design vs runtime is explicit
4. **Graceful Degradation** — Missing Docker doesn't break design tools
5. **Discovery** — `get_runtime_mode()` shows what's available

View File

@ -0,0 +1,265 @@
---
title: Runtime Communication
description: How GNU Radio flowgraphs communicate at runtime
draft: false
---
import { Aside } from '@astrojs/starlight/components';
This document explains how GNU Radio Companion (GRC) communicates with running flowgraph
processes and the two mechanisms available for runtime control.
## Key Insight: GRC is a Code Generator
GRC runs flowgraphs as **completely separate subprocesses** via `subprocess.Popen()`.
It does not have built-in runtime control capabilities.
```
┌────────────────────┐ subprocess.Popen() ┌─────────────────────┐
│ GNU Radio │ ────────────────────────► │ Generated Python │
│ Companion (GRC) │ │ Flowgraph Script │
│ │ ◄──────────────────────── │ │
│ (Qt/GTK GUI) │ stdout/stderr pipe │ (gr.top_block) │
└────────────────────┘ └─────────────────────┘
```
The generated Python script runs independently. To control parameters at runtime,
you must use one of the two communication mechanisms described below.
## GRC Execution Flow
```
.grc file (YAML)
▼ Platform.load_and_generate_flow_graph()
Generator (Mako templates)
▼ generator.write()
Python script (with set_*/get_* methods)
▼ ExecFlowGraphThread -> subprocess.Popen()
Running flowgraph process
▼ stdout/stderr piped back to GRC console
```
### Key GRC Execution Files
| File | Purpose |
|------|---------|
| `grc/main.py` | Entry point |
| `grc/gui_qt/components/executor.py` | ExecFlowGraphThread subprocess launcher |
| `grc/core/platform.py` | Block registry, flowgraph loading |
| `grc/core/generator/Generator.py` | Generator factory |
| `grc/workflows/common.py` | Base generator classes |
| `grc/workflows/python_nogui/flow_graph_nogui.py.mako` | Mako template for Python |
## Two Runtime Control Mechanisms
### 1. XML-RPC Server (Simple, HTTP-based)
A **block-based approach** — add the `xmlrpc_server` block to your flowgraph to
expose GRC variables over HTTP.
| Aspect | Details |
|--------|---------|
| Protocol | HTTP (XML-RPC) |
| Default Port | 8080 |
| Setup | Add `XMLRPC Server` block to flowgraph |
| Naming | `set_varname()` / `get_varname()` |
| Type Support | Basic Python types |
#### How It Works
1. Add `XMLRPC Server` block to flowgraph
2. GRC variables automatically become `set_X()` / `get_X()` methods
3. Connect with any XML-RPC client (Python, C++, curl, etc.)
#### Client Example
```python
import xmlrpc.client
# Connect to running flowgraph
server = xmlrpc.client.ServerProxy('http://localhost:8080')
# Read and write variables
print(server.get_freq()) # Read a variable
server.set_freq(145.5e6) # Set a variable
# Flowgraph control
server.stop() # Stop flowgraph
server.start() # Start flowgraph
server.lock() # Lock flowgraph for modifications
server.unlock() # Unlock flowgraph
```
#### Key Files
| File | Purpose |
|------|---------|
| `gr-blocks/grc/xmlrpc_server.block.yml` | Server block definition |
| `gr-blocks/grc/xmlrpc_client.block.yml` | Client block definition |
| `gr-blocks/examples/xmlrpc/` | Example flowgraphs |
### 2. ControlPort/Thrift (Advanced, Binary)
A **configuration-based approach** — blocks register their parameters via `setup_rpc()`
in C++ code.
| Aspect | Details |
|--------|---------|
| Protocol | Thrift Binary TCP |
| Default Port | 9090 |
| Setup | Enable in config, blocks call `setup_rpc()` |
| Naming | `block_alias::varname` |
| Type Support | Rich (complex, vectors, PMT types) |
| Metadata | Units, min/max, display hints |
#### Architecture
```
┌──────────────────────────────────────────────────────────────────┐
│ Running Flowgraph Process │
├──────────────────────────────────────────────────────────────────┤
│ Block A Block B │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ setup_rpc() { │ │ setup_rpc() { │ │
│ │ add_rpc_var( │ │ add_rpc_var( │ │
│ │ "gain", │ │ "freq", │ │
│ │ &get_gain, │ │ &get_freq, │ │
│ │ &set_gain); │ │ &set_freq); │ │
│ │ } │ │ } │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ rpcserver_thrift (port 9090) ││
│ │ ┌─────────────────┐ ┌─────────────────┐ ││
│ │ │ setcallbackmap │ │ getcallbackmap │ ││
│ │ │ "blockA::gain" │ │ "blockA::gain" │ ││
│ │ │ "blockB::freq" │ │ "blockB::freq" │ ││
│ │ └─────────────────┘ └─────────────────┘ ││
│ └──────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────┘
│ Thrift Binary Protocol (TCP)
┌──────────────────────────────────────────────────────────────────┐
│ Python Client │
│ from gnuradio.ctrlport import GNURadioControlPortClient │
│ │
│ client = GNURadioControlPortClient(host='localhost', port=9090) │
│ knobs = client.getKnobs(['blockA::gain', 'blockB::freq']) │
│ client.setKnobs({'blockA::gain': 2.5}) │
└──────────────────────────────────────────────────────────────────┘
```
#### Enabling ControlPort
**~/.gnuradio/config.conf:**
```ini
[ControlPort]
on = True
edges_list = True
[thrift]
port = 9090
nthreads = 2
```
#### Client Example
```python
from gnuradio.ctrlport.GNURadioControlPortClient import GNURadioControlPortClient
# Connect to running flowgraph
client = GNURadioControlPortClient(host='localhost', port=9090)
# Get knobs (read values)
knobs = client.getKnobs(['analog_sig_source_0::frequency'])
print(knobs)
# Set knobs (write values)
client.setKnobs({'analog_sig_source_0::frequency': 1500.0})
# Regex-based retrieval - get all frequency knobs
all_freq_knobs = client.getRe(['.*::frequency'])
# Get metadata (units, min, max, description)
props = client.properties(['analog_sig_source_0::frequency'])
print(props['analog_sig_source_0::frequency'].units)
print(props['analog_sig_source_0::frequency'].min)
```
#### GUI Monitoring Tools
- **gr-ctrlport-monitor** — Real-time variable inspection
- **gr-perf-monitorx** — Performance profiling visualization
```bash
gr-ctrlport-monitor localhost 9090
gr-perf-monitorx localhost 9090
```
#### Key Files
| File | Purpose |
|------|---------|
| `gnuradio-runtime/lib/controlport/thrift/gnuradio.thrift` | Thrift IDL definition |
| `gnuradio-runtime/include/gnuradio/rpcserver_thrift.h` | Server implementation |
| `gnuradio-runtime/include/gnuradio/rpcregisterhelpers.h` | Registration templates |
| `gnuradio-runtime/python/gnuradio/ctrlport/GNURadioControlPortClient.py` | Python client |
| `gnuradio-runtime/python/gnuradio/ctrlport/RPCConnectionThrift.py` | Thrift connection |
## Comparison: XML-RPC vs ControlPort
| Feature | XML-RPC | ControlPort/Thrift |
|---------|---------|-------------------|
| Setup | Add block to flowgraph | Enable in config.conf |
| Protocol | HTTP | Binary TCP |
| Performance | Slower (text-based) | Faster (binary) |
| Type support | Basic Python types | Complex, vectors, PMT |
| Metadata | None | Units, min/max, hints |
| Tooling | Any HTTP client | Specialized monitors |
| Use case | Simple control | Performance monitoring |
### When to Use Each
**Use XML-RPC when:**
- You need quick, simple remote control
- Integration with web applications
- Language-agnostic client access
- Minimal configuration
**Use ControlPort when:**
- You need performance monitoring
- Working with complex data types
- Block-level control granularity
- Need metadata about parameters
## GR-MCP Integration
GR-MCP wraps both protocols:
```python
# XML-RPC via GR-MCP
connect_to_container(name="fm-radio")
set_variable(name="freq", value=101.1e6)
# ControlPort via GR-MCP
connect_to_container_controlport(name="fm-profiled")
get_performance_counters()
```
<Aside type="tip">
Both connections can be active simultaneously. Use XML-RPC for simple variable
control and ControlPort for performance monitoring.
</Aside>
## Related Documentation
- [Runtime Control Guide](/guides/runtime-control/) — XML-RPC usage in GR-MCP
- [ControlPort Monitoring Guide](/guides/controlport/) — ControlPort usage in GR-MCP
- GNU Radio docs: `docs/doxygen/other/ctrlport.dox` — Block implementation guide

View File

@ -0,0 +1,240 @@
---
title: Your First Flowgraph
description: Build and validate a complete GNU Radio flowgraph with GR-MCP
draft: false
---
import { Steps, Aside, Code, Tabs, TabItem } from '@astrojs/starlight/components';
This tutorial walks through building a simple FM receiver flowgraph programmatically.
You'll learn the core workflow: create blocks, set parameters, connect ports, validate, and save.
## What We're Building
A basic FM receiver chain:
```
osmosdr_source → low_pass_filter → analog_wfm_rcv → audio_sink
```
This receives RF at a configurable frequency, filters it, demodulates FM, and outputs audio.
## Step-by-Step
<Steps>
1. **Create the source block**
```python
make_block(block_type="osmosdr_source")
# Returns: "osmosdr_source_0"
```
GR-MCP automatically assigns unique names by appending `_0`, `_1`, etc.
2. **Set source parameters**
First, inspect available parameters:
```python
get_block_params(block_name="osmosdr_source_0")
```
Then configure:
```python
set_block_params(block_name="osmosdr_source_0", params={
"freq": "101.1e6", # 101.1 MHz FM station
"sample_rate": "2e6", # 2 MHz sample rate
"gain": "40", # RF gain
"args": '""' # Auto-detect device
})
```
3. **Create the filter**
```python
make_block(block_type="low_pass_filter")
set_block_params(block_name="low_pass_filter_0", params={
"type": "fir_filter_ccf",
"decim": "10", # Decimate by 10 → 200 kHz
"cutoff_freq": "100e3", # 100 kHz cutoff
"transition_width": "10e3",
"win": "window.WIN_HAMMING"
})
```
4. **Create the FM demodulator**
```python
make_block(block_type="analog_wfm_rcv")
set_block_params(block_name="analog_wfm_rcv_0", params={
"quad_rate": "200e3", # Input rate after decimation
"audio_decimation": "4" # Output at 50 kHz
})
```
5. **Create the audio output**
```python
make_block(block_type="audio_sink")
set_block_params(block_name="audio_sink_0", params={
"samp_rate": "48000",
"device_name": '""' # Default audio device
})
```
6. **Connect the blocks**
```python
connect_blocks(
source_block_name="osmosdr_source_0",
sink_block_name="low_pass_filter_0",
source_port_name="0",
sink_port_name="0"
)
connect_blocks(
source_block_name="low_pass_filter_0",
sink_block_name="analog_wfm_rcv_0",
source_port_name="0",
sink_port_name="0"
)
connect_blocks(
source_block_name="analog_wfm_rcv_0",
sink_block_name="audio_sink_0",
source_port_name="0",
sink_port_name="0"
)
```
<Aside type="tip">
Use `get_block_sources()` and `get_block_sinks()` to discover available ports.
Most signal processing blocks use port `"0"` for their primary input/output.
</Aside>
7. **Validate the flowgraph**
```python
validate_flowgraph()
# Returns: True
# Check for any warnings
get_all_errors()
# Returns: []
```
8. **Save the flowgraph**
```python
save_flowgraph(filepath="/tmp/fm_receiver.grc")
```
You can now open this in GNU Radio Companion!
</Steps>
## Generate Python Code
Instead of saving as `.grc`, you can generate executable Python directly:
```python
result = generate_code(output_dir="/tmp")
# Returns GeneratedCodeModel with:
# - file_path: "/tmp/fm_receiver.py"
# - is_valid: True
# - warnings: []
```
<Aside>
`generate_code()` does **not** block on validation errors — it returns warnings in the
response so you can decide whether to proceed. This differs from the `grcc` command-line tool.
</Aside>
## Using Variables
GR-MCP supports flowgraph variables for runtime tuning. Set them via flowgraph options:
```python
set_flowgraph_options(params={
"title": "FM Receiver",
"author": "Your Name",
# Variables are defined here too
})
# Or use the expression evaluator to test values
evaluate_expression("101.1e6 + 200e3") # Returns: 101300000.0
```
## Complete Script
Here's the full example as a Python script:
```python
#!/usr/bin/env python3
"""Build an FM receiver with GR-MCP."""
import asyncio
from fastmcp import Client
async def build_fm_receiver():
# Connect to GR-MCP (running as MCP server)
async with Client("gr-mcp") as client:
# Create blocks
await client.call_tool("make_block", {"block_type": "osmosdr_source"})
await client.call_tool("make_block", {"block_type": "low_pass_filter"})
await client.call_tool("make_block", {"block_type": "analog_wfm_rcv"})
await client.call_tool("make_block", {"block_type": "audio_sink"})
# Configure source
await client.call_tool("set_block_params", {
"block_name": "osmosdr_source_0",
"params": {
"freq": "101.1e6",
"sample_rate": "2e6",
"gain": "40"
}
})
# Configure filter
await client.call_tool("set_block_params", {
"block_name": "low_pass_filter_0",
"params": {
"type": "fir_filter_ccf",
"decim": "10",
"cutoff_freq": "100e3",
"transition_width": "10e3"
}
})
# Connect signal chain
for src, dst in [
("osmosdr_source_0", "low_pass_filter_0"),
("low_pass_filter_0", "analog_wfm_rcv_0"),
("analog_wfm_rcv_0", "audio_sink_0"),
]:
await client.call_tool("connect_blocks", {
"source_block_name": src,
"sink_block_name": dst,
"source_port_name": "0",
"sink_port_name": "0"
})
# Validate and save
result = await client.call_tool("validate_flowgraph", {})
print(f"Valid: {result.data}")
await client.call_tool("save_flowgraph", {
"filepath": "/tmp/fm_receiver.grc"
})
if __name__ == "__main__":
asyncio.run(build_fm_receiver())
```
## Next Steps
- [Running in Docker](/getting-started/running-in-docker/) — Launch the flowgraph with runtime control
- [OOT Modules](/guides/oot-modules/) — Add gr-osmosdr and other modules

View File

@ -0,0 +1,173 @@
---
title: Installation
description: Set up GR-MCP with UV and GNU Radio
draft: false
---
import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components';
GR-MCP requires Python 3.14+, GNU Radio with GRC, and optionally Docker for runtime control features.
## Prerequisites
<Tabs>
<TabItem label="Arch Linux">
```bash
# GNU Radio and GRC
sudo pacman -S gnuradio gnuradio-companion
# UV package manager (if not installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Docker (optional, for runtime control)
sudo pacman -S docker docker-compose
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
```
</TabItem>
<TabItem label="Debian/Ubuntu">
```bash
# GNU Radio and GRC
sudo apt install gnuradio gnuradio-dev
# UV package manager
curl -LsSf https://astral.sh/uv/install.sh | sh
# Docker (optional)
sudo apt install docker.io docker-compose
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
```
</TabItem>
<TabItem label="Fedora">
```bash
# GNU Radio and GRC
sudo dnf install gnuradio gnuradio-devel
# UV package manager
curl -LsSf https://astral.sh/uv/install.sh | sh
# Docker (optional)
sudo dnf install docker docker-compose
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
```
</TabItem>
</Tabs>
## Install GR-MCP
<Steps>
1. **Clone the repository**
```bash
git clone https://git.supported.systems/MCP/gr-mcp
cd gr-mcp
```
2. **Create a virtual environment with system site-packages**
<Aside type="caution">
The `--system-site-packages` flag is **required** because GNU Radio's Python bindings
are installed system-wide and must be accessible within the virtual environment.
</Aside>
```bash
uv venv --system-site-packages --python 3.14
```
3. **Install dependencies**
```bash
uv sync
```
4. **Verify the installation**
```bash
uv run main.py
```
You should see the FastMCP server start. Press `Ctrl+C` to stop.
</Steps>
## Configure Your MCP Client
Add GR-MCP to your MCP client configuration:
<Tabs>
<TabItem label="Claude Desktop">
Edit `~/.config/claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"gr-mcp": {
"command": "uv",
"args": ["--directory", "/path/to/gr-mcp", "run", "main.py"]
}
}
}
```
</TabItem>
<TabItem label="Claude Code">
```bash
claude mcp add gr-mcp -- uv --directory /path/to/gr-mcp run main.py
```
Or add to your project's `.mcp.json`:
```json
{
"mcpServers": {
"gr-mcp": {
"command": "uv",
"args": ["--directory", "/path/to/gr-mcp", "run", "main.py"]
}
}
}
```
</TabItem>
<TabItem label="Cursor">
Edit your Cursor settings or `.cursor/mcp.json`:
```json
{
"mcpServers": {
"gr-mcp": {
"command": "uv",
"args": ["--directory", "/path/to/gr-mcp", "run", "main.py"]
}
}
}
```
</TabItem>
</Tabs>
## Build Docker Images (Optional)
For runtime control features (launching flowgraphs in containers, visual feedback, code coverage):
```bash
cd gr-mcp
# Base runtime image with Xvfb + VNC + ImageMagick
docker build -f docker/Dockerfile.gnuradio-runtime \
-t gnuradio-runtime:latest docker/
# Coverage image (adds python3-coverage)
docker build -f docker/Dockerfile.gnuradio-coverage \
-t gnuradio-coverage:latest docker/
```
<Aside>
OOT module images are built automatically when you use `install_oot_module()` or
`launch_flowgraph(..., auto_image=True)`.
</Aside>
## Next Steps
Now that GR-MCP is installed, try:
- [Build your first flowgraph](/getting-started/first-flowgraph/) — Create and validate a complete signal chain
- [Running in Docker](/getting-started/running-in-docker/) — Launch flowgraphs with runtime control

View File

@ -0,0 +1,270 @@
---
title: Running in Docker
description: Launch flowgraphs in Docker containers with runtime control
draft: false
---
import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
GR-MCP can run flowgraphs in Docker containers, providing isolation, headless GUI rendering
(via Xvfb), and real-time control through XML-RPC or ControlPort.
## Prerequisites
- Docker daemon running (`sudo systemctl start docker`)
- User in the `docker` group (`sudo usermod -aG docker $USER`)
- Base runtime image built (see [Installation](/getting-started/installation/))
## Enable Runtime Mode
Runtime tools are not loaded by default to minimize context usage. Enable them first:
```python
enable_runtime_mode()
# Returns RuntimeModeStatus with:
# enabled: True
# tools_registered: ["launch_flowgraph", "connect_to_container", ...]
# docker_available: True
# oot_available: True
```
<Aside>
Once enabled, you'll have access to ~40 additional tools for container lifecycle,
XML-RPC control, ControlPort monitoring, and code coverage collection.
</Aside>
## Launch a Flowgraph
<Steps>
1. **Generate the Python script**
Docker containers run `.py` files, not `.grc` files. Generate code first:
```python
generate_code(output_dir="/tmp")
# Creates /tmp/fm_receiver.py
```
2. **Launch in a container**
```python
launch_flowgraph(
flowgraph_path="/tmp/fm_receiver.py",
name="fm-radio",
xmlrpc_port=8080, # Enable XML-RPC on this port
enable_vnc=True # Optional: VNC server on port 5900
)
```
Returns a `ContainerModel` with:
- `name`: Container name
- `status`: "running"
- `xmlrpc_port`: Mapped port number
- `vnc_port`: VNC port (if enabled)
3. **Connect to the running flowgraph**
```python
connect_to_container(name="fm-radio")
```
This auto-discovers the XML-RPC port from container labels.
</Steps>
## Real-Time Variable Control
Once connected, you can read and modify flowgraph variables:
```python
# List available variables
list_variables()
# Returns: [VariableModel(name="freq", value=101100000.0), ...]
# Read a variable
get_variable(name="freq")
# Returns: 101100000.0
# Tune to a different station
set_variable(name="freq", value=98.5e6)
```
### Thread-Safe Updates
For multiple parameter changes, lock the flowgraph first:
```python
lock() # Pause processing
set_variable(name="freq", value=102.7e6)
set_variable(name="gain", value=35)
unlock() # Resume processing
```
## Visual Feedback
### Screenshots
Capture the QT GUI display:
```python
screenshot = capture_screenshot(name="fm-radio")
# Returns ScreenshotModel with:
# path: "/tmp/gr-mcp-screenshots/fm-radio-2024-01-15-14-30-00.png"
# width: 1024
# height: 768
```
### VNC Access
If launched with `enable_vnc=True`, connect with any VNC client:
```bash
# Using TigerVNC
vncviewer localhost:5900
# Using Remmina
remmina -c vnc://localhost:5900
```
### Container Logs
Check for errors or debug output:
```python
get_container_logs(name="fm-radio", tail=50)
# Returns last 50 lines of stdout/stderr
```
## Container Lifecycle
```python
# List all GR-MCP containers
list_containers()
# Stop a running flowgraph (graceful shutdown)
stop_flowgraph(name="fm-radio")
# Remove the container
remove_flowgraph(name="fm-radio")
# Force remove if stuck
remove_flowgraph(name="fm-radio", force=True)
```
## Flowgraph Execution Control
Control the flowgraph's execution state via XML-RPC:
```python
stop() # Pause execution (flowgraph.stop())
start() # Resume execution (flowgraph.start())
```
## Using SDR Hardware
Pass device paths to access hardware inside the container:
```python
launch_flowgraph(
flowgraph_path="/tmp/fm_receiver.py",
name="fm-hardware",
device_paths=["/dev/bus/usb"] # RTL-SDR access
)
```
<Tabs>
<TabItem label="RTL-SDR">
```python
device_paths=["/dev/bus/usb"]
```
</TabItem>
<TabItem label="HackRF">
```python
device_paths=["/dev/bus/usb"]
```
</TabItem>
<TabItem label="LimeSDR">
```python
device_paths=["/dev/bus/usb"]
```
</TabItem>
<TabItem label="USRP (Serial)">
```python
device_paths=["/dev/ttyUSB0"]
```
</TabItem>
</Tabs>
## Auto-Image Selection
For flowgraphs using OOT modules, let GR-MCP auto-detect and build the required image:
```python
launch_flowgraph(
flowgraph_path="lora_rx.py",
auto_image=True # Detects gr-lora_sdr, builds/uses appropriate image
)
```
This:
1. Analyzes the flowgraph for OOT imports
2. Checks if required modules are installed
3. Builds missing modules from the OOT catalog
4. Creates a combo image if multiple OOT modules are needed
## Complete Example
```python
#!/usr/bin/env python3
"""Launch and control an FM receiver via Docker."""
import asyncio
import time
from fastmcp import Client
async def main():
async with Client("gr-mcp") as client:
# Enable runtime tools
await client.call_tool("enable_runtime_mode", {})
# Launch container
result = await client.call_tool("launch_flowgraph", {
"flowgraph_path": "/tmp/fm_receiver.py",
"name": "fm-demo",
"xmlrpc_port": 8080,
"enable_vnc": True
})
print(f"Container: {result.data.name}")
# Wait for startup
time.sleep(3)
# Connect
await client.call_tool("connect_to_container", {"name": "fm-demo"})
# Scan FM band
for freq in [88.1e6, 95.5e6, 101.1e6, 107.9e6]:
await client.call_tool("set_variable", {
"name": "freq",
"value": freq
})
print(f"Tuned to {freq/1e6:.1f} MHz")
time.sleep(2)
# Capture final state
await client.call_tool("capture_screenshot", {"name": "fm-demo"})
# Cleanup
await client.call_tool("stop_flowgraph", {"name": "fm-demo"})
await client.call_tool("remove_flowgraph", {"name": "fm-demo"})
if __name__ == "__main__":
asyncio.run(main())
```
## Next Steps
- [Runtime Control Guide](/guides/runtime-control/) — Advanced XML-RPC patterns
- [ControlPort Monitoring](/guides/controlport/) — Performance metrics via Thrift
- [Code Coverage](/guides/code-coverage/) — Collect coverage from containerized runs

View File

@ -0,0 +1,287 @@
---
title: Code Coverage
description: Collect Python code coverage from containerized flowgraphs
draft: false
---
import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
GR-MCP can collect Python code coverage from flowgraphs running in Docker containers.
This is useful for measuring test coverage of GNU Radio applications and embedded Python blocks.
## How It Works
1. Launch with `enable_coverage=True` — container runs flowgraph under `coverage.py`
2. Run your test scenarios via XML-RPC variable control
3. Stop the flowgraph gracefully — coverage data is written to disk
4. Collect and analyze — generate reports in HTML, XML, or JSON
```
┌─────────────────────────────────────────────────────────────┐
│ Docker Container │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ coverage run flowgraph.py ││
│ │ └─ writes .coverage.* files on exit ││
│ └─────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ /tmp/gr-mcp-coverage/{container_name}/ │
│ ├─ .coverage │
│ ├─ .coverage.{hostname}.{pid}.* │
│ └─ htmlcov/ │
└─────────────────────────────────────────────────────────────┘
```
## Prerequisites
Build the coverage-enabled Docker image:
```bash
docker build -f docker/Dockerfile.gnuradio-coverage \
-t gnuradio-coverage:latest docker/
```
<Aside>
The coverage image extends `gnuradio-runtime` with `python3-coverage`.
It's used automatically when `enable_coverage=True`.
</Aside>
## Collect Coverage
<Steps>
1. **Launch with coverage enabled**
```python
enable_runtime_mode()
launch_flowgraph(
flowgraph_path="/tmp/flowgraph.py",
name="coverage-test",
enable_coverage=True
)
```
2. **Run your test scenario**
```python
connect_to_container(name="coverage-test")
# Exercise the flowgraph
set_variable(name="freq", value=100e6)
time.sleep(2)
set_variable(name="freq", value=200e6)
time.sleep(2)
# ... more test actions ...
```
3. **Stop gracefully** (required for coverage data)
```python
stop_flowgraph(name="coverage-test")
```
<Aside type="caution">
Use `stop_flowgraph()`, not `remove_flowgraph(force=True)`.
Coverage data is only written when the process exits cleanly.
</Aside>
4. **Collect the coverage data**
```python
collect_coverage(name="coverage-test")
# Returns CoverageDataModel:
# container_name: "coverage-test"
# coverage_file: "/tmp/gr-mcp-coverage/coverage-test/.coverage"
# summary: "Name Stmts Miss Cover\n..."
# lines_covered: 150
# lines_total: 200
# coverage_percent: 75.0
```
5. **Generate a report**
```python
generate_coverage_report(
name="coverage-test",
format="html"
)
# Returns CoverageReportModel:
# report_path: "/tmp/gr-mcp-coverage/coverage-test/htmlcov/index.html"
```
</Steps>
## Report Formats
<Tabs>
<TabItem label="HTML">
```python
generate_coverage_report(name="coverage-test", format="html")
# Creates: /tmp/gr-mcp-coverage/coverage-test/htmlcov/index.html
```
Best for visual inspection — shows line-by-line coverage with highlighting.
</TabItem>
<TabItem label="XML">
```python
generate_coverage_report(name="coverage-test", format="xml")
# Creates: /tmp/gr-mcp-coverage/coverage-test/coverage.xml
```
Cobertura-compatible format for CI/CD integration.
</TabItem>
<TabItem label="JSON">
```python
generate_coverage_report(name="coverage-test", format="json")
# Creates: /tmp/gr-mcp-coverage/coverage-test/coverage.json
```
Machine-readable format for custom analysis.
</TabItem>
</Tabs>
## Combine Multiple Runs
Aggregate coverage from multiple test scenarios:
```python
# Run first scenario
launch_flowgraph(..., name="test-1", enable_coverage=True)
# ... exercise ...
stop_flowgraph(name="test-1")
# Run second scenario
launch_flowgraph(..., name="test-2", enable_coverage=True)
# ... exercise ...
stop_flowgraph(name="test-2")
# Combine results
combined = combine_coverage(names=["test-1", "test-2"])
# Returns CoverageDataModel for combined data
# Generate combined report
generate_coverage_report(name="combined", format="html")
```
## Clean Up Coverage Data
```python
# Delete specific container's coverage
delete_coverage(name="coverage-test")
# Returns: 1 (directories deleted)
# Delete old coverage data
delete_coverage(older_than_days=7)
# Returns: number of directories deleted
# Delete all coverage data
delete_coverage()
# Returns: number of directories deleted
```
## Example: Test Suite with Coverage
```python
#!/usr/bin/env python3
"""Run a test suite with coverage collection."""
import asyncio
import time
from fastmcp import Client
TEST_SCENARIOS = [
{"name": "low-freq", "freq": 50e6, "duration": 5},
{"name": "mid-freq", "freq": 500e6, "duration": 5},
{"name": "high-freq", "freq": 2.4e9, "duration": 5},
]
async def run_test_suite():
async with Client("gr-mcp") as client:
await client.call_tool("enable_runtime_mode", {})
container_names = []
for scenario in TEST_SCENARIOS:
name = f"cov-{scenario['name']}"
container_names.append(name)
print(f"Running scenario: {scenario['name']}")
# Launch with coverage
await client.call_tool("launch_flowgraph", {
"flowgraph_path": "/tmp/radio_app.py",
"name": name,
"enable_coverage": True
})
time.sleep(3)
# Connect and run scenario
await client.call_tool("connect_to_container", {"name": name})
await client.call_tool("set_variable", {
"name": "freq",
"value": scenario["freq"]
})
time.sleep(scenario["duration"])
# Stop gracefully for coverage
await client.call_tool("stop_flowgraph", {"name": name})
# Collect this run's coverage
result = await client.call_tool("collect_coverage", {"name": name})
print(f" Coverage: {result.data.coverage_percent}%")
# Combine all runs
print("\nCombining coverage from all scenarios...")
combined = await client.call_tool("combine_coverage", {
"names": container_names
})
print(f"Combined coverage: {combined.data.coverage_percent}%")
# Generate final report
await client.call_tool("generate_coverage_report", {
"name": "combined",
"format": "html"
})
print("HTML report: /tmp/gr-mcp-coverage/combined/htmlcov/index.html")
# Cleanup containers
for name in container_names:
await client.call_tool("remove_flowgraph", {"name": name})
if __name__ == "__main__":
asyncio.run(run_test_suite())
```
## Coverage for Embedded Python Blocks
Coverage includes any embedded Python blocks in your flowgraph:
```python
# Create an embedded block
source = """
import numpy as np
from gnuradio import gr
class my_block(gr.sync_block):
def __init__(self):
gr.sync_block.__init__(self, ...)
def work(self, input_items, output_items):
# This code path will show in coverage
if input_items[0][0] > 0.5:
output_items[0][:] = input_items[0] * 2
else:
output_items[0][:] = input_items[0]
return len(output_items[0])
"""
create_embedded_python_block(source_code=source)
```
Coverage reports will show which branches of your embedded block were exercised.
## Next Steps
- [Running in Docker](/getting-started/running-in-docker/) — Container launch basics
- [Runtime Control](/guides/runtime-control/) — Control flowgraphs during tests

View File

@ -0,0 +1,262 @@
---
title: ControlPort Monitoring
description: Advanced runtime control with performance counters and Thrift
draft: false
---
import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
ControlPort is GNU Radio's binary protocol for advanced runtime control. It provides
richer functionality than XML-RPC: native type support, performance counters, knob
metadata, and PMT message injection.
## ControlPort vs XML-RPC
| Feature | XML-RPC | ControlPort |
|---------|---------|-------------|
| Protocol | HTTP (text) | Thrift (binary) |
| Performance | Slower | Faster |
| Type Support | Basic Python | Complex, vectors, PMT |
| Metadata | None | Units, min/max, hints |
| Perf Counters | No | Yes |
| Message Ports | No | Yes |
**Use ControlPort when:**
- You need performance profiling (throughput, timing, buffer utilization)
- Working with complex data types (complex numbers, vectors)
- You want parameter metadata (units, valid ranges)
- You need to send PMT messages to blocks
## Enable ControlPort
Launch with ControlPort enabled:
```python
enable_runtime_mode()
launch_flowgraph(
flowgraph_path="/tmp/fm_receiver.py",
name="fm-profiled",
enable_controlport=True, # Enable ControlPort
controlport_port=9090, # Port (default 9090)
enable_perf_counters=True # Enable performance counters
)
```
<Aside>
The base runtime image includes GNU Radio's Thrift support. No additional
configuration is needed.
</Aside>
## Connect via ControlPort
<Tabs>
<TabItem label="By Container Name">
```python
connect_to_container_controlport(name="fm-profiled")
```
</TabItem>
<TabItem label="By Host/Port">
```python
connect_controlport(host="127.0.0.1", port=9090)
```
</TabItem>
</Tabs>
## Working with Knobs
ControlPort exposes block parameters as "knobs" with the naming pattern:
`block_alias::parameter`
### Get Knobs
```python
# Get all knobs
get_knobs(pattern="")
# Returns: [KnobModel(name="osmosdr_source_0::freq_corr", value=0.0, ...), ...]
# Filter by regex
get_knobs(pattern=".*::freq.*")
# Returns: [KnobModel(name="osmosdr_source_0::freq_corr", ...), ...]
# Get knobs for a specific block
get_knobs(pattern="osmosdr_source_0::.*")
```
### Set Knobs
```python
# Set multiple knobs atomically
set_knobs({
"sig_source_0::frequency": 1500.0,
"sig_source_0::amplitude": 0.8
})
```
### Get Knob Properties
```python
get_knob_properties(names=["sig_source_0::frequency"])
# Returns: [KnobPropertiesModel(
# name="sig_source_0::frequency",
# units="Hz",
# min=0.0,
# max=1e9,
# description="Output signal frequency"
# )]
```
## Performance Counters
Monitor block performance in real-time:
```python
get_performance_counters()
# Returns: [PerfCounterModel(
# block="low_pass_filter_0",
# nproduced=1048576,
# nconsumed=10485760,
# avg_work_time_us=45.3,
# avg_throughput=23.2e6,
# pc_input_buffers_full=0.85,
# pc_output_buffers_full=0.12
# ), ...]
# Filter by block
get_performance_counters(block="low_pass_filter_0")
```
### Performance Metrics Explained
| Metric | Description |
|--------|-------------|
| `nproduced` | Items produced by block |
| `nconsumed` | Items consumed by block |
| `avg_work_time_us` | Average time in `work()` function |
| `avg_throughput` | Items per second |
| `pc_input_buffers_full` | Input buffer utilization (0-1) |
| `pc_output_buffers_full` | Output buffer utilization (0-1) |
<Aside type="tip">
High `pc_input_buffers_full` indicates downstream blocks are slow.
High `pc_output_buffers_full` indicates the block itself is a bottleneck.
</Aside>
## PMT Message Injection
Send messages to block message ports:
```python
# Send a simple string message
post_message(
block="pdu_sink_0",
port="pdus",
message="hello"
)
# Send a dict (converted to PMT dict)
post_message(
block="command_handler_0",
port="command",
message={"freq": 1e6, "gain": 40}
)
```
## Example: Performance Monitor
```python
#!/usr/bin/env python3
"""Monitor flowgraph performance and identify bottlenecks."""
import asyncio
import time
from fastmcp import Client
async def monitor_performance():
async with Client("gr-mcp") as client:
await client.call_tool("enable_runtime_mode", {})
# Launch with ControlPort
await client.call_tool("launch_flowgraph", {
"flowgraph_path": "/tmp/signal_chain.py",
"name": "perf-test",
"enable_controlport": True,
"enable_perf_counters": True
})
time.sleep(5) # Let it warm up
# Connect via ControlPort
await client.call_tool("connect_to_container_controlport", {
"name": "perf-test"
})
# Sample performance 10 times
for i in range(10):
result = await client.call_tool("get_performance_counters", {})
print(f"\n--- Sample {i+1} ---")
for counter in result.data:
buffer_status = "OK"
if counter.pc_input_buffers_full > 0.9:
buffer_status = "INPUT BOTTLENECK"
elif counter.pc_output_buffers_full > 0.9:
buffer_status = "BLOCK BOTTLENECK"
print(f"{counter.block}: {counter.avg_throughput/1e6:.2f} MSps, "
f"work={counter.avg_work_time_us:.1f}us [{buffer_status}]")
time.sleep(1)
# Cleanup
await client.call_tool("disconnect_controlport", {})
await client.call_tool("stop_flowgraph", {"name": "perf-test"})
await client.call_tool("remove_flowgraph", {"name": "perf-test"})
if __name__ == "__main__":
asyncio.run(monitor_performance())
```
## Using Both XML-RPC and ControlPort
You can have both connections active simultaneously:
```python
# Launch with both
launch_flowgraph(
flowgraph_path="...",
name="dual-control",
xmlrpc_port=8080,
enable_controlport=True
)
# Connect to XML-RPC for variable control
connect_to_container(name="dual-control")
# Connect to ControlPort for performance monitoring
connect_to_container_controlport(name="dual-control")
# Use XML-RPC for simple variable changes
set_variable(name="freq", value=101.1e6)
# Use ControlPort for performance data
get_performance_counters()
# Disconnect both
disconnect() # Disconnects both XML-RPC and ControlPort
```
## Disconnect ControlPort
```python
# Disconnect only ControlPort (keep XML-RPC)
disconnect_controlport()
# Or disconnect everything
disconnect()
```
## Next Steps
- [Runtime Communication Concepts](/concepts/runtime-communication/) — Deep dive into protocol details
- [Code Coverage](/guides/code-coverage/) — Collect test coverage from runtime

View File

@ -0,0 +1,251 @@
---
title: Working with OOT Modules
description: Install and manage GNU Radio Out-of-Tree modules
draft: false
---
import { Steps, Aside, Tabs, TabItem, CardGrid, Card } from '@astrojs/starlight/components';
Out-of-Tree (OOT) modules extend GNU Radio with specialized functionality like LoRa demodulation,
ADS-B decoding, or hardware support. GR-MCP provides tools to discover, install, and combine
OOT modules into Docker images.
## Browse Available Modules
GR-MCP includes a curated catalog of 24 OOT modules accessible via MCP resources:
```python
# List all modules
# Resource: oot://directory
# Get details for a specific module
# Resource: oot://directory/lora_sdr
```
<CardGrid>
<Card title="12 Preinstalled" icon="rocket">
Available in the base `gnuradio-runtime` image: osmosdr, satellites, gsm, rds, fosphor, air_modes, etc.
</Card>
<Card title="12 Installable" icon="add-document">
Built on-demand: lora_sdr, ieee802_11, adsb, iridium, dab, nrsc5, etc.
</Card>
</CardGrid>
### Module Categories
| Category | Modules |
|----------|---------|
| **Hardware** | osmosdr, funcube, hpsdr, limesdr |
| **Satellite** | satellites, satnogs, iridium, leo |
| **Cellular** | gsm |
| **Broadcast** | rds, dab, dl5eu, nrsc5 |
| **Aviation** | air_modes, adsb |
| **IoT** | lora_sdr, ieee802_15_4 |
| **WiFi** | ieee802_11 |
| **Analysis** | iqbal, radar, inspector |
| **Visualization** | fosphor |
| **Utility** | foo |
## Install a Module
<Steps>
1. **Enable runtime mode** (required for Docker operations)
```python
enable_runtime_mode()
```
2. **Install the module**
For catalog modules:
```python
install_oot_module(
git_url="https://github.com/tapparelj/gr-lora_sdr",
branch="master"
)
```
This:
- Clones the repository
- Compiles with cmake
- Creates a Docker image: `gnuradio-lora_sdr-runtime:latest`
3. **Use the new image**
```python
launch_flowgraph(
flowgraph_path="lora_rx.py",
image="gnuradio-lora_sdr-runtime:latest"
)
```
</Steps>
### With Build Dependencies
Some modules need extra packages for compilation:
```python
install_oot_module(
git_url="https://github.com/hboeglen/gr-dab",
branch="maint-3.10",
build_deps=["autoconf", "automake", "libtool", "libfaad-dev"],
cmake_args=["-DENABLE_DOXYGEN=OFF"]
)
```
<Aside type="tip">
The OOT catalog includes all required `build_deps` and `cmake_args` for each module.
Check the resource `oot://directory/{module_name}` for a copy-paste example.
</Aside>
## Detect Required Modules
GR-MCP can analyze flowgraphs to identify OOT dependencies:
```python
detect_oot_modules(flowgraph_path="lora_rx.py")
# Returns OOTDetectionResult:
# detected_modules: ["lora_sdr"]
# unknown_blocks: []
# recommended_image: "gnuradio-lora_sdr-runtime:latest"
```
For `.grc` files, detection uses prefix matching against the catalog.
For `.py` files, it parses Python imports for more accuracy.
## Auto-Image Selection
The simplest approach: let GR-MCP handle everything:
```python
launch_flowgraph(
flowgraph_path="lora_rx.py",
auto_image=True
)
```
This automatically:
1. Detects required OOT modules
2. Installs missing modules from the catalog
3. Builds combo images if multiple modules are needed
4. Launches with the appropriate image
## Combine Multiple Modules
Some flowgraphs need multiple OOT modules. Create a combo image:
```python
build_multi_oot_image(module_names=["lora_sdr", "adsb"])
# Returns ComboImageResult:
# success: True
# image: ComboImageInfo(
# combo_key: "combo:adsb+lora_sdr",
# image_tag: "gr-combo-adsb-lora_sdr:latest",
# modules: ["adsb", "lora_sdr"]
# )
```
Missing modules are auto-built from the catalog.
### Launch with Combo Image
```python
launch_flowgraph(
flowgraph_path="multi_protocol_rx.py",
image="gr-combo-adsb-lora_sdr:latest"
)
```
Or use auto-detection:
```python
detect_oot_modules("multi_protocol_rx.py")
# detected_modules: ["lora_sdr", "adsb"]
# recommended_image: "gr-combo-adsb-lora_sdr:latest"
```
## Manage Installed Images
```python
# List installed OOT images
list_oot_images()
# Returns: [OOTImageInfo(module_name="lora_sdr", image_tag="..."), ...]
# List combo images
list_combo_images()
# Returns: [ComboImageInfo(combo_key="combo:adsb+lora_sdr", ...)]
# Remove a single-module image
remove_oot_image(module_name="lora_sdr")
# Remove a combo image
remove_combo_image(combo_key="combo:adsb+lora_sdr")
```
## Load OOT Blocks at Design Time
For flowgraph design (without Docker), load OOT block definitions:
```python
# Add a path containing .block.yml files
add_block_path("/usr/local/share/gnuradio/grc/blocks")
# Check current paths and block count
get_block_paths()
# Returns BlockPathsModel with paths and total_blocks
# Load multiple paths at once
load_oot_blocks(paths=[
"/usr/local/share/gnuradio/grc/blocks",
"/home/user/gr-modules/lib/grc"
])
```
<Aside>
GR-MCP auto-discovers OOT blocks from common locations (`/usr/local/share/gnuradio/grc/blocks`,
`~/.local/share/gnuradio/grc/blocks`) at startup.
</Aside>
## Example: LoRa Receiver Setup
Complete workflow for receiving LoRa packets:
```python
# 1. Enable runtime mode
enable_runtime_mode()
# 2. Check if lora_sdr is available
# Resource: oot://directory/lora_sdr
# -> preinstalled: False, installed: False
# 3. Install the module
install_oot_module(
git_url="https://github.com/tapparelj/gr-lora_sdr",
branch="master"
)
# -> image_tag: "gnuradio-lora_sdr-runtime:latest"
# 4. Load blocks for design
add_block_path("/usr/local/share/gnuradio/grc/blocks")
# 5. Build the flowgraph
make_block(block_type="lora_sdr_lora_sdr_lora_rx")
# ... configure and connect ...
# 6. Generate code
generate_code(output_dir="/tmp")
# 7. Launch with the new image
launch_flowgraph(
flowgraph_path="/tmp/lora_rx.py",
name="lora-receiver",
image="gnuradio-lora_sdr-runtime:latest",
device_paths=["/dev/bus/usb"]
)
```
## Next Steps
- [Runtime Control](/guides/runtime-control/) — Control variables while the flowgraph runs
- [OOT Catalog Reference](/reference/oot-catalog/) — Full module list with install examples

View File

@ -0,0 +1,207 @@
---
title: Runtime Control
description: Control flowgraph variables and execution in real-time
draft: false
---
import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
Once a flowgraph is running, you can read and modify variables, control execution,
and monitor status through XML-RPC. This enables real-time tuning without restarting.
## How It Works
Generated flowgraphs expose variables through an XML-RPC server:
```
┌─────────────────────────────┐ XML-RPC (HTTP) ┌─────────────────┐
│ GR-MCP │ ←─────────────────────→ │ Flowgraph │
│ (MCP Server) │ get_freq() │ (Python) │
│ │ set_freq(101.1e6) │ │
└─────────────────────────────┘ └─────────────────┘
```
The flowgraph's variables become `get_X()` and `set_X()` methods automatically.
## Prerequisites
Your flowgraph must include an XML-RPC server block, or be launched via GR-MCP's
`launch_flowgraph()` which configures this automatically.
<Aside>
GR-MCP containers expose XML-RPC on a dynamically allocated port. Use
`connect_to_container()` to auto-discover it from container labels.
</Aside>
## Connect to a Flowgraph
<Tabs>
<TabItem label="By Container Name">
```python
# Most common: connect to a GR-MCP container
connect_to_container(name="fm-radio")
```
</TabItem>
<TabItem label="By URL">
```python
# Direct connection to any XML-RPC endpoint
connect(url="http://localhost:8080")
```
</TabItem>
</Tabs>
Both methods return `ConnectionInfoModel` with endpoint details.
## Variable Operations
### List Variables
```python
list_variables()
# Returns: [
# VariableModel(name="freq", value=101100000.0, type="float"),
# VariableModel(name="gain", value=40, type="int"),
# VariableModel(name="samp_rate", value=2000000.0, type="float"),
# ]
```
### Get a Variable
```python
get_variable(name="freq")
# Returns: 101100000.0
```
### Set a Variable
```python
set_variable(name="freq", value=98.5e6)
# Returns: True
```
<Aside type="caution">
Variable changes take effect immediately. For audio applications, this can cause
audible glitches. Use `lock()` / `unlock()` for smoother transitions.
</Aside>
## Thread-Safe Updates
For multiple parameter changes:
```python
# Pause signal processing
lock()
# Make changes (no audio during this)
set_variable(name="freq", value=102.7e6)
set_variable(name="gain", value=35)
set_variable(name="bandwidth", value=200e3)
# Resume processing
unlock()
```
The flowgraph buffers input during the lock and processes it on unlock.
## Execution Control
```python
# Stop the flowgraph (tb.stop())
stop()
# Restart the flowgraph (tb.start())
start()
```
<Aside>
`stop()` gracefully halts processing. Use this before collecting coverage data
or when you need a clean shutdown.
</Aside>
## Check Connection Status
```python
get_status()
# Returns RuntimeStatusModel:
# connected: True
# connection: ConnectionInfoModel(url="http://localhost:8080", ...)
# containers: [ContainerModel(...), ...]
```
## Disconnect
```python
disconnect()
# Closes XML-RPC connection (and ControlPort if active)
```
## Example: FM Band Scanner
```python
#!/usr/bin/env python3
"""Scan FM broadcast band and measure signal strength."""
import asyncio
import time
from fastmcp import Client
FM_STATIONS = [88.1, 90.3, 94.7, 98.5, 101.1, 103.5, 107.9]
async def scan_fm_band():
async with Client("gr-mcp") as client:
await client.call_tool("enable_runtime_mode", {})
# Launch with a signal strength indicator
await client.call_tool("launch_flowgraph", {
"flowgraph_path": "/tmp/fm_scanner.py",
"name": "fm-scanner",
"xmlrpc_port": 8080
})
time.sleep(3)
await client.call_tool("connect_to_container", {"name": "fm-scanner"})
# Scan each station
results = []
for freq_mhz in FM_STATIONS:
await client.call_tool("set_variable", {
"name": "freq",
"value": freq_mhz * 1e6
})
time.sleep(0.5) # Allow settling
# Read signal level (if flowgraph exposes it)
try:
level = await client.call_tool("get_variable", {"name": "signal_level"})
results.append((freq_mhz, level.data))
except Exception:
results.append((freq_mhz, None))
# Report findings
for freq, level in results:
status = f"{level:.1f} dB" if level else "no signal"
print(f"{freq:.1f} MHz: {status}")
# Cleanup
await client.call_tool("stop_flowgraph", {"name": "fm-scanner"})
await client.call_tool("remove_flowgraph", {"name": "fm-scanner"})
if __name__ == "__main__":
asyncio.run(scan_fm_band())
```
## Error Handling
Common errors and solutions:
| Error | Cause | Solution |
|-------|-------|----------|
| "Not connected" | No active XML-RPC connection | Call `connect()` or `connect_to_container()` |
| "Unknown variable" | Variable not exposed via XML-RPC | Check flowgraph has XML-RPC block with callback vars |
| "Connection refused" | Container not running / wrong port | Verify with `list_containers()`, `get_status()` |
| "Timeout" | Flowgraph unresponsive | Check logs with `get_container_logs()` |
## Next Steps
- [ControlPort Monitoring](/guides/controlport/) — Advanced monitoring with performance counters
- [Code Coverage](/guides/code-coverage/) — Collect coverage from runtime tests

View File

@ -0,0 +1,116 @@
---
title: GR-MCP
description: GNU Radio MCP Server for programmatic, automated, and AI-driven flowgraph control
draft: false
template: splash
hero:
tagline: Build, run, and control GNU Radio flowgraphs through the Model Context Protocol
actions:
- text: Get Started
link: /getting-started/installation/
icon: right-arrow
- text: View on Git
link: https://git.supported.systems/MCP/gr-mcp
icon: external
variant: minimal
---
import { Card, CardGrid, Tabs, TabItem } from '@astrojs/starlight/components';
## What is GR-MCP?
GR-MCP is a [FastMCP](https://gofastmcp.com) server that exposes GNU Radio's flowgraph capabilities through the [Model Context Protocol](https://modelcontextprotocol.io). This enables:
- **Programmatic flowgraph creation** — Build `.grc` files from code or automation
- **Runtime control** — Adjust variables and monitor performance without restarting
- **Docker isolation** — Run flowgraphs in containers with automatic VNC/X11 support
- **OOT module management** — Install and combine Out-of-Tree modules on demand
<CardGrid stagger>
<Card title="29 Platform Tools" icon="puzzle">
Create blocks, connect ports, validate flowgraphs, and generate Python code
</Card>
<Card title="40+ Runtime Tools" icon="rocket">
Launch containers, control XML-RPC variables, monitor ControlPort knobs
</Card>
<Card title="24 OOT Modules" icon="add-document">
Curated catalog with gr-osmosdr, gr-satellites, gr-lora_sdr, and more
</Card>
<Card title="Dynamic Registration" icon="setting">
Runtime tools load on-demand to minimize context usage
</Card>
</CardGrid>
## Quick Example
<Tabs>
<TabItem label="Build a Flowgraph">
```python
# Create an FM receiver flowgraph
make_block(block_type="osmosdr_source")
make_block(block_type="low_pass_filter")
make_block(block_type="analog_wfm_rcv")
make_block(block_type="audio_sink")
# Connect the signal chain
connect_blocks("osmosdr_source_0", "0", "low_pass_filter_0", "0")
connect_blocks("low_pass_filter_0", "0", "analog_wfm_rcv_0", "0")
connect_blocks("analog_wfm_rcv_0", "0", "audio_sink_0", "0")
# Validate and save
validate_flowgraph()
save_flowgraph("/tmp/fm_receiver.grc")
```
</TabItem>
<TabItem label="Control at Runtime">
```python
# Launch flowgraph in Docker container
launch_flowgraph(
flowgraph_path="/tmp/fm_receiver.py",
name="fm-radio",
xmlrpc_port=8080,
enable_vnc=True
)
# Connect and tune
connect_to_container("fm-radio")
set_variable("freq", 101.1e6)
# Capture display
capture_screenshot("fm-radio")
```
</TabItem>
<TabItem label="Install OOT Modules">
```python
# Check what's available
# Resource: oot://directory
# Install gr-lora_sdr for LoRa reception
install_oot_module(
git_url="https://github.com/tapparelj/gr-lora_sdr",
branch="master"
)
# Launch with the new module
launch_flowgraph(
flowgraph_path="lora_rx.py",
image="gnuradio-lora_sdr-runtime:latest"
)
```
</TabItem>
</Tabs>
## Getting Started
<CardGrid>
<Card title="Installation" icon="laptop">
Set up GR-MCP with `uv` and GNU Radio
[Read the guide →](/getting-started/installation/)
</Card>
<Card title="Your First Flowgraph" icon="document">
Build and validate a complete flowgraph
[Follow the tutorial →](/getting-started/first-flowgraph/)
</Card>
</CardGrid>

View File

@ -0,0 +1,204 @@
---
title: Docker Images
description: Reference for GR-MCP Docker images
draft: false
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
GR-MCP uses Docker images for running flowgraphs in isolation with headless GUI support.
## Base Images
### `gnuradio-runtime:latest`
The base runtime image for running flowgraphs.
**Includes:**
- GNU Radio 3.10.x
- Xvfb (virtual framebuffer for headless X11)
- VNC server (optional visual debugging)
- ImageMagick (screenshot capture)
- 12 preinstalled OOT modules (osmosdr, satellites, gsm, etc.)
**Build:**
```bash
docker build -f docker/Dockerfile.gnuradio-runtime \
-t gnuradio-runtime:latest docker/
```
### `gnuradio-coverage:latest`
Extended runtime image with Python coverage support.
**Includes:**
- Everything in `gnuradio-runtime`
- `python3-coverage` package
**Build:**
```bash
docker build -f docker/Dockerfile.gnuradio-coverage \
-t gnuradio-coverage:latest docker/
```
## OOT Module Images
When you install an OOT module via `install_oot_module()`, GR-MCP creates a new image.
### Naming Convention
```
gnuradio-{module_name}-runtime:latest
```
**Examples:**
- `gnuradio-lora_sdr-runtime:latest`
- `gnuradio-adsb-runtime:latest`
- `gnuradio-ieee802_11-runtime:latest`
### Build Process
1. Clones the git repository
2. Builds with cmake in a multi-stage Docker build
3. Installs into the runtime image
4. Tags with the module name
## Combo Images
For flowgraphs requiring multiple OOT modules, GR-MCP creates combo images.
### Naming Convention
```
gr-combo-{module1}-{module2}-...:latest
```
Modules are sorted alphabetically in the name.
**Examples:**
- `gr-combo-adsb-lora_sdr:latest`
- `gr-combo-adsb-ieee802_11-lora_sdr:latest`
### Combo Keys
Combo images are tracked by a `combo_key`:
```
combo:{module1}+{module2}+...
```
**Examples:**
- `combo:adsb+lora_sdr`
- `combo:adsb+ieee802_11+lora_sdr`
## Container Configuration
When launching a flowgraph, containers are configured with:
| Feature | Configuration |
|---------|---------------|
| Display | `DISPLAY=:99` (Xvfb) |
| X11 | Xvfb runs on `:99` |
| VNC | Port 5900 (if enabled) |
| XML-RPC | Dynamic port mapping |
| ControlPort | Port 9090 (if enabled) |
| Workdir | `/flowgraph` |
| Network | Host network mode |
### Labels
GR-MCP containers are tagged with labels for management:
```
gr-mcp=true
gr-mcp-name={container_name}
gr-mcp-xmlrpc-port={port}
gr-mcp-vnc={enabled}
gr-mcp-controlport={enabled}
gr-mcp-controlport-port={port}
gr-mcp-coverage={enabled}
```
### Volume Mounts
| Host Path | Container Path | Purpose |
|-----------|----------------|---------|
| Flowgraph file | `/flowgraph/{name}.py` | Flowgraph script |
| Coverage dir | `/coverage` | Coverage data output |
## Hardware Access
To access SDR hardware inside containers, pass device paths:
<Tabs>
<TabItem label="USB Devices">
```python
launch_flowgraph(
flowgraph_path="...",
device_paths=["/dev/bus/usb"]
)
```
Grants access to all USB devices (RTL-SDR, HackRF, Airspy, etc.)
</TabItem>
<TabItem label="Serial Devices">
```python
launch_flowgraph(
flowgraph_path="...",
device_paths=["/dev/ttyUSB0"]
)
```
Grants access to specific serial ports (USRP, etc.)
</TabItem>
<TabItem label="Multiple Devices">
```python
launch_flowgraph(
flowgraph_path="...",
device_paths=["/dev/bus/usb", "/dev/ttyUSB0"]
)
```
</TabItem>
</Tabs>
<Aside type="caution">
The container runs with privileged access when device paths are specified.
Ensure you trust the flowgraph code.
</Aside>
## Image Management
### List Images
```python
# List OOT images
list_oot_images()
# List combo images
list_combo_images()
```
### Remove Images
```python
# Remove single-module image
remove_oot_image(module_name="lora_sdr")
# Remove combo image
remove_combo_image(combo_key="combo:adsb+lora_sdr")
```
### Docker Commands
```bash
# List all GR-MCP images
docker images | grep -E "gnuradio-|gr-combo"
# Remove all GR-MCP images
docker images --format '{{.Repository}}:{{.Tag}}' | \
grep -E "gnuradio-|gr-combo" | xargs docker rmi
# List all GR-MCP containers
docker ps -a --filter "label=gr-mcp=true"
# Remove all stopped GR-MCP containers
docker container prune --filter "label=gr-mcp=true"
```

View File

@ -0,0 +1,249 @@
---
title: OOT Catalog
description: Curated catalog of GNU Radio Out-of-Tree modules
draft: false
---
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
GR-MCP includes a curated catalog of 24 OOT modules. 12 are preinstalled in the base
runtime image; 12 can be installed on-demand.
## Accessing the Catalog
The catalog is exposed as MCP resources:
```python
# List all modules
# Resource: oot://directory
# Get details for a specific module
# Resource: oot://directory/lora_sdr
```
## Preinstalled Modules
These modules are included in `gnuradio-runtime:latest`:
### Hardware
| Module | Description |
|--------|-------------|
| **osmosdr** | Hardware source/sink for RTL-SDR, Airspy, HackRF, and more |
| **funcube** | Funcube Dongle Pro/Pro+ controller and source block |
| **hpsdr** | OpenHPSDR Protocol 1 interface for HPSDR hardware |
| **limesdr** | LimeSDR source/sink blocks (LMS7002M) |
### Satellite
| Module | Description |
|--------|-------------|
| **satellites** | Satellite telemetry decoders (AX.25, CCSDS, AO-73, etc.) |
| **satnogs** | SatNOGS satellite ground station decoders and deframers |
### Broadcast
| Module | Description |
|--------|-------------|
| **rds** | FM RDS/RBDS (Radio Data System) decoder |
### Cellular
| Module | Description |
|--------|-------------|
| **gsm** | GSM/GPRS burst receiver and channel decoder |
### Aviation
| Module | Description |
|--------|-------------|
| **air_modes** | Mode-S/ADS-B aircraft transponder decoder (1090 MHz) |
### Analysis & Visualization
| Module | Description |
|--------|-------------|
| **iqbal** | Blind IQ imbalance estimator and correction |
| **radar** | Radar signal processing toolbox (FMCW, OFDM radar) |
| **fosphor** | GPU-accelerated real-time spectrum display (OpenCL) |
## Installable Modules
Install these with `install_oot_module()`:
### IoT
#### gr-lora_sdr
LoRa PHY transceiver (CSS modulation/demodulation)
```python
install_oot_module(
git_url="https://github.com/tapparelj/gr-lora_sdr",
branch="master"
)
```
#### gr-ieee802_15_4
IEEE 802.15.4 (Zigbee) O-QPSK transceiver
```python
install_oot_module(
git_url="https://github.com/bastibl/gr-ieee802-15-4",
branch="maint-3.10",
build_deps=["castxml"]
)
```
### WiFi
#### gr-ieee802_11
IEEE 802.11a/g/p OFDM transceiver
```python
install_oot_module(
git_url="https://github.com/bastibl/gr-ieee802-11",
branch="maint-3.10",
build_deps=["castxml"]
)
```
### Aviation
#### gr-adsb
ADS-B (1090 MHz) aircraft transponder decoder
```python
install_oot_module(
git_url="https://github.com/mhostetter/gr-adsb",
branch="maint-3.10"
)
```
### Satellite
#### gr-iridium
Iridium satellite burst detector and demodulator
```python
install_oot_module(
git_url="https://github.com/muccc/gr-iridium",
branch="master"
)
```
#### gr-leo
LEO satellite channel simulator (Doppler, path loss, atmosphere)
```python
install_oot_module(
git_url="https://gitlab.com/librespacefoundation/gr-leo",
branch="gnuradio-3.10"
)
```
### Broadcast
#### gr-dab
DAB/DAB+ digital audio broadcast receiver
```python
install_oot_module(
git_url="https://github.com/hboeglen/gr-dab",
branch="maint-3.10",
build_deps=["autoconf", "automake", "libtool", "libfaad-dev"],
cmake_args=["-DENABLE_DOXYGEN=OFF"]
)
```
#### gr-nrsc5
HD Radio (NRSC-5) digital broadcast decoder
```python
install_oot_module(
git_url="https://github.com/argilo/gr-nrsc5",
branch="master",
build_deps=["autoconf", "automake", "libtool"]
)
```
#### gr-dl5eu
DVB-T OFDM synchronization and TPS decoder
```python
install_oot_module(
git_url="https://github.com/dl5eu/gr-dl5eu",
branch="main"
)
```
### Utility
#### gr-foo
Wireshark PCAP connector, burst tagger, periodic msg source
```python
install_oot_module(
git_url="https://github.com/bastibl/gr-foo",
branch="maint-3.10",
build_deps=["castxml"]
)
```
### Optical
#### gr-owc
Optical Wireless Communication channel simulation and modulation
```python
install_oot_module(
git_url="https://github.com/UCaNLabUMB/gr-owc",
branch="main"
)
```
### Analysis
#### gr-inspector
Signal analysis toolbox (energy detection, OFDM estimation)
<Aside type="caution">
gr-inspector has API compatibility issues with GNU Radio 3.10. The master branch
targets GNU Radio 3.9.
</Aside>
```python
install_oot_module(
git_url="https://github.com/gnuradio/gr-inspector",
branch="master",
build_deps=["qtbase5-dev", "libqwt-qt5-dev"]
)
```
## Module Categories Summary
| Category | Preinstalled | Installable |
|----------|--------------|-------------|
| Hardware | 4 | 0 |
| Satellite | 2 | 2 |
| Cellular | 1 | 0 |
| Broadcast | 1 | 3 |
| Aviation | 1 | 1 |
| IoT | 0 | 2 |
| WiFi | 0 | 1 |
| Analysis | 2 | 1 |
| Visualization | 1 | 0 |
| Utility | 0 | 1 |
| Optical | 0 | 1 |
| **Total** | **12** | **12** |

View File

@ -0,0 +1,171 @@
---
title: Tools Overview
description: Complete reference of all GR-MCP tools
draft: false
---
import { CardGrid, Card, Aside } from '@astrojs/starlight/components';
GR-MCP exposes tools in two groups: **platform tools** (always available) for flowgraph
design, and **runtime tools** (loaded on demand) for container control.
## Tool Organization
<CardGrid>
<Card title="29 Platform Tools" icon="puzzle">
Always available for flowgraph building, validation, and code generation.
No Docker required.
</Card>
<Card title="~40 Runtime Tools" icon="rocket">
Loaded via `enable_runtime_mode()`. Requires Docker for container features.
</Card>
</CardGrid>
## Platform Tools (Always Available)
### Flowgraph Management
| Tool | Description |
|------|-------------|
| [`get_blocks`](/reference/tools/flowgraph#get_blocks) | List all blocks in the flowgraph |
| [`make_block`](/reference/tools/flowgraph#make_block) | Create a new block |
| [`remove_block`](/reference/tools/flowgraph#remove_block) | Remove a block |
| [`save_flowgraph`](/reference/tools/flowgraph#save_flowgraph) | Save to `.grc` file |
| [`load_flowgraph`](/reference/tools/flowgraph#load_flowgraph) | Load from `.grc` file |
| [`get_flowgraph_options`](/reference/tools/flowgraph#get_flowgraph_options) | Get flowgraph metadata |
| [`set_flowgraph_options`](/reference/tools/flowgraph#set_flowgraph_options) | Set flowgraph metadata |
| [`export_flowgraph_data`](/reference/tools/flowgraph#export_flowgraph_data) | Export as dict |
| [`import_flowgraph_data`](/reference/tools/flowgraph#import_flowgraph_data) | Import from dict |
### Block Management
| Tool | Description |
|------|-------------|
| [`get_block_params`](/reference/tools/blocks#get_block_params) | List block parameters |
| [`set_block_params`](/reference/tools/blocks#set_block_params) | Set block parameters |
| [`get_block_sources`](/reference/tools/blocks#get_block_sources) | List output ports |
| [`get_block_sinks`](/reference/tools/blocks#get_block_sinks) | List input ports |
| [`bypass_block`](/reference/tools/blocks#bypass_block) | Bypass a block |
| [`unbypass_block`](/reference/tools/blocks#unbypass_block) | Unbypass a block |
### Connection Management
| Tool | Description |
|------|-------------|
| [`get_connections`](/reference/tools/connections#get_connections) | List all connections |
| [`connect_blocks`](/reference/tools/connections#connect_blocks) | Connect two blocks |
| [`disconnect_blocks`](/reference/tools/connections#disconnect_blocks) | Disconnect two blocks |
### Validation
| Tool | Description |
|------|-------------|
| [`validate_block`](/reference/tools/validation#validate_block) | Validate a single block |
| [`validate_flowgraph`](/reference/tools/validation#validate_flowgraph) | Validate entire flowgraph |
| [`get_all_errors`](/reference/tools/validation#get_all_errors) | Get all validation errors |
### Platform
| Tool | Description |
|------|-------------|
| [`get_all_available_blocks`](/reference/tools/platform#get_all_available_blocks) | List all block types |
| [`search_blocks`](/reference/tools/platform#search_blocks) | Search blocks by keyword |
| [`get_block_categories`](/reference/tools/platform#get_block_categories) | List block categories |
| [`load_oot_blocks`](/reference/tools/platform#load_oot_blocks) | Load OOT block paths |
| [`add_block_path`](/reference/tools/platform#add_block_path) | Add a block path |
| [`get_block_paths`](/reference/tools/platform#get_block_paths) | List block paths |
### Code Generation
| Tool | Description |
|------|-------------|
| [`generate_code`](/reference/tools/codegen#generate_code) | Generate Python code |
| [`evaluate_expression`](/reference/tools/codegen#evaluate_expression) | Evaluate Python expression |
| [`create_embedded_python_block`](/reference/tools/codegen#create_embedded_python_block) | Create embedded Python block |
## Runtime Tools (Enable with `enable_runtime_mode()`)
<Aside>
Runtime tools require Docker. If Docker is unavailable, container-related tools
won't be registered.
</Aside>
### Runtime Mode Control
| Tool | Description |
|------|-------------|
| [`get_runtime_mode`](/reference/tools/runtime-mode#get_runtime_mode) | Check runtime mode status |
| [`enable_runtime_mode`](/reference/tools/runtime-mode#enable_runtime_mode) | Enable runtime tools |
| [`disable_runtime_mode`](/reference/tools/runtime-mode#disable_runtime_mode) | Disable runtime tools |
| [`get_client_capabilities`](/reference/tools/runtime-mode#get_client_capabilities) | Get MCP client capabilities |
| [`list_client_roots`](/reference/tools/runtime-mode#list_client_roots) | List client root directories |
### Docker Container Management
| Tool | Description |
|------|-------------|
| [`launch_flowgraph`](/reference/tools/docker#launch_flowgraph) | Launch in Docker container |
| [`list_containers`](/reference/tools/docker#list_containers) | List running containers |
| [`stop_flowgraph`](/reference/tools/docker#stop_flowgraph) | Stop a container |
| [`remove_flowgraph`](/reference/tools/docker#remove_flowgraph) | Remove a container |
| [`capture_screenshot`](/reference/tools/docker#capture_screenshot) | Capture GUI screenshot |
| [`get_container_logs`](/reference/tools/docker#get_container_logs) | Get container logs |
### XML-RPC Control
| Tool | Description |
|------|-------------|
| [`connect`](/reference/tools/xmlrpc#connect) | Connect to XML-RPC URL |
| [`connect_to_container`](/reference/tools/xmlrpc#connect_to_container) | Connect by container name |
| [`disconnect`](/reference/tools/xmlrpc#disconnect) | Disconnect |
| [`get_status`](/reference/tools/xmlrpc#get_status) | Get connection status |
| [`list_variables`](/reference/tools/xmlrpc#list_variables) | List exposed variables |
| [`get_variable`](/reference/tools/xmlrpc#get_variable) | Get variable value |
| [`set_variable`](/reference/tools/xmlrpc#set_variable) | Set variable value |
| [`start`](/reference/tools/xmlrpc#start) | Start flowgraph |
| [`stop`](/reference/tools/xmlrpc#stop) | Stop flowgraph |
| [`lock`](/reference/tools/xmlrpc#lock) | Lock for updates |
| [`unlock`](/reference/tools/xmlrpc#unlock) | Unlock after updates |
### ControlPort/Thrift
| Tool | Description |
|------|-------------|
| [`connect_controlport`](/reference/tools/controlport#connect_controlport) | Connect to ControlPort |
| [`connect_to_container_controlport`](/reference/tools/controlport#connect_to_container_controlport) | Connect by container |
| [`disconnect_controlport`](/reference/tools/controlport#disconnect_controlport) | Disconnect ControlPort |
| [`get_knobs`](/reference/tools/controlport#get_knobs) | Get knob values |
| [`set_knobs`](/reference/tools/controlport#set_knobs) | Set knob values |
| [`get_knob_properties`](/reference/tools/controlport#get_knob_properties) | Get knob metadata |
| [`get_performance_counters`](/reference/tools/controlport#get_performance_counters) | Get perf metrics |
| [`post_message`](/reference/tools/controlport#post_message) | Send PMT message |
### Code Coverage
| Tool | Description |
|------|-------------|
| [`collect_coverage`](/reference/tools/coverage#collect_coverage) | Collect coverage data |
| [`generate_coverage_report`](/reference/tools/coverage#generate_coverage_report) | Generate report |
| [`combine_coverage`](/reference/tools/coverage#combine_coverage) | Combine multiple runs |
| [`delete_coverage`](/reference/tools/coverage#delete_coverage) | Delete coverage data |
### OOT Module Installation
| Tool | Description |
|------|-------------|
| [`detect_oot_modules`](/reference/tools/oot#detect_oot_modules) | Detect OOT dependencies |
| [`install_oot_module`](/reference/tools/oot#install_oot_module) | Install OOT module |
| [`list_oot_images`](/reference/tools/oot#list_oot_images) | List installed images |
| [`remove_oot_image`](/reference/tools/oot#remove_oot_image) | Remove OOT image |
| [`build_multi_oot_image`](/reference/tools/oot#build_multi_oot_image) | Build combo image |
| [`list_combo_images`](/reference/tools/oot#list_combo_images) | List combo images |
| [`remove_combo_image`](/reference/tools/oot#remove_combo_image) | Remove combo image |
## MCP Resources
In addition to tools, GR-MCP exposes resources for OOT module discovery:
| URI | Description |
|-----|-------------|
| `oot://directory` | Index of all OOT modules in the catalog |
| `oot://directory/{name}` | Detailed info for a specific module |

View File

@ -0,0 +1,168 @@
---
title: Block Tools
description: Tools for block parameter and port management
draft: false
---
Tools for managing block parameters and inspecting input/output ports.
## `get_block_params`
Get all parameters for a block.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `block_name` | `str` | - | Name of the block |
### Returns
**Type:** `list[ParamModel]`
List of parameters with key, value, name, and type.
### Example
```python
params = get_block_params(block_name="osmosdr_source_0")
# Returns: [
# ParamModel(key="freq", value="101.1e6", name="Ch0: Frequency", dtype="real"),
# ParamModel(key="sample_rate", value="2e6", name="Sample Rate", dtype="real"),
# ...
# ]
```
---
## `set_block_params`
Set parameters on a block.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `block_name` | `str` | - | Name of the block |
| `params` | `dict[str, Any]` | - | Dict of parameter key → value |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
set_block_params(block_name="osmosdr_source_0", params={
"freq": "101.1e6",
"sample_rate": "2e6",
"gain": "40"
})
# Returns: True
```
---
## `get_block_sources`
Get output ports (sources) for a block.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `block_name` | `str` | - | Name of the block |
### Returns
**Type:** `list[PortModel]`
List of output ports with key, name, and data type.
### Example
```python
sources = get_block_sources(block_name="osmosdr_source_0")
# Returns: [
# PortModel(key="0", name="out", dtype="complex", direction="source")
# ]
```
---
## `get_block_sinks`
Get input ports (sinks) for a block.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `block_name` | `str` | - | Name of the block |
### Returns
**Type:** `list[PortModel]`
List of input ports with key, name, and data type.
### Example
```python
sinks = get_block_sinks(block_name="low_pass_filter_0")
# Returns: [
# PortModel(key="0", name="in", dtype="complex", direction="sink")
# ]
```
---
## `bypass_block`
Bypass a block (pass signal through without processing).
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `block_name` | `str` | - | Name of the block to bypass |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
bypass_block(block_name="low_pass_filter_0")
# Returns: True
```
---
## `unbypass_block`
Re-enable a bypassed block.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `block_name` | `str` | - | Name of the block to unbypass |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
unbypass_block(block_name="low_pass_filter_0")
# Returns: True
```

View File

@ -0,0 +1,147 @@
---
title: Code Generation
description: Tools for generating Python code and evaluating expressions
draft: false
---
Tools for generating executable Python code from flowgraphs.
## `generate_code`
Generate Python/C++ code from the current flowgraph.
Unlike the `grcc` command-line tool, this does **not** block on validation errors.
Validation warnings are included in the response for reference.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `output_dir` | `str` | `""` | Output directory (defaults to temp directory) |
### Returns
**Type:** `GeneratedCodeModel`
Generated code result with path, validity, and warnings.
### Example
```python
result = generate_code(output_dir="/tmp")
# Returns: GeneratedCodeModel(
# file_path="/tmp/fm_receiver.py",
# is_valid=True,
# warnings=[]
# )
# With validation warnings
result = generate_code()
# Returns: GeneratedCodeModel(
# file_path="/tmp/tmpXXXXXX/fm_receiver.py",
# is_valid=False,
# warnings=["audio_sink_0: Sample rate mismatch"]
# )
```
### Notes
- Generated code includes XML-RPC server if the flowgraph has XML-RPC variables
- Output type (Python/C++) depends on `generate_options` in flowgraph options
- File name is derived from flowgraph title
---
## `evaluate_expression`
Evaluate a Python expression in the flowgraph's namespace.
Useful for testing parameter expressions before setting them.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `expr` | `str` | - | Python expression to evaluate |
### Returns
**Type:** `Any`
Result of the expression evaluation.
### Example
```python
# Arithmetic
result = evaluate_expression("101.1e6 + 200e3")
# Returns: 101300000.0
# Using numpy (available in flowgraph namespace)
result = evaluate_expression("np.pi * 2")
# Returns: 6.283185307179586
# Complex expressions
result = evaluate_expression("int(2e6 / 10)")
# Returns: 200000
```
---
## `create_embedded_python_block`
Create an embedded Python block from source code.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `source_code` | `str` | - | Python source code for the block |
| `block_name` | `str \| None` | `None` | Optional custom block name |
### Returns
**Type:** `str`
The assigned block name.
### Example
```python
source = '''
import numpy as np
from gnuradio import gr
class my_multiplier(gr.sync_block):
"""Multiply input by a constant."""
def __init__(self, multiplier=2.0):
gr.sync_block.__init__(
self,
name="my_multiplier",
in_sig=[np.complex64],
out_sig=[np.complex64]
)
self.multiplier = multiplier
def work(self, input_items, output_items):
output_items[0][:] = input_items[0] * self.multiplier
return len(output_items[0])
'''
name = create_embedded_python_block(source_code=source)
# Returns: "epy_block_0"
# With custom name
name = create_embedded_python_block(
source_code=source,
block_name="multiplier"
)
# Returns: "multiplier_0"
```
### Notes
- Embedded blocks must inherit from `gr.sync_block`, `gr.decim_block`, or `gr.interp_block`
- The block class must define `__init__` and `work` methods
- Use `set_block_params()` to configure the block after creation

View File

@ -0,0 +1,113 @@
---
title: Connection Tools
description: Tools for wiring blocks together
draft: false
---
Tools for managing connections (wires) between blocks.
## `get_connections`
List all connections in the flowgraph.
### Returns
**Type:** `list[ConnectionModel]`
List of connections with source/sink block names and port keys.
### Example
```python
connections = get_connections()
# Returns: [
# ConnectionModel(
# source_block="osmosdr_source_0", source_port="0",
# sink_block="low_pass_filter_0", sink_port="0"
# ),
# ...
# ]
```
---
## `connect_blocks`
Connect two blocks by their ports.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `source_block_name` | `str` | - | Name of source block |
| `sink_block_name` | `str` | - | Name of sink block |
| `source_port_name` | `str` | - | Port key on source block |
| `sink_port_name` | `str` | - | Port key on sink block |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
connect_blocks(
source_block_name="osmosdr_source_0",
sink_block_name="low_pass_filter_0",
source_port_name="0",
sink_port_name="0"
)
# Returns: True
```
### Notes
- Use `get_block_sources()` and `get_block_sinks()` to discover available ports
- Most blocks use port `"0"` for their primary input/output
- Port types must be compatible (e.g., both complex, both float)
---
## `disconnect_blocks`
Disconnect two blocks.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `source_port` | `PortModel` | - | Source port to disconnect |
| `sink_port` | `PortModel` | - | Sink port to disconnect |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
# Get the connection to disconnect
connections = get_connections()
conn = connections[0]
disconnect_blocks(
source_port=PortModel(
parent=conn.source_block,
key=conn.source_port,
name="out",
dtype="complex",
direction="source"
),
sink_port=PortModel(
parent=conn.sink_block,
key=conn.sink_port,
name="in",
dtype="complex",
direction="sink"
)
)
```

View File

@ -0,0 +1,268 @@
---
title: ControlPort Tools
description: Tools for ControlPort/Thrift connection and monitoring
draft: false
---
import { Aside } from '@astrojs/starlight/components';
Tools for advanced runtime control via ControlPort (Thrift protocol).
<Aside type="caution">
These tools require `enable_runtime_mode()` to be called first.
</Aside>
## `connect_controlport`
Connect to a GNU Radio ControlPort/Thrift endpoint.
ControlPort provides richer functionality than XML-RPC:
- Native type support (complex numbers, vectors)
- Performance counters (throughput, timing, buffer utilization)
- Knob metadata (units, min/max, descriptions)
- PMT message injection
- Regex-based knob queries
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `host` | `str` | `"127.0.0.1"` | Hostname or IP address |
| `port` | `int` | `9090` | ControlPort Thrift port |
### Returns
**Type:** `ThriftConnectionInfoModel`
Connection details.
### Example
```python
connect_controlport(host="127.0.0.1", port=9090)
# Returns: ThriftConnectionInfoModel(
# host="127.0.0.1",
# port=9090,
# container_name=None
# )
```
---
## `connect_to_container_controlport`
Connect to a flowgraph's ControlPort by container name.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str` | - | Container name |
### Returns
**Type:** `ThriftConnectionInfoModel`
Connection details.
### Example
```python
connect_to_container_controlport(name="fm-profiled")
```
<Aside>
The container must have been launched with `enable_controlport=True`.
</Aside>
---
## `disconnect_controlport`
Disconnect from the current ControlPort endpoint.
### Returns
**Type:** `bool`
True if successful.
### Example
```python
disconnect_controlport()
# Returns: True
```
---
## `get_knobs`
Get ControlPort knobs, optionally filtered by regex pattern.
Knobs are named using the pattern: `block_alias::varname`
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `pattern` | `str` | `""` | Regex pattern for filtering (empty = all) |
### Returns
**Type:** `list[KnobModel]`
List of knobs with name, value, and type.
### Example
```python
# Get all knobs
knobs = get_knobs(pattern="")
# Filter by pattern
knobs = get_knobs(pattern=".*frequency.*")
# Returns: [KnobModel(name="sig_source_0::frequency", value=1000.0, ...)]
# Get knobs for a specific block
knobs = get_knobs(pattern="osmosdr_source_0::.*")
```
---
## `set_knobs`
Set multiple ControlPort knobs atomically.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `knobs` | `dict[str, Any]` | - | Dict mapping knob names to new values |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
set_knobs({
"sig_source_0::frequency": 1500.0,
"sig_source_0::amplitude": 0.8
})
# Returns: True
```
---
## `get_knob_properties`
Get metadata (units, min/max, description) for specified knobs.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `names` | `list[str]` | - | Knob names to query (empty = all) |
### Returns
**Type:** `list[KnobPropertiesModel]`
Knob metadata.
### Example
```python
props = get_knob_properties(names=["sig_source_0::frequency"])
# Returns: [KnobPropertiesModel(
# name="sig_source_0::frequency",
# units="Hz",
# min=0.0,
# max=1e9,
# description="Output signal frequency"
# )]
```
---
## `get_performance_counters`
Get performance metrics for blocks via ControlPort.
Requires the flowgraph to be launched with `enable_controlport=True` and
`enable_perf_counters=True` (default).
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `block` | `str \| None` | `None` | Block alias to filter (None = all blocks) |
### Returns
**Type:** `list[PerfCounterModel]`
Performance metrics.
### Example
```python
counters = get_performance_counters()
# Returns: [PerfCounterModel(
# block="low_pass_filter_0",
# nproduced=1048576,
# nconsumed=10485760,
# avg_work_time_us=45.3,
# avg_throughput=23.2e6,
# pc_input_buffers_full=0.85,
# pc_output_buffers_full=0.12
# ), ...]
# Filter by block
counters = get_performance_counters(block="low_pass_filter_0")
```
### Metrics
| Metric | Description |
|--------|-------------|
| `nproduced` | Items produced by block |
| `nconsumed` | Items consumed by block |
| `avg_work_time_us` | Average time in `work()` function |
| `avg_throughput` | Items per second |
| `pc_input_buffers_full` | Input buffer utilization (0-1) |
| `pc_output_buffers_full` | Output buffer utilization (0-1) |
---
## `post_message`
Send a PMT message to a block's message port via ControlPort.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `block` | `str` | - | Block alias (e.g., "msg_sink0") |
| `port` | `str` | - | Message port name (e.g., "in") |
| `message` | `Any` | - | Message to send (converted to PMT if needed) |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
# Send a string message
post_message(block="pdu_sink_0", port="pdus", message="hello")
# Send a dict (converted to PMT dict)
post_message(block="command_handler_0", port="command", message={"freq": 1e6})
```

View File

@ -0,0 +1,158 @@
---
title: Coverage Tools
description: Tools for collecting Python code coverage
draft: false
---
import { Aside } from '@astrojs/starlight/components';
Tools for collecting Python code coverage from containerized flowgraphs.
<Aside type="caution">
These tools require Docker and `enable_runtime_mode()` to be called first.
</Aside>
## `collect_coverage`
Collect coverage data from a stopped container.
Combines any parallel coverage files and returns a summary.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str` | - | Container name |
### Returns
**Type:** `CoverageDataModel`
Coverage summary.
### Example
```python
data = collect_coverage(name="coverage-test")
# Returns: CoverageDataModel(
# container_name="coverage-test",
# coverage_file="/tmp/gr-mcp-coverage/coverage-test/.coverage",
# summary="Name Stmts Miss Cover\n...",
# lines_covered=150,
# lines_total=200,
# coverage_percent=75.0
# )
```
<Aside>
The container must have been stopped (not force-removed) for coverage data
to be available. Use `stop_flowgraph()` for clean shutdown.
</Aside>
---
## `generate_coverage_report`
Generate a coverage report in the specified format.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str` | - | Container name |
| `format` | `str` | `"html"` | Report format: `html`, `xml`, or `json` |
### Returns
**Type:** `CoverageReportModel`
Report location.
### Example
```python
# HTML report
report = generate_coverage_report(name="coverage-test", format="html")
# Returns: CoverageReportModel(
# container_name="coverage-test",
# format="html",
# report_path="/tmp/gr-mcp-coverage/coverage-test/htmlcov/index.html"
# )
# XML report (Cobertura format)
report = generate_coverage_report(name="coverage-test", format="xml")
# report_path="/tmp/gr-mcp-coverage/coverage-test/coverage.xml"
# JSON report
report = generate_coverage_report(name="coverage-test", format="json")
# report_path="/tmp/gr-mcp-coverage/coverage-test/coverage.json"
```
---
## `combine_coverage`
Combine coverage data from multiple containers.
Useful for aggregating coverage across a test suite.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `names` | `list[str]` | - | Container names to combine |
### Returns
**Type:** `CoverageDataModel`
Combined coverage summary.
### Example
```python
combined = combine_coverage(names=["test-1", "test-2", "test-3"])
# Returns: CoverageDataModel(
# container_name="combined",
# coverage_file="/tmp/gr-mcp-coverage/combined/.coverage",
# summary="...",
# lines_covered=250,
# lines_total=300,
# coverage_percent=83.3
# )
```
---
## `delete_coverage`
Delete coverage data.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str \| None` | `None` | Delete specific container's coverage |
| `older_than_days` | `int \| None` | `None` | Delete coverage older than N days |
### Returns
**Type:** `int`
Number of coverage directories deleted.
### Example
```python
# Delete specific container's coverage
delete_coverage(name="coverage-test")
# Returns: 1
# Delete old coverage
delete_coverage(older_than_days=7)
# Returns: 5
# Delete all coverage
delete_coverage()
# Returns: 10
```

View File

@ -0,0 +1,212 @@
---
title: Docker Tools
description: Tools for container lifecycle management
draft: false
---
import { Aside } from '@astrojs/starlight/components';
Tools for launching and managing flowgraphs in Docker containers.
<Aside type="caution">
These tools require Docker and are only available after `enable_runtime_mode()`.
</Aside>
## `launch_flowgraph`
Launch a flowgraph in a Docker container with Xvfb (headless X11).
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `flowgraph_path` | `str` | - | Path to the `.py` flowgraph file |
| `name` | `str \| None` | `None` | Container name (defaults to `gr-{stem}`) |
| `xmlrpc_port` | `int` | `0` | XML-RPC port (0 = auto-allocate) |
| `enable_vnc` | `bool` | `False` | Enable VNC server for visual debugging |
| `enable_coverage` | `bool` | `False` | Enable Python code coverage collection |
| `enable_controlport` | `bool` | `False` | Enable ControlPort/Thrift |
| `controlport_port` | `int` | `9090` | ControlPort port |
| `enable_perf_counters` | `bool` | `True` | Enable performance counters |
| `device_paths` | `list[str] \| None` | `None` | Host device paths to pass through |
| `image` | `str \| None` | `None` | Docker image to use |
| `auto_image` | `bool` | `False` | Auto-detect OOT modules and build image |
### Returns
**Type:** `ContainerModel`
Container information including name, status, and ports.
### Example
```python
# Basic launch
container = launch_flowgraph(
flowgraph_path="/tmp/fm_receiver.py",
name="fm-radio"
)
# With XML-RPC and VNC
container = launch_flowgraph(
flowgraph_path="/tmp/fm_receiver.py",
name="fm-radio",
xmlrpc_port=8080,
enable_vnc=True
)
# With SDR hardware access
container = launch_flowgraph(
flowgraph_path="/tmp/fm_receiver.py",
name="fm-hardware",
device_paths=["/dev/bus/usb"]
)
# Auto-detect OOT modules
container = launch_flowgraph(
flowgraph_path="/tmp/lora_rx.py",
auto_image=True
)
```
---
## `list_containers`
List all GR-MCP managed containers.
### Returns
**Type:** `list[ContainerModel]`
List of containers with status and port information.
### Example
```python
containers = list_containers()
# Returns: [
# ContainerModel(
# name="fm-radio",
# status="running",
# xmlrpc_port=32768,
# vnc_port=5900,
# ...
# ),
# ...
# ]
```
---
## `stop_flowgraph`
Stop a running flowgraph container (graceful shutdown).
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str` | - | Container name |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
stop_flowgraph(name="fm-radio")
# Returns: True
```
<Aside>
Use `stop_flowgraph()` before collecting coverage data. Coverage is only
written when the process exits cleanly.
</Aside>
---
## `remove_flowgraph`
Remove a flowgraph container.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str` | - | Container name |
| `force` | `bool` | `False` | Force remove even if running |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
# Normal removal (must be stopped first)
remove_flowgraph(name="fm-radio")
# Force removal
remove_flowgraph(name="fm-radio", force=True)
```
---
## `capture_screenshot`
Capture a screenshot of the flowgraph's QT GUI.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str \| None` | `None` | Container name (uses active if not specified) |
### Returns
**Type:** `ScreenshotModel`
Screenshot information with path and dimensions.
### Example
```python
screenshot = capture_screenshot(name="fm-radio")
# Returns: ScreenshotModel(
# path="/tmp/gr-mcp-screenshots/fm-radio-2024-01-15-14-30-00.png",
# width=1024,
# height=768
# )
```
---
## `get_container_logs`
Get logs from a flowgraph container.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str \| None` | `None` | Container name (uses active if not specified) |
| `tail` | `int` | `100` | Number of lines to return |
### Returns
**Type:** `str`
Container stdout/stderr output.
### Example
```python
logs = get_container_logs(name="fm-radio", tail=50)
# Returns: "Using Volk machine: avx2_64_mmx\nStarting flowgraph...\n..."
```

View File

@ -0,0 +1,228 @@
---
title: Flowgraph Tools
description: Tools for managing flowgraph structure
draft: false
---
Tools for managing flowgraph structure: blocks, connections, save/load, and metadata.
## `get_blocks`
List all blocks currently in the flowgraph.
### Returns
**Type:** `list[BlockModel]`
List of blocks with their names, keys, and states.
### Example
```python
blocks = get_blocks()
# Returns: [
# BlockModel(name="osmosdr_source_0", key="osmosdr_source", ...),
# BlockModel(name="low_pass_filter_0", key="low_pass_filter", ...),
# ]
```
---
## `make_block`
Create a new block in the flowgraph.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `block_name` | `str` | - | Block type key (e.g., "osmosdr_source") |
### Returns
**Type:** `str`
The unique name assigned to the block (e.g., "osmosdr_source_0").
### Example
```python
name = make_block(block_name="osmosdr_source")
# Returns: "osmosdr_source_0"
```
---
## `remove_block`
Remove a block from the flowgraph.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `block_name` | `str` | - | Name of the block to remove |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
remove_block(block_name="osmosdr_source_0")
# Returns: True
```
---
## `save_flowgraph`
Save the flowgraph to a `.grc` file.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `filepath` | `str` | - | Path to save the `.grc` file |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
save_flowgraph(filepath="/tmp/my_flowgraph.grc")
# Returns: True
```
---
## `load_flowgraph`
Load a flowgraph from a `.grc` file, replacing the current flowgraph.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `filepath` | `str` | - | Path to the `.grc` file |
### Returns
**Type:** `list[BlockModel]`
List of blocks in the loaded flowgraph.
### Example
```python
blocks = load_flowgraph(filepath="/tmp/my_flowgraph.grc")
# Returns: [BlockModel(...), ...]
```
---
## `get_flowgraph_options`
Get the flowgraph-level options (title, author, generate_options, etc.).
### Returns
**Type:** `FlowgraphOptionsModel`
Flowgraph metadata including title, author, category, description, and generate options.
### Example
```python
options = get_flowgraph_options()
# Returns: FlowgraphOptionsModel(
# title="FM Receiver",
# author="Your Name",
# generate_options="qt_gui",
# ...
# )
```
---
## `set_flowgraph_options`
Set flowgraph-level options on the 'options' block.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `params` | `dict[str, Any]` | - | Options to set |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
set_flowgraph_options(params={
"title": "FM Receiver",
"author": "Your Name",
"generate_options": "qt_gui"
})
# Returns: True
```
---
## `export_flowgraph_data`
Export the flowgraph as a nested dict (same format as `.grc` files).
### Returns
**Type:** `dict`
Complete flowgraph data in GRC YAML format.
### Example
```python
data = export_flowgraph_data()
# Returns: {"options": {...}, "blocks": [...], "connections": [...]}
```
---
## `import_flowgraph_data`
Import flowgraph data from a dict, replacing current contents.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `data` | `dict` | - | Flowgraph data in GRC format |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
import_flowgraph_data(data={
"options": {"title": "Test"},
"blocks": [...],
"connections": [...]
})
# Returns: True
```

View File

@ -0,0 +1,238 @@
---
title: OOT Tools
description: Tools for OOT module detection and installation
draft: false
---
import { Aside } from '@astrojs/starlight/components';
Tools for detecting, installing, and managing Out-of-Tree (OOT) modules.
<Aside type="caution">
These tools require Docker and `enable_runtime_mode()` to be called first.
</Aside>
## `detect_oot_modules`
Detect which OOT modules a flowgraph requires.
Analyzes `.py` or `.grc` files to find OOT module dependencies.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `flowgraph_path` | `str` | - | Path to a `.py` or `.grc` flowgraph file |
### Returns
**Type:** `OOTDetectionResult`
Detection results with modules and recommended image.
### Example
```python
result = detect_oot_modules(flowgraph_path="lora_rx.py")
# Returns: OOTDetectionResult(
# detected_modules=["lora_sdr"],
# unknown_blocks=[],
# recommended_image="gnuradio-lora_sdr-runtime:latest"
# )
# Multiple modules
result = detect_oot_modules(flowgraph_path="multi_protocol_rx.py")
# Returns: OOTDetectionResult(
# detected_modules=["lora_sdr", "adsb"],
# unknown_blocks=[],
# recommended_image="gr-combo-adsb-lora_sdr:latest"
# )
```
---
## `install_oot_module`
Install an OOT module into a Docker image.
Clones the git repo, compiles with cmake, and creates a reusable Docker image.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `git_url` | `str` | - | Git repository URL |
| `branch` | `str` | `"main"` | Git branch to build from |
| `build_deps` | `list[str] \| None` | `None` | Extra apt packages for compilation |
| `cmake_args` | `list[str] \| None` | `None` | Extra cmake flags |
| `base_image` | `str \| None` | `None` | Base image (default: gnuradio-runtime:latest) |
| `force` | `bool` | `False` | Rebuild even if image exists |
### Returns
**Type:** `OOTInstallResult`
Installation result with image tag.
### Example
```python
# Basic install
result = install_oot_module(
git_url="https://github.com/tapparelj/gr-lora_sdr",
branch="master"
)
# Returns: OOTInstallResult(
# success=True,
# module_name="lora_sdr",
# image_tag="gnuradio-lora_sdr-runtime:latest",
# error=None
# )
# With build dependencies
result = install_oot_module(
git_url="https://github.com/hboeglen/gr-dab",
branch="maint-3.10",
build_deps=["autoconf", "automake", "libtool", "libfaad-dev"],
cmake_args=["-DENABLE_DOXYGEN=OFF"]
)
```
---
## `list_oot_images`
List all installed OOT module images.
### Returns
**Type:** `list[OOTImageInfo]`
List of installed images.
### Example
```python
images = list_oot_images()
# Returns: [
# OOTImageInfo(
# module_name="lora_sdr",
# image_tag="gnuradio-lora_sdr-runtime:latest",
# git_url="https://github.com/tapparelj/gr-lora_sdr",
# branch="master"
# ),
# ...
# ]
```
---
## `remove_oot_image`
Remove an OOT module image and its registry entry.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `module_name` | `str` | - | Module name to remove |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
remove_oot_image(module_name="lora_sdr")
# Returns: True
```
---
## `build_multi_oot_image`
Combine multiple OOT modules into a single Docker image.
Missing modules that exist in the catalog are auto-built first.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `module_names` | `list[str]` | - | Module names to combine |
| `force` | `bool` | `False` | Rebuild even if exists |
### Returns
**Type:** `ComboImageResult`
Combo image details.
### Example
```python
result = build_multi_oot_image(module_names=["lora_sdr", "adsb"])
# Returns: ComboImageResult(
# success=True,
# image=ComboImageInfo(
# combo_key="combo:adsb+lora_sdr",
# image_tag="gr-combo-adsb-lora_sdr:latest",
# modules=["adsb", "lora_sdr"]
# ),
# error=None
# )
```
---
## `list_combo_images`
List all combined multi-OOT images.
### Returns
**Type:** `list[ComboImageInfo]`
List of combo images.
### Example
```python
combos = list_combo_images()
# Returns: [
# ComboImageInfo(
# combo_key="combo:adsb+lora_sdr",
# image_tag="gr-combo-adsb-lora_sdr:latest",
# modules=["adsb", "lora_sdr"]
# ),
# ...
# ]
```
---
## `remove_combo_image`
Remove a combined image by its combo key.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `combo_key` | `str` | - | Combo key (e.g., "combo:adsb+lora_sdr") |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
remove_combo_image(combo_key="combo:adsb+lora_sdr")
# Returns: True
```

View File

@ -0,0 +1,173 @@
---
title: Platform Tools
description: Tools for discovering blocks and managing paths
draft: false
---
Tools for discovering available blocks and managing OOT block paths.
## `get_all_available_blocks`
List all block types available on the platform.
### Returns
**Type:** `list[BlockTypeModel]`
List of block types with key and category.
### Example
```python
blocks = get_all_available_blocks()
# Returns: [
# BlockTypeModel(key="osmosdr_source", category="OsmoSDR"),
# BlockTypeModel(key="analog_sig_source_x", category="Waveform Generators"),
# ...
# ]
```
---
## `search_blocks`
Search available blocks by keyword and/or category.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `query` | `str` | `""` | Search keyword |
| `category` | `str \| None` | `None` | Filter by category |
### Returns
**Type:** `list[BlockTypeDetailModel]`
Matching blocks with full details.
### Example
```python
# Search by keyword
blocks = search_blocks(query="filter")
# Returns: [BlockTypeDetailModel(key="low_pass_filter", ...), ...]
# Search by category
blocks = search_blocks(category="Audio")
# Returns: [BlockTypeDetailModel(key="audio_sink", ...), ...]
# Combined search
blocks = search_blocks(query="sink", category="Audio")
```
---
## `get_block_categories`
Get all block categories with their block keys.
### Returns
**Type:** `dict[str, list[str]]`
Dict mapping category name to list of block keys.
### Example
```python
categories = get_block_categories()
# Returns: {
# "Audio": ["audio_sink", "audio_source"],
# "OsmoSDR": ["osmosdr_source", "osmosdr_sink"],
# "Filters": ["low_pass_filter", "high_pass_filter", ...],
# ...
# }
```
---
## `load_oot_blocks`
Load OOT (Out-of-Tree) block paths into the platform.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `paths` | `list[str]` | - | Directories containing `.block.yml` files |
### Returns
**Type:** `dict`
Result with added_paths, invalid_paths, and block counts.
### Example
```python
result = load_oot_blocks(paths=[
"/usr/local/share/gnuradio/grc/blocks",
"/home/user/gr-modules/lib/grc"
])
# Returns: {
# "added_paths": ["/usr/local/share/gnuradio/grc/blocks"],
# "invalid_paths": ["/home/user/gr-modules/lib/grc"],
# "blocks_before": 200,
# "blocks_after": 215
# }
```
---
## `add_block_path`
Add a single directory containing OOT module block YAML files.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `path` | `str` | - | Directory path |
### Returns
**Type:** `BlockPathsModel`
Updated paths and block count.
### Example
```python
result = add_block_path(path="/usr/local/share/gnuradio/grc/blocks")
# Returns: BlockPathsModel(
# paths=[...],
# total_blocks=215,
# blocks_added=15
# )
```
---
## `get_block_paths`
Show current OOT block paths and total block count.
### Returns
**Type:** `BlockPathsModel`
Current paths configuration.
### Example
```python
paths = get_block_paths()
# Returns: BlockPathsModel(
# paths=[
# "/usr/share/gnuradio/grc/blocks",
# "/usr/local/share/gnuradio/grc/blocks"
# ],
# total_blocks=215
# )
```

View File

@ -0,0 +1,170 @@
---
title: Runtime Mode
description: Tools for enabling runtime features and checking capabilities
draft: false
---
import { Aside } from '@astrojs/starlight/components';
Tools for managing runtime mode and inspecting MCP client capabilities.
<Aside>
Runtime mode tools are **always available**. They control whether the larger set
of Docker/XML-RPC/ControlPort tools are registered.
</Aside>
## `get_runtime_mode`
Check if runtime mode is enabled and what capabilities are available.
### Returns
**Type:** `RuntimeModeStatus`
Status including enabled state, registered tools, and availability.
### Example
```python
status = get_runtime_mode()
# Returns: RuntimeModeStatus(
# enabled=False,
# tools_registered=[],
# docker_available=True,
# oot_available=True
# )
```
---
## `enable_runtime_mode`
Enable runtime mode, registering all runtime control tools.
This adds tools for:
- XML-RPC connection and variable control
- ControlPort/Thrift for performance monitoring
- Docker container lifecycle (if Docker available)
- OOT module installation (if Docker available)
### Returns
**Type:** `RuntimeModeStatus`
Updated status with newly registered tools.
### Example
```python
status = enable_runtime_mode()
# Returns: RuntimeModeStatus(
# enabled=True,
# tools_registered=[
# "launch_flowgraph", "list_containers", "stop_flowgraph",
# "connect", "list_variables", "get_variable", "set_variable",
# ...
# ],
# docker_available=True,
# oot_available=True
# )
```
### Notes
- Calling when already enabled is a no-op (returns current status)
- If Docker is unavailable, container-related tools are not registered
- If Docker is unavailable, OOT tools are not registered
---
## `disable_runtime_mode`
Disable runtime mode, removing runtime tools to reduce context.
Use this when you're done with runtime operations and want to reduce the tool list
for flowgraph design work.
### Returns
**Type:** `RuntimeModeStatus`
Updated status showing disabled state.
### Example
```python
status = disable_runtime_mode()
# Returns: RuntimeModeStatus(
# enabled=False,
# tools_registered=[],
# docker_available=True,
# oot_available=True
# )
```
---
## `get_client_capabilities`
Get the connected MCP client's capabilities.
Useful for debugging MCP connections and understanding what features the client supports.
### Returns
**Type:** `ClientCapabilities`
Client information and capability flags.
### Example
```python
caps = get_client_capabilities()
# Returns: ClientCapabilities(
# client_name="claude-code",
# client_version="2.1.15",
# protocol_version="2024-11-05",
# roots=RootsCapability(supported=True, list_changed=True),
# sampling=SamplingCapability(supported=True, tools=True, context=False),
# elicitation=ElicitationCapability(supported=False, form=False, url=False),
# raw_capabilities={"roots": {"listChanged": True}, ...},
# experimental={}
# )
```
### Capability Explanations
| Capability | Description |
|------------|-------------|
| `roots` | Client exposes workspace directories |
| `sampling` | Server can request LLM completions |
| `elicitation` | Server can prompt user for input |
---
## `list_client_roots`
List the root directories advertised by the MCP client.
Roots represent project directories or workspaces the client wants the server to be aware of.
### Returns
**Type:** `list[ClientRoot]`
List of root directories.
### Example
```python
roots = list_client_roots()
# Returns: [
# ClientRoot(uri="file:///home/user/project", name="project"),
# ClientRoot(uri="file:///home/user/gr-mcp", name="gr-mcp"),
# ]
```
### Notes
- Returns empty list if roots capability is not supported
- Typically includes the current working directory

View File

@ -0,0 +1,98 @@
---
title: Validation Tools
description: Tools for checking flowgraph validity
draft: false
---
Tools for validating blocks and flowgraphs.
## `validate_block`
Validate a single block's configuration.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `block_name` | `str` | - | Name of the block to validate |
### Returns
**Type:** `bool`
True if the block is valid.
### Example
```python
is_valid = validate_block(block_name="osmosdr_source_0")
# Returns: True
```
---
## `validate_flowgraph`
Validate the entire flowgraph.
Checks all blocks and connections for errors.
### Returns
**Type:** `bool`
True if the flowgraph is valid.
### Example
```python
is_valid = validate_flowgraph()
# Returns: True
```
### Notes
- Validation checks parameter types, required connections, and compatibility
- Use `get_all_errors()` to see specific validation failures
- `generate_code()` does not require validation to pass
---
## `get_all_errors`
Get all validation errors from the flowgraph.
### Returns
**Type:** `list[ErrorModel]`
List of errors with block name, parameter, and message.
### Example
```python
errors = get_all_errors()
# Returns: [
# ErrorModel(
# block="audio_sink_0",
# param="samp_rate",
# message="Sample rate 50000 not supported by audio device"
# ),
# ...
# ]
# Empty list means no errors
errors = get_all_errors()
# Returns: []
```
### Error Types
Common validation errors:
| Error Type | Example |
|------------|---------|
| Missing connection | "Block has unconnected input port" |
| Type mismatch | "Cannot connect complex to float" |
| Invalid parameter | "Invalid sample rate value" |
| Missing dependency | "Block requires gr-osmosdr" |

View File

@ -0,0 +1,263 @@
---
title: XML-RPC Tools
description: Tools for XML-RPC connection and variable control
draft: false
---
import { Aside } from '@astrojs/starlight/components';
Tools for connecting to running flowgraphs via XML-RPC and controlling variables.
<Aside type="caution">
These tools require `enable_runtime_mode()` to be called first.
</Aside>
## `connect`
Connect to a GNU Radio XML-RPC endpoint by URL.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `url` | `str` | - | XML-RPC URL (e.g., `http://localhost:8080`) |
### Returns
**Type:** `ConnectionInfoModel`
Connection details including URL and port.
### Example
```python
connect(url="http://localhost:8080")
# Returns: ConnectionInfoModel(
# url="http://localhost:8080",
# xmlrpc_port=8080,
# container_name=None
# )
```
---
## `connect_to_container`
Connect to a flowgraph by container name (resolves port automatically).
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str` | - | Container name |
### Returns
**Type:** `ConnectionInfoModel`
Connection details.
### Example
```python
connect_to_container(name="fm-radio")
# Returns: ConnectionInfoModel(
# url="http://localhost:32768",
# xmlrpc_port=32768,
# container_name="fm-radio"
# )
```
---
## `disconnect`
Disconnect from the current XML-RPC (and ControlPort) endpoint.
### Returns
**Type:** `bool`
True if successful.
### Example
```python
disconnect()
# Returns: True
```
---
## `get_status`
Get runtime status including connection and container info.
### Returns
**Type:** `RuntimeStatusModel`
Current runtime state.
### Example
```python
status = get_status()
# Returns: RuntimeStatusModel(
# connected=True,
# connection=ConnectionInfoModel(...),
# containers=[ContainerModel(...), ...]
# )
```
---
## `list_variables`
List all XML-RPC-exposed variables.
### Returns
**Type:** `list[VariableModel]`
List of variables with name, value, and type.
### Example
```python
vars = list_variables()
# Returns: [
# VariableModel(name="freq", value=101100000.0, type="float"),
# VariableModel(name="gain", value=40, type="int"),
# VariableModel(name="samp_rate", value=2000000.0, type="float"),
# ]
```
---
## `get_variable`
Get a variable value.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str` | - | Variable name |
### Returns
**Type:** `Any`
Current variable value.
### Example
```python
freq = get_variable(name="freq")
# Returns: 101100000.0
```
---
## `set_variable`
Set a variable value.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str` | - | Variable name |
| `value` | `Any` | - | New value |
### Returns
**Type:** `bool`
True if successful.
### Example
```python
set_variable(name="freq", value=98.5e6)
# Returns: True
```
---
## `start`
Start the connected flowgraph.
### Returns
**Type:** `bool`
True if successful.
### Example
```python
start()
# Returns: True
```
---
## `stop`
Stop the connected flowgraph.
### Returns
**Type:** `bool`
True if successful.
### Example
```python
stop()
# Returns: True
```
---
## `lock`
Lock the flowgraph for thread-safe parameter updates.
### Returns
**Type:** `bool`
True if successful.
### Example
```python
lock()
set_variable(name="freq", value=102.7e6)
set_variable(name="gain", value=35)
unlock()
```
---
## `unlock`
Unlock the flowgraph after parameter updates.
### Returns
**Type:** `bool`
True if successful.
### Example
```python
unlock()
# Returns: True
```

View File

@ -0,0 +1,63 @@
/* GR-MCP Documentation Theme */
:root {
/* Brand colors - no purple, clean tech aesthetic */
--sl-color-accent-low: #1e3a5f;
--sl-color-accent: #0ea5e9;
--sl-color-accent-high: #7dd3fc;
/* Text colors */
--sl-color-white: #f8fafc;
--sl-color-gray-1: #e2e8f0;
--sl-color-gray-2: #cbd5e1;
--sl-color-gray-3: #94a3b8;
--sl-color-gray-4: #64748b;
--sl-color-gray-5: #475569;
--sl-color-gray-6: #1e293b;
--sl-color-black: #0f172a;
}
/* Dark mode refinements */
:root[data-theme='dark'] {
--sl-color-bg: #0f172a;
--sl-color-bg-nav: #1e293b;
--sl-color-bg-sidebar: #1e293b;
}
/* Code blocks - terminal aesthetic */
.expressive-code {
--ec-brdRad: 6px;
}
/* Headings - clean and readable */
h1, h2, h3, h4 {
font-weight: 600;
letter-spacing: -0.01em;
}
/* Links in prose */
.sl-markdown-content a:not(.card-grid a) {
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
/* Tables - better contrast */
table {
width: 100%;
}
th {
background: var(--sl-color-gray-6);
font-weight: 600;
}
/* Callouts/asides - consistent with brand */
.starlight-aside--tip {
--sl-color-asides-text-accent: var(--sl-color-accent);
border-color: var(--sl-color-accent);
}
/* Hero section tweaks */
.hero {
padding-block: 3rem;
}

View File

@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Minimal flowgraph that uses blocks from both adsb and lora_sdr.
Verifies combo Docker images contain working blocks from multiple
OOT modules. Runs for 5 seconds then exits cleanly.
"""
import signal
import sys
import time
from xmlrpc.server import SimpleXMLRPCServer
import threading
from gnuradio import gr, blocks, analog
# Import OOT modules to verify they're available
from gnuradio import adsb as gr_adsb
from gnuradio import lora_sdr as gr_lora_sdr
class combo_test(gr.top_block):
def __init__(self):
gr.top_block.__init__(self, "Combo Test: ADS-B + LoRa SDR")
##################################################
# Variables
##################################################
self.samp_rate = samp_rate = 2e6
##################################################
# ADS-B: noise(float) -> throttle -> demod -> null
##################################################
self.noise_adsb = analog.noise_source_f(analog.GR_GAUSSIAN, 0.01, 0)
self.throttle_adsb = blocks.throttle(gr.sizeof_float, samp_rate, True)
self.adsb_demod = gr_adsb.demod(samp_rate)
self.null_adsb = blocks.null_sink(gr.sizeof_float)
self.connect(self.noise_adsb, self.throttle_adsb, self.adsb_demod, self.null_adsb)
##################################################
# LoRa: tx -> throttle -> null
##################################################
self.lora_tx = gr_lora_sdr.lora_sdr_lora_tx(
samp_rate=125000,
bw=125000,
sf=7,
impl_head=True,
cr=1,
has_crc=True,
ldro_mode=2,
frame_zero_padd=128,
)
self.throttle_lora = blocks.throttle(gr.sizeof_gr_complex, 125000, True)
self.null_lora = blocks.null_sink(gr.sizeof_gr_complex)
self.connect(self.lora_tx, self.throttle_lora, self.null_lora)
##################################################
# XML-RPC for runtime control
##################################################
self.xmlrpc_port = 8080
self.xmlrpc_server = SimpleXMLRPCServer(
("0.0.0.0", self.xmlrpc_port),
allow_none=True,
logRequests=False,
)
self.xmlrpc_server.register_instance(self)
threading.Thread(
target=self.xmlrpc_server.serve_forever,
daemon=True,
).start()
def main():
tb = combo_test()
print(f"[combo_test] Starting flowgraph with ADS-B + LoRa SDR blocks")
print(f"[combo_test] XML-RPC on port {tb.xmlrpc_port}")
print(f"[combo_test] ADS-B demod: {type(tb.adsb_demod).__name__}")
print(f"[combo_test] LoRa TX: {type(tb.lora_tx).__name__}")
sys.stdout.flush()
tb.start()
def sig_handler(sig, frame):
tb.stop()
tb.wait()
sys.exit(0)
signal.signal(signal.SIGTERM, sig_handler)
signal.signal(signal.SIGINT, sig_handler)
# Run for a bit then exit
time.sleep(5)
print("[combo_test] Flowgraph ran successfully for 5s, stopping")
sys.stdout.flush()
tb.stop()
tb.wait()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,540 @@
options:
parameters:
author: gr-mcp
catch_exceptions: 'True'
category: '[GRC Hier Blocks]'
cmake_opt: ''
comment: ''
copyright: ''
description: Scans US ISM 915MHz band for LoRa activity. Retune via XML-RPC.
gen_cmake: 'On'
gen_linking: dynamic
generate_options: no_gui
hier_block_src_path: '.:'
id: lora_channel_scanner
max_nouts: '0'
output_language: python
placement: (0,0)
qt_qss_theme: ''
realtime_scheduling: ''
run: 'True'
run_command: '{python} -u {filename}'
run_options: prompt
sizing_mode: fixed
thread_safe_setters: ''
title: LoRa Channel Scanner
window_size: (1000,1000)
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [8, 8]
rotation: 0
state: enabled
blocks:
- name: center_freq
id: variable
parameters:
comment: ''
value: 915e6
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: channel_index
id: variable
parameters:
comment: ''
value: '0'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_bw
id: variable
parameters:
comment: ''
value: '125000'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_cr
id: variable
parameters:
comment: ''
value: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_sf
id: variable
parameters:
comment: ''
value: '7'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: rf_gain
id: variable
parameters:
comment: ''
value: '40'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: samp_rate
id: variable
parameters:
comment: ''
value: '1000000'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: blocks_message_debug_0
id: blocks_message_debug
parameters:
affinity: ''
alias: ''
comment: ''
en_uvec: 'True'
log_level: info
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_rx_0
id: lora_rx
parameters:
affinity: ''
alias: ''
bw: lora_bw
comment: ''
cr: lora_cr
has_crc: 'True'
impl_head: 'False'
ldro: '2'
maxoutbuf: '0'
minoutbuf: '0'
pay_len: '255'
print_rx: '[True,True]'
samp_rate: int(samp_rate/2)
sf: lora_sf
soft_decoding: 'True'
sync_word: '[0x12]'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: low_pass_filter_0
id: low_pass_filter
parameters:
affinity: ''
alias: ''
beta: '6.76'
comment: ''
cutoff_freq: 200e3
decim: '2'
gain: '1'
interp: '1'
maxoutbuf: '0'
minoutbuf: '0'
samp_rate: samp_rate
type: fir_filter_ccf
width: 50e3
win: window.WIN_HAMMING
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: osmosdr_source_0
id: osmosdr_source
parameters:
affinity: ''
alias: ''
ant0: ''
ant1: ''
ant10: ''
ant11: ''
ant12: ''
ant13: ''
ant14: ''
ant15: ''
ant16: ''
ant17: ''
ant18: ''
ant19: ''
ant2: ''
ant20: ''
ant21: ''
ant22: ''
ant23: ''
ant24: ''
ant25: ''
ant26: ''
ant27: ''
ant28: ''
ant29: ''
ant3: ''
ant30: ''
ant31: ''
ant4: ''
ant5: ''
ant6: ''
ant7: ''
ant8: ''
ant9: ''
args: rtl=0
bb_gain0: '20'
bb_gain1: '20'
bb_gain10: '20'
bb_gain11: '20'
bb_gain12: '20'
bb_gain13: '20'
bb_gain14: '20'
bb_gain15: '20'
bb_gain16: '20'
bb_gain17: '20'
bb_gain18: '20'
bb_gain19: '20'
bb_gain2: '20'
bb_gain20: '20'
bb_gain21: '20'
bb_gain22: '20'
bb_gain23: '20'
bb_gain24: '20'
bb_gain25: '20'
bb_gain26: '20'
bb_gain27: '20'
bb_gain28: '20'
bb_gain29: '20'
bb_gain3: '20'
bb_gain30: '20'
bb_gain31: '20'
bb_gain4: '20'
bb_gain5: '20'
bb_gain6: '20'
bb_gain7: '20'
bb_gain8: '20'
bb_gain9: '20'
bw0: '0'
bw1: '0'
bw10: '0'
bw11: '0'
bw12: '0'
bw13: '0'
bw14: '0'
bw15: '0'
bw16: '0'
bw17: '0'
bw18: '0'
bw19: '0'
bw2: '0'
bw20: '0'
bw21: '0'
bw22: '0'
bw23: '0'
bw24: '0'
bw25: '0'
bw26: '0'
bw27: '0'
bw28: '0'
bw29: '0'
bw3: '0'
bw30: '0'
bw31: '0'
bw4: '0'
bw5: '0'
bw6: '0'
bw7: '0'
bw8: '0'
bw9: '0'
clock_source0: ''
clock_source1: ''
clock_source2: ''
clock_source3: ''
clock_source4: ''
clock_source5: ''
clock_source6: ''
clock_source7: ''
comment: ''
corr0: '0'
corr1: '0'
corr10: '0'
corr11: '0'
corr12: '0'
corr13: '0'
corr14: '0'
corr15: '0'
corr16: '0'
corr17: '0'
corr18: '0'
corr19: '0'
corr2: '0'
corr20: '0'
corr21: '0'
corr22: '0'
corr23: '0'
corr24: '0'
corr25: '0'
corr26: '0'
corr27: '0'
corr28: '0'
corr29: '0'
corr3: '0'
corr30: '0'
corr31: '0'
corr4: '0'
corr5: '0'
corr6: '0'
corr7: '0'
corr8: '0'
corr9: '0'
dc_offset_mode0: '2'
dc_offset_mode1: '0'
dc_offset_mode10: '0'
dc_offset_mode11: '0'
dc_offset_mode12: '0'
dc_offset_mode13: '0'
dc_offset_mode14: '0'
dc_offset_mode15: '0'
dc_offset_mode16: '0'
dc_offset_mode17: '0'
dc_offset_mode18: '0'
dc_offset_mode19: '0'
dc_offset_mode2: '0'
dc_offset_mode20: '0'
dc_offset_mode21: '0'
dc_offset_mode22: '0'
dc_offset_mode23: '0'
dc_offset_mode24: '0'
dc_offset_mode25: '0'
dc_offset_mode26: '0'
dc_offset_mode27: '0'
dc_offset_mode28: '0'
dc_offset_mode29: '0'
dc_offset_mode3: '0'
dc_offset_mode30: '0'
dc_offset_mode31: '0'
dc_offset_mode4: '0'
dc_offset_mode5: '0'
dc_offset_mode6: '0'
dc_offset_mode7: '0'
dc_offset_mode8: '0'
dc_offset_mode9: '0'
freq0: center_freq
freq1: 100e6
freq10: 100e6
freq11: 100e6
freq12: 100e6
freq13: 100e6
freq14: 100e6
freq15: 100e6
freq16: 100e6
freq17: 100e6
freq18: 100e6
freq19: 100e6
freq2: 100e6
freq20: 100e6
freq21: 100e6
freq22: 100e6
freq23: 100e6
freq24: 100e6
freq25: 100e6
freq26: 100e6
freq27: 100e6
freq28: 100e6
freq29: 100e6
freq3: 100e6
freq30: 100e6
freq31: 100e6
freq4: 100e6
freq5: 100e6
freq6: 100e6
freq7: 100e6
freq8: 100e6
freq9: 100e6
gain0: rf_gain
gain1: '10'
gain10: '10'
gain11: '10'
gain12: '10'
gain13: '10'
gain14: '10'
gain15: '10'
gain16: '10'
gain17: '10'
gain18: '10'
gain19: '10'
gain2: '10'
gain20: '10'
gain21: '10'
gain22: '10'
gain23: '10'
gain24: '10'
gain25: '10'
gain26: '10'
gain27: '10'
gain28: '10'
gain29: '10'
gain3: '10'
gain30: '10'
gain31: '10'
gain4: '10'
gain5: '10'
gain6: '10'
gain7: '10'
gain8: '10'
gain9: '10'
gain_mode0: 'False'
gain_mode1: 'False'
gain_mode10: 'False'
gain_mode11: 'False'
gain_mode12: 'False'
gain_mode13: 'False'
gain_mode14: 'False'
gain_mode15: 'False'
gain_mode16: 'False'
gain_mode17: 'False'
gain_mode18: 'False'
gain_mode19: 'False'
gain_mode2: 'False'
gain_mode20: 'False'
gain_mode21: 'False'
gain_mode22: 'False'
gain_mode23: 'False'
gain_mode24: 'False'
gain_mode25: 'False'
gain_mode26: 'False'
gain_mode27: 'False'
gain_mode28: 'False'
gain_mode29: 'False'
gain_mode3: 'False'
gain_mode30: 'False'
gain_mode31: 'False'
gain_mode4: 'False'
gain_mode5: 'False'
gain_mode6: 'False'
gain_mode7: 'False'
gain_mode8: 'False'
gain_mode9: 'False'
if_gain0: '20'
if_gain1: '20'
if_gain10: '20'
if_gain11: '20'
if_gain12: '20'
if_gain13: '20'
if_gain14: '20'
if_gain15: '20'
if_gain16: '20'
if_gain17: '20'
if_gain18: '20'
if_gain19: '20'
if_gain2: '20'
if_gain20: '20'
if_gain21: '20'
if_gain22: '20'
if_gain23: '20'
if_gain24: '20'
if_gain25: '20'
if_gain26: '20'
if_gain27: '20'
if_gain28: '20'
if_gain29: '20'
if_gain3: '20'
if_gain30: '20'
if_gain31: '20'
if_gain4: '20'
if_gain5: '20'
if_gain6: '20'
if_gain7: '20'
if_gain8: '20'
if_gain9: '20'
iq_balance_mode0: '2'
iq_balance_mode1: '0'
iq_balance_mode10: '0'
iq_balance_mode11: '0'
iq_balance_mode12: '0'
iq_balance_mode13: '0'
iq_balance_mode14: '0'
iq_balance_mode15: '0'
iq_balance_mode16: '0'
iq_balance_mode17: '0'
iq_balance_mode18: '0'
iq_balance_mode19: '0'
iq_balance_mode2: '0'
iq_balance_mode20: '0'
iq_balance_mode21: '0'
iq_balance_mode22: '0'
iq_balance_mode23: '0'
iq_balance_mode24: '0'
iq_balance_mode25: '0'
iq_balance_mode26: '0'
iq_balance_mode27: '0'
iq_balance_mode28: '0'
iq_balance_mode29: '0'
iq_balance_mode3: '0'
iq_balance_mode30: '0'
iq_balance_mode31: '0'
iq_balance_mode4: '0'
iq_balance_mode5: '0'
iq_balance_mode6: '0'
iq_balance_mode7: '0'
iq_balance_mode8: '0'
iq_balance_mode9: '0'
maxoutbuf: '0'
minoutbuf: '0'
nchan: '1'
num_mboards: '1'
sample_rate: samp_rate
sync: sync
time_source0: ''
time_source1: ''
time_source2: ''
time_source3: ''
time_source4: ''
time_source5: ''
time_source6: ''
time_source7: ''
type: fc32
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: xmlrpc_server_0
id: xmlrpc_server
parameters:
addr: localhost
alias: ''
comment: ''
port: '8080'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
connections:
- [lora_rx_0, out, blocks_message_debug_0, print]
- [low_pass_filter_0, '0', lora_rx_0, '0']
- [osmosdr_source_0, '0', low_pass_filter_0, '0']
metadata:
file_format: 1
grc_version: 3.10.12.0

167
examples/lora_channel_scanner.py Executable file
View File

@ -0,0 +1,167 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: GPL-3.0
#
# GNU Radio Python Flow Graph
# Title: LoRa Channel Scanner
# Author: gr-mcp
# Description: Scans US ISM 915MHz band for LoRa activity. Retune via XML-RPC.
# GNU Radio version: 3.10.12.0
from gnuradio import blocks, gr
from gnuradio import filter
from gnuradio.filter import firdes
from gnuradio import gr
from gnuradio.fft import window
import sys
import signal
from argparse import ArgumentParser
from gnuradio.eng_arg import eng_float, intx
from gnuradio import eng_notation
from xmlrpc.server import SimpleXMLRPCServer
import threading
import gnuradio.lora_sdr as lora_sdr
import osmosdr
import time
class lora_channel_scanner(gr.top_block):
def __init__(self):
gr.top_block.__init__(self, "LoRa Channel Scanner", catch_exceptions=True)
self.flowgraph_started = threading.Event()
##################################################
# Variables
##################################################
self.samp_rate = samp_rate = 1000000
self.rf_gain = rf_gain = 40
self.lora_sf = lora_sf = 7
self.lora_cr = lora_cr = 1
self.lora_bw = lora_bw = 125000
self.channel_index = channel_index = 0
self.center_freq = center_freq = 915e6
##################################################
# Blocks
##################################################
self.xmlrpc_server_0 = SimpleXMLRPCServer(('localhost', 8080), allow_none=True)
self.xmlrpc_server_0.register_instance(self)
self.xmlrpc_server_0_thread = threading.Thread(target=self.xmlrpc_server_0.serve_forever)
self.xmlrpc_server_0_thread.daemon = True
self.xmlrpc_server_0_thread.start()
self.osmosdr_source_0 = osmosdr.source(
args="numchan=" + str(1) + " " + 'rtl=0'
)
self.osmosdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t())
self.osmosdr_source_0.set_sample_rate(samp_rate)
self.osmosdr_source_0.set_center_freq(center_freq, 0)
self.osmosdr_source_0.set_freq_corr(0, 0)
self.osmosdr_source_0.set_dc_offset_mode(2, 0)
self.osmosdr_source_0.set_iq_balance_mode(2, 0)
self.osmosdr_source_0.set_gain_mode(False, 0)
self.osmosdr_source_0.set_gain(rf_gain, 0)
self.osmosdr_source_0.set_if_gain(20, 0)
self.osmosdr_source_0.set_bb_gain(20, 0)
self.osmosdr_source_0.set_antenna('', 0)
self.osmosdr_source_0.set_bandwidth(0, 0)
self.low_pass_filter_0 = filter.fir_filter_ccf(
2,
firdes.low_pass(
1,
samp_rate,
200e3,
50e3,
window.WIN_HAMMING,
6.76))
self.lora_rx_0 = lora_sdr.lora_sdr_lora_rx( bw=lora_bw, cr=1, has_crc=True, impl_head=False, pay_len=255, samp_rate=(int(samp_rate/2)), sf=lora_sf, sync_word=[0x12], soft_decoding=True, ldro_mode=2, print_rx=[True,True])
self.blocks_message_debug_0 = blocks.message_debug(True, gr.log_levels.info)
##################################################
# Connections
##################################################
self.msg_connect((self.lora_rx_0, 'out'), (self.blocks_message_debug_0, 'print'))
self.connect((self.low_pass_filter_0, 0), (self.lora_rx_0, 0))
self.connect((self.osmosdr_source_0, 0), (self.low_pass_filter_0, 0))
def get_samp_rate(self):
return self.samp_rate
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.low_pass_filter_0.set_taps(firdes.low_pass(1, self.samp_rate, 200e3, 50e3, window.WIN_HAMMING, 6.76))
self.osmosdr_source_0.set_sample_rate(self.samp_rate)
def get_rf_gain(self):
return self.rf_gain
def set_rf_gain(self, rf_gain):
self.rf_gain = rf_gain
self.osmosdr_source_0.set_gain(self.rf_gain, 0)
def get_lora_sf(self):
return self.lora_sf
def set_lora_sf(self, lora_sf):
self.lora_sf = lora_sf
def get_lora_cr(self):
return self.lora_cr
def set_lora_cr(self, lora_cr):
self.lora_cr = lora_cr
def get_lora_bw(self):
return self.lora_bw
def set_lora_bw(self, lora_bw):
self.lora_bw = lora_bw
def get_channel_index(self):
return self.channel_index
def set_channel_index(self, channel_index):
self.channel_index = channel_index
def get_center_freq(self):
return self.center_freq
def set_center_freq(self, center_freq):
self.center_freq = center_freq
self.osmosdr_source_0.set_center_freq(self.center_freq, 0)
def main(top_block_cls=lora_channel_scanner, options=None):
tb = top_block_cls()
def sig_handler(sig=None, frame=None):
tb.stop()
tb.wait()
sys.exit(0)
signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGTERM, sig_handler)
tb.start()
tb.flowgraph_started.set()
try:
input('Press Enter to quit: ')
except EOFError:
pass
tb.stop()
tb.wait()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,673 @@
options:
parameters:
author: gr-mcp
catch_exceptions: 'True'
category: '[GRC Hier Blocks]'
cmake_opt: ''
comment: ''
copyright: ''
description: LoRa decoder with IQ recording and real-time signal power measurement.
gen_cmake: 'On'
gen_linking: dynamic
generate_options: no_gui
hier_block_src_path: '.:'
id: lora_quality_analyzer
max_nouts: '0'
output_language: python
placement: (0,0)
qt_qss_theme: ''
realtime_scheduling: ''
run: 'True'
run_command: '{python} -u {filename}'
run_options: prompt
sizing_mode: fixed
thread_safe_setters: ''
title: LoRa Signal Quality Analyzer
window_size: (1000,1000)
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [8, 8]
rotation: 0
state: enabled
blocks:
- name: center_freq
id: variable
parameters:
comment: ''
value: 915e6
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: iq_file
id: variable
parameters:
comment: ''
value: '"/tmp/iq_capture.cf32"'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_bw
id: variable
parameters:
comment: ''
value: '125000'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_sf
id: variable
parameters:
comment: ''
value: '7'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: recording_selector
id: variable
parameters:
comment: ''
value: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: rf_gain
id: variable
parameters:
comment: ''
value: '40'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: samp_rate
id: variable
parameters:
comment: ''
value: '1000000'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: signal_power_db
id: variable_function_probe
parameters:
block_id: blocks_probe_signal_x_0
comment: ''
function_args: ''
function_name: level
poll_rate: '2'
value: '0'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: blocks_complex_to_mag_squared_0
id: blocks_complex_to_mag_squared
parameters:
affinity: ''
alias: ''
comment: ''
maxoutbuf: '0'
minoutbuf: '0'
vlen: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: blocks_file_sink_0
id: blocks_file_sink
parameters:
affinity: ''
alias: ''
append: 'False'
comment: ''
file: iq_file
type: complex
unbuffered: 'False'
vlen: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: blocks_message_debug_0
id: blocks_message_debug
parameters:
affinity: ''
alias: ''
comment: ''
en_uvec: 'True'
log_level: info
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: blocks_moving_average_xx_0
id: blocks_moving_average_xx
parameters:
affinity: ''
alias: ''
comment: ''
length: '10000'
max_iter: '4000'
maxoutbuf: '0'
minoutbuf: '0'
scale: 1.0/10000
type: float
vlen: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: blocks_nlog10_ff_0
id: blocks_nlog10_ff
parameters:
affinity: ''
alias: ''
comment: ''
k: '0'
maxoutbuf: '0'
minoutbuf: '0'
n: '10'
vlen: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: blocks_null_sink_0
id: blocks_null_sink
parameters:
affinity: ''
alias: ''
bus_structure_sink: '[[0,],]'
comment: ''
num_inputs: '1'
type: complex
vlen: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: blocks_probe_signal_x_0
id: blocks_probe_signal_x
parameters:
affinity: ''
alias: ''
comment: ''
type: float
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: blocks_selector_0
id: blocks_selector
parameters:
affinity: ''
alias: ''
comment: ''
enabled: 'True'
input_index: '0'
maxoutbuf: '0'
minoutbuf: '0'
num_inputs: '1'
num_outputs: '2'
output_index: recording_selector
showports: 'True'
type: complex
vlen: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_rx_0
id: lora_rx
parameters:
affinity: ''
alias: ''
bw: lora_bw
comment: ''
cr: '1'
has_crc: 'True'
impl_head: 'False'
ldro: '2'
maxoutbuf: '0'
minoutbuf: '0'
pay_len: '255'
print_rx: '[True,True]'
samp_rate: int(samp_rate/2)
sf: lora_sf
soft_decoding: 'True'
sync_word: '[0x12]'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: low_pass_filter_0
id: low_pass_filter
parameters:
affinity: ''
alias: ''
beta: '6.76'
comment: ''
cutoff_freq: 200e3
decim: '2'
gain: '1'
interp: '1'
maxoutbuf: '0'
minoutbuf: '0'
samp_rate: samp_rate
type: fir_filter_ccf
width: 50e3
win: window.WIN_HAMMING
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: osmosdr_source_0
id: osmosdr_source
parameters:
affinity: ''
alias: ''
ant0: ''
ant1: ''
ant10: ''
ant11: ''
ant12: ''
ant13: ''
ant14: ''
ant15: ''
ant16: ''
ant17: ''
ant18: ''
ant19: ''
ant2: ''
ant20: ''
ant21: ''
ant22: ''
ant23: ''
ant24: ''
ant25: ''
ant26: ''
ant27: ''
ant28: ''
ant29: ''
ant3: ''
ant30: ''
ant31: ''
ant4: ''
ant5: ''
ant6: ''
ant7: ''
ant8: ''
ant9: ''
args: rtl=0
bb_gain0: '20'
bb_gain1: '20'
bb_gain10: '20'
bb_gain11: '20'
bb_gain12: '20'
bb_gain13: '20'
bb_gain14: '20'
bb_gain15: '20'
bb_gain16: '20'
bb_gain17: '20'
bb_gain18: '20'
bb_gain19: '20'
bb_gain2: '20'
bb_gain20: '20'
bb_gain21: '20'
bb_gain22: '20'
bb_gain23: '20'
bb_gain24: '20'
bb_gain25: '20'
bb_gain26: '20'
bb_gain27: '20'
bb_gain28: '20'
bb_gain29: '20'
bb_gain3: '20'
bb_gain30: '20'
bb_gain31: '20'
bb_gain4: '20'
bb_gain5: '20'
bb_gain6: '20'
bb_gain7: '20'
bb_gain8: '20'
bb_gain9: '20'
bw0: '0'
bw1: '0'
bw10: '0'
bw11: '0'
bw12: '0'
bw13: '0'
bw14: '0'
bw15: '0'
bw16: '0'
bw17: '0'
bw18: '0'
bw19: '0'
bw2: '0'
bw20: '0'
bw21: '0'
bw22: '0'
bw23: '0'
bw24: '0'
bw25: '0'
bw26: '0'
bw27: '0'
bw28: '0'
bw29: '0'
bw3: '0'
bw30: '0'
bw31: '0'
bw4: '0'
bw5: '0'
bw6: '0'
bw7: '0'
bw8: '0'
bw9: '0'
clock_source0: ''
clock_source1: ''
clock_source2: ''
clock_source3: ''
clock_source4: ''
clock_source5: ''
clock_source6: ''
clock_source7: ''
comment: ''
corr0: '0'
corr1: '0'
corr10: '0'
corr11: '0'
corr12: '0'
corr13: '0'
corr14: '0'
corr15: '0'
corr16: '0'
corr17: '0'
corr18: '0'
corr19: '0'
corr2: '0'
corr20: '0'
corr21: '0'
corr22: '0'
corr23: '0'
corr24: '0'
corr25: '0'
corr26: '0'
corr27: '0'
corr28: '0'
corr29: '0'
corr3: '0'
corr30: '0'
corr31: '0'
corr4: '0'
corr5: '0'
corr6: '0'
corr7: '0'
corr8: '0'
corr9: '0'
dc_offset_mode0: '2'
dc_offset_mode1: '0'
dc_offset_mode10: '0'
dc_offset_mode11: '0'
dc_offset_mode12: '0'
dc_offset_mode13: '0'
dc_offset_mode14: '0'
dc_offset_mode15: '0'
dc_offset_mode16: '0'
dc_offset_mode17: '0'
dc_offset_mode18: '0'
dc_offset_mode19: '0'
dc_offset_mode2: '0'
dc_offset_mode20: '0'
dc_offset_mode21: '0'
dc_offset_mode22: '0'
dc_offset_mode23: '0'
dc_offset_mode24: '0'
dc_offset_mode25: '0'
dc_offset_mode26: '0'
dc_offset_mode27: '0'
dc_offset_mode28: '0'
dc_offset_mode29: '0'
dc_offset_mode3: '0'
dc_offset_mode30: '0'
dc_offset_mode31: '0'
dc_offset_mode4: '0'
dc_offset_mode5: '0'
dc_offset_mode6: '0'
dc_offset_mode7: '0'
dc_offset_mode8: '0'
dc_offset_mode9: '0'
freq0: center_freq
freq1: 100e6
freq10: 100e6
freq11: 100e6
freq12: 100e6
freq13: 100e6
freq14: 100e6
freq15: 100e6
freq16: 100e6
freq17: 100e6
freq18: 100e6
freq19: 100e6
freq2: 100e6
freq20: 100e6
freq21: 100e6
freq22: 100e6
freq23: 100e6
freq24: 100e6
freq25: 100e6
freq26: 100e6
freq27: 100e6
freq28: 100e6
freq29: 100e6
freq3: 100e6
freq30: 100e6
freq31: 100e6
freq4: 100e6
freq5: 100e6
freq6: 100e6
freq7: 100e6
freq8: 100e6
freq9: 100e6
gain0: rf_gain
gain1: '10'
gain10: '10'
gain11: '10'
gain12: '10'
gain13: '10'
gain14: '10'
gain15: '10'
gain16: '10'
gain17: '10'
gain18: '10'
gain19: '10'
gain2: '10'
gain20: '10'
gain21: '10'
gain22: '10'
gain23: '10'
gain24: '10'
gain25: '10'
gain26: '10'
gain27: '10'
gain28: '10'
gain29: '10'
gain3: '10'
gain30: '10'
gain31: '10'
gain4: '10'
gain5: '10'
gain6: '10'
gain7: '10'
gain8: '10'
gain9: '10'
gain_mode0: 'False'
gain_mode1: 'False'
gain_mode10: 'False'
gain_mode11: 'False'
gain_mode12: 'False'
gain_mode13: 'False'
gain_mode14: 'False'
gain_mode15: 'False'
gain_mode16: 'False'
gain_mode17: 'False'
gain_mode18: 'False'
gain_mode19: 'False'
gain_mode2: 'False'
gain_mode20: 'False'
gain_mode21: 'False'
gain_mode22: 'False'
gain_mode23: 'False'
gain_mode24: 'False'
gain_mode25: 'False'
gain_mode26: 'False'
gain_mode27: 'False'
gain_mode28: 'False'
gain_mode29: 'False'
gain_mode3: 'False'
gain_mode30: 'False'
gain_mode31: 'False'
gain_mode4: 'False'
gain_mode5: 'False'
gain_mode6: 'False'
gain_mode7: 'False'
gain_mode8: 'False'
gain_mode9: 'False'
if_gain0: '20'
if_gain1: '20'
if_gain10: '20'
if_gain11: '20'
if_gain12: '20'
if_gain13: '20'
if_gain14: '20'
if_gain15: '20'
if_gain16: '20'
if_gain17: '20'
if_gain18: '20'
if_gain19: '20'
if_gain2: '20'
if_gain20: '20'
if_gain21: '20'
if_gain22: '20'
if_gain23: '20'
if_gain24: '20'
if_gain25: '20'
if_gain26: '20'
if_gain27: '20'
if_gain28: '20'
if_gain29: '20'
if_gain3: '20'
if_gain30: '20'
if_gain31: '20'
if_gain4: '20'
if_gain5: '20'
if_gain6: '20'
if_gain7: '20'
if_gain8: '20'
if_gain9: '20'
iq_balance_mode0: '2'
iq_balance_mode1: '0'
iq_balance_mode10: '0'
iq_balance_mode11: '0'
iq_balance_mode12: '0'
iq_balance_mode13: '0'
iq_balance_mode14: '0'
iq_balance_mode15: '0'
iq_balance_mode16: '0'
iq_balance_mode17: '0'
iq_balance_mode18: '0'
iq_balance_mode19: '0'
iq_balance_mode2: '0'
iq_balance_mode20: '0'
iq_balance_mode21: '0'
iq_balance_mode22: '0'
iq_balance_mode23: '0'
iq_balance_mode24: '0'
iq_balance_mode25: '0'
iq_balance_mode26: '0'
iq_balance_mode27: '0'
iq_balance_mode28: '0'
iq_balance_mode29: '0'
iq_balance_mode3: '0'
iq_balance_mode30: '0'
iq_balance_mode31: '0'
iq_balance_mode4: '0'
iq_balance_mode5: '0'
iq_balance_mode6: '0'
iq_balance_mode7: '0'
iq_balance_mode8: '0'
iq_balance_mode9: '0'
maxoutbuf: '0'
minoutbuf: '0'
nchan: '1'
num_mboards: '1'
sample_rate: samp_rate
sync: sync
time_source0: ''
time_source1: ''
time_source2: ''
time_source3: ''
time_source4: ''
time_source5: ''
time_source6: ''
time_source7: ''
type: fc32
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: xmlrpc_server_0
id: xmlrpc_server
parameters:
addr: localhost
alias: ''
comment: ''
port: '8080'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
connections:
- [blocks_complex_to_mag_squared_0, '0', blocks_moving_average_xx_0, '0']
- [blocks_moving_average_xx_0, '0', blocks_nlog10_ff_0, '0']
- [blocks_nlog10_ff_0, '0', blocks_probe_signal_x_0, '0']
- [blocks_selector_0, '0', blocks_file_sink_0, '0']
- [blocks_selector_0, '1', blocks_null_sink_0, '0']
- [lora_rx_0, out, blocks_message_debug_0, print]
- [low_pass_filter_0, '0', blocks_complex_to_mag_squared_0, '0']
- [low_pass_filter_0, '0', blocks_selector_0, '0']
- [low_pass_filter_0, '0', lora_rx_0, '0']
- [osmosdr_source_0, '0', low_pass_filter_0, '0']
metadata:
file_format: 1
grc_version: 3.10.12.0

209
examples/lora_quality_analyzer.py Executable file
View File

@ -0,0 +1,209 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: GPL-3.0
#
# GNU Radio Python Flow Graph
# Title: LoRa Signal Quality Analyzer
# Author: gr-mcp
# Description: LoRa decoder with IQ recording and real-time signal power measurement.
# GNU Radio version: 3.10.12.0
from gnuradio import blocks
from gnuradio import blocks, gr
from gnuradio import filter
from gnuradio.filter import firdes
from gnuradio import gr
from gnuradio.fft import window
import sys
import signal
from argparse import ArgumentParser
from gnuradio.eng_arg import eng_float, intx
from gnuradio import eng_notation
from xmlrpc.server import SimpleXMLRPCServer
import threading
import gnuradio.lora_sdr as lora_sdr
import osmosdr
import time
class lora_quality_analyzer(gr.top_block):
def __init__(self):
gr.top_block.__init__(self, "LoRa Signal Quality Analyzer", catch_exceptions=True)
self.flowgraph_started = threading.Event()
##################################################
# Variables
##################################################
self.signal_power_db = signal_power_db = 0
self.samp_rate = samp_rate = 1000000
self.rf_gain = rf_gain = 40
self.recording_selector = recording_selector = 1
self.lora_sf = lora_sf = 7
self.lora_bw = lora_bw = 125000
self.iq_file = iq_file = "/tmp/iq_capture.cf32"
self.center_freq = center_freq = 915e6
##################################################
# Blocks
##################################################
self.blocks_probe_signal_x_0 = blocks.probe_signal_f()
self.xmlrpc_server_0 = SimpleXMLRPCServer(('localhost', 8080), allow_none=True)
self.xmlrpc_server_0.register_instance(self)
self.xmlrpc_server_0_thread = threading.Thread(target=self.xmlrpc_server_0.serve_forever)
self.xmlrpc_server_0_thread.daemon = True
self.xmlrpc_server_0_thread.start()
def _signal_power_db_probe():
self.flowgraph_started.wait()
while True:
val = self.blocks_probe_signal_x_0.level()
try:
try:
self.doc.add_next_tick_callback(functools.partial(self.set_signal_power_db,val))
except AttributeError:
self.set_signal_power_db(val)
except AttributeError:
pass
time.sleep(1.0 / (2))
_signal_power_db_thread = threading.Thread(target=_signal_power_db_probe)
_signal_power_db_thread.daemon = True
_signal_power_db_thread.start()
self.osmosdr_source_0 = osmosdr.source(
args="numchan=" + str(1) + " " + 'rtl=0'
)
self.osmosdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t())
self.osmosdr_source_0.set_sample_rate(samp_rate)
self.osmosdr_source_0.set_center_freq(center_freq, 0)
self.osmosdr_source_0.set_freq_corr(0, 0)
self.osmosdr_source_0.set_dc_offset_mode(2, 0)
self.osmosdr_source_0.set_iq_balance_mode(2, 0)
self.osmosdr_source_0.set_gain_mode(False, 0)
self.osmosdr_source_0.set_gain(rf_gain, 0)
self.osmosdr_source_0.set_if_gain(20, 0)
self.osmosdr_source_0.set_bb_gain(20, 0)
self.osmosdr_source_0.set_antenna('', 0)
self.osmosdr_source_0.set_bandwidth(0, 0)
self.low_pass_filter_0 = filter.fir_filter_ccf(
2,
firdes.low_pass(
1,
samp_rate,
200e3,
50e3,
window.WIN_HAMMING,
6.76))
self.lora_rx_0 = lora_sdr.lora_sdr_lora_rx( bw=lora_bw, cr=1, has_crc=True, impl_head=False, pay_len=255, samp_rate=(int(samp_rate/2)), sf=lora_sf, sync_word=[0x12], soft_decoding=True, ldro_mode=2, print_rx=[True,True])
self.blocks_selector_0 = blocks.selector(gr.sizeof_gr_complex*1,0,recording_selector)
self.blocks_selector_0.set_enabled(True)
self.blocks_null_sink_0 = blocks.null_sink(gr.sizeof_gr_complex*1)
self.blocks_nlog10_ff_0 = blocks.nlog10_ff(10, 1, 0)
self.blocks_moving_average_xx_0 = blocks.moving_average_ff(10000, (1.0/10000), 4000, 1)
self.blocks_message_debug_0 = blocks.message_debug(True, gr.log_levels.info)
self.blocks_file_sink_0 = blocks.file_sink(gr.sizeof_gr_complex*1, iq_file, False)
self.blocks_file_sink_0.set_unbuffered(False)
self.blocks_complex_to_mag_squared_0 = blocks.complex_to_mag_squared(1)
##################################################
# Connections
##################################################
self.msg_connect((self.lora_rx_0, 'out'), (self.blocks_message_debug_0, 'print'))
self.connect((self.blocks_complex_to_mag_squared_0, 0), (self.blocks_moving_average_xx_0, 0))
self.connect((self.blocks_moving_average_xx_0, 0), (self.blocks_nlog10_ff_0, 0))
self.connect((self.blocks_nlog10_ff_0, 0), (self.blocks_probe_signal_x_0, 0))
self.connect((self.blocks_selector_0, 0), (self.blocks_file_sink_0, 0))
self.connect((self.blocks_selector_0, 1), (self.blocks_null_sink_0, 0))
self.connect((self.low_pass_filter_0, 0), (self.blocks_complex_to_mag_squared_0, 0))
self.connect((self.low_pass_filter_0, 0), (self.blocks_selector_0, 0))
self.connect((self.low_pass_filter_0, 0), (self.lora_rx_0, 0))
self.connect((self.osmosdr_source_0, 0), (self.low_pass_filter_0, 0))
def get_signal_power_db(self):
return self.signal_power_db
def set_signal_power_db(self, signal_power_db):
self.signal_power_db = signal_power_db
def get_samp_rate(self):
return self.samp_rate
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.low_pass_filter_0.set_taps(firdes.low_pass(1, self.samp_rate, 200e3, 50e3, window.WIN_HAMMING, 6.76))
self.osmosdr_source_0.set_sample_rate(self.samp_rate)
def get_rf_gain(self):
return self.rf_gain
def set_rf_gain(self, rf_gain):
self.rf_gain = rf_gain
self.osmosdr_source_0.set_gain(self.rf_gain, 0)
def get_recording_selector(self):
return self.recording_selector
def set_recording_selector(self, recording_selector):
self.recording_selector = recording_selector
self.blocks_selector_0.set_output_index(self.recording_selector)
def get_lora_sf(self):
return self.lora_sf
def set_lora_sf(self, lora_sf):
self.lora_sf = lora_sf
def get_lora_bw(self):
return self.lora_bw
def set_lora_bw(self, lora_bw):
self.lora_bw = lora_bw
def get_iq_file(self):
return self.iq_file
def set_iq_file(self, iq_file):
self.iq_file = iq_file
self.blocks_file_sink_0.open(self.iq_file)
def get_center_freq(self):
return self.center_freq
def set_center_freq(self, center_freq):
self.center_freq = center_freq
self.osmosdr_source_0.set_center_freq(self.center_freq, 0)
def main(top_block_cls=lora_quality_analyzer, options=None):
tb = top_block_cls()
def sig_handler(sig=None, frame=None):
tb.stop()
tb.wait()
sys.exit(0)
signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGTERM, sig_handler)
tb.start()
tb.flowgraph_started.set()
try:
input('Press Enter to quit: ')
except EOFError:
pass
tb.stop()
tb.wait()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,588 @@
options:
parameters:
author: gr-mcp
catch_exceptions: 'True'
category: '[GRC Hier Blocks]'
cmake_opt: ''
comment: ''
copyright: ''
description: Simultaneous LoRa decoder for SF7-SF12. 915MHz, BW125k.
gen_cmake: 'On'
gen_linking: dynamic
generate_options: no_gui
hier_block_src_path: '.:'
id: multi_sf_lora_rx
max_nouts: '0'
output_language: python
placement: (0,0)
qt_qss_theme: ''
realtime_scheduling: ''
run: 'True'
run_command: '{python} -u {filename}'
run_options: prompt
sizing_mode: fixed
thread_safe_setters: ''
title: Multi-SF LoRa Receiver
window_size: (1000,1000)
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [8, 8]
rotation: 0
state: enabled
blocks:
- name: center_freq
id: variable
parameters:
comment: ''
value: 915e6
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_bw
id: variable
parameters:
comment: ''
value: '125000'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: rf_gain
id: variable
parameters:
comment: ''
value: '40'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: samp_rate
id: variable
parameters:
comment: ''
value: '1000000'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: blocks_message_debug_0
id: blocks_message_debug
parameters:
affinity: ''
alias: ''
comment: ''
en_uvec: 'True'
log_level: info
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_rx_0
id: lora_rx
parameters:
affinity: ''
alias: ''
bw: lora_bw
comment: ''
cr: '1'
has_crc: 'True'
impl_head: 'False'
ldro: '2'
maxoutbuf: '0'
minoutbuf: '0'
pay_len: '255'
print_rx: '[True,True]'
samp_rate: int(samp_rate/2)
sf: '7'
soft_decoding: 'True'
sync_word: '[0x12]'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_rx_1
id: lora_rx
parameters:
affinity: ''
alias: ''
bw: lora_bw
comment: ''
cr: '1'
has_crc: 'True'
impl_head: 'False'
ldro: '2'
maxoutbuf: '0'
minoutbuf: '0'
pay_len: '255'
print_rx: '[True,True]'
samp_rate: int(samp_rate/2)
sf: '8'
soft_decoding: 'True'
sync_word: '[0x12]'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_rx_2
id: lora_rx
parameters:
affinity: ''
alias: ''
bw: lora_bw
comment: ''
cr: '1'
has_crc: 'True'
impl_head: 'False'
ldro: '2'
maxoutbuf: '0'
minoutbuf: '0'
pay_len: '255'
print_rx: '[True,True]'
samp_rate: int(samp_rate/2)
sf: '9'
soft_decoding: 'True'
sync_word: '[0x12]'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: lora_rx_3
id: lora_rx
parameters:
affinity: ''
alias: ''
bw: lora_bw
comment: ''
cr: '1'
has_crc: 'True'
impl_head: 'False'
ldro: '2'
maxoutbuf: '0'
minoutbuf: '0'
pay_len: '255'
print_rx: '[True,True]'
samp_rate: int(samp_rate/2)
sf: '10'
soft_decoding: 'True'
sync_word: '[0x12]'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: low_pass_filter_0
id: low_pass_filter
parameters:
affinity: ''
alias: ''
beta: '6.76'
comment: ''
cutoff_freq: 200e3
decim: '2'
gain: '1'
interp: '1'
maxoutbuf: '0'
minoutbuf: '0'
samp_rate: samp_rate
type: fir_filter_ccf
width: 50e3
win: window.WIN_HAMMING
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: osmosdr_source_0
id: osmosdr_source
parameters:
affinity: ''
alias: ''
ant0: ''
ant1: ''
ant10: ''
ant11: ''
ant12: ''
ant13: ''
ant14: ''
ant15: ''
ant16: ''
ant17: ''
ant18: ''
ant19: ''
ant2: ''
ant20: ''
ant21: ''
ant22: ''
ant23: ''
ant24: ''
ant25: ''
ant26: ''
ant27: ''
ant28: ''
ant29: ''
ant3: ''
ant30: ''
ant31: ''
ant4: ''
ant5: ''
ant6: ''
ant7: ''
ant8: ''
ant9: ''
args: rtl=0
bb_gain0: '20'
bb_gain1: '20'
bb_gain10: '20'
bb_gain11: '20'
bb_gain12: '20'
bb_gain13: '20'
bb_gain14: '20'
bb_gain15: '20'
bb_gain16: '20'
bb_gain17: '20'
bb_gain18: '20'
bb_gain19: '20'
bb_gain2: '20'
bb_gain20: '20'
bb_gain21: '20'
bb_gain22: '20'
bb_gain23: '20'
bb_gain24: '20'
bb_gain25: '20'
bb_gain26: '20'
bb_gain27: '20'
bb_gain28: '20'
bb_gain29: '20'
bb_gain3: '20'
bb_gain30: '20'
bb_gain31: '20'
bb_gain4: '20'
bb_gain5: '20'
bb_gain6: '20'
bb_gain7: '20'
bb_gain8: '20'
bb_gain9: '20'
bw0: '0'
bw1: '0'
bw10: '0'
bw11: '0'
bw12: '0'
bw13: '0'
bw14: '0'
bw15: '0'
bw16: '0'
bw17: '0'
bw18: '0'
bw19: '0'
bw2: '0'
bw20: '0'
bw21: '0'
bw22: '0'
bw23: '0'
bw24: '0'
bw25: '0'
bw26: '0'
bw27: '0'
bw28: '0'
bw29: '0'
bw3: '0'
bw30: '0'
bw31: '0'
bw4: '0'
bw5: '0'
bw6: '0'
bw7: '0'
bw8: '0'
bw9: '0'
clock_source0: ''
clock_source1: ''
clock_source2: ''
clock_source3: ''
clock_source4: ''
clock_source5: ''
clock_source6: ''
clock_source7: ''
comment: ''
corr0: '0'
corr1: '0'
corr10: '0'
corr11: '0'
corr12: '0'
corr13: '0'
corr14: '0'
corr15: '0'
corr16: '0'
corr17: '0'
corr18: '0'
corr19: '0'
corr2: '0'
corr20: '0'
corr21: '0'
corr22: '0'
corr23: '0'
corr24: '0'
corr25: '0'
corr26: '0'
corr27: '0'
corr28: '0'
corr29: '0'
corr3: '0'
corr30: '0'
corr31: '0'
corr4: '0'
corr5: '0'
corr6: '0'
corr7: '0'
corr8: '0'
corr9: '0'
dc_offset_mode0: '2'
dc_offset_mode1: '0'
dc_offset_mode10: '0'
dc_offset_mode11: '0'
dc_offset_mode12: '0'
dc_offset_mode13: '0'
dc_offset_mode14: '0'
dc_offset_mode15: '0'
dc_offset_mode16: '0'
dc_offset_mode17: '0'
dc_offset_mode18: '0'
dc_offset_mode19: '0'
dc_offset_mode2: '0'
dc_offset_mode20: '0'
dc_offset_mode21: '0'
dc_offset_mode22: '0'
dc_offset_mode23: '0'
dc_offset_mode24: '0'
dc_offset_mode25: '0'
dc_offset_mode26: '0'
dc_offset_mode27: '0'
dc_offset_mode28: '0'
dc_offset_mode29: '0'
dc_offset_mode3: '0'
dc_offset_mode30: '0'
dc_offset_mode31: '0'
dc_offset_mode4: '0'
dc_offset_mode5: '0'
dc_offset_mode6: '0'
dc_offset_mode7: '0'
dc_offset_mode8: '0'
dc_offset_mode9: '0'
freq0: center_freq
freq1: 100e6
freq10: 100e6
freq11: 100e6
freq12: 100e6
freq13: 100e6
freq14: 100e6
freq15: 100e6
freq16: 100e6
freq17: 100e6
freq18: 100e6
freq19: 100e6
freq2: 100e6
freq20: 100e6
freq21: 100e6
freq22: 100e6
freq23: 100e6
freq24: 100e6
freq25: 100e6
freq26: 100e6
freq27: 100e6
freq28: 100e6
freq29: 100e6
freq3: 100e6
freq30: 100e6
freq31: 100e6
freq4: 100e6
freq5: 100e6
freq6: 100e6
freq7: 100e6
freq8: 100e6
freq9: 100e6
gain0: rf_gain
gain1: '10'
gain10: '10'
gain11: '10'
gain12: '10'
gain13: '10'
gain14: '10'
gain15: '10'
gain16: '10'
gain17: '10'
gain18: '10'
gain19: '10'
gain2: '10'
gain20: '10'
gain21: '10'
gain22: '10'
gain23: '10'
gain24: '10'
gain25: '10'
gain26: '10'
gain27: '10'
gain28: '10'
gain29: '10'
gain3: '10'
gain30: '10'
gain31: '10'
gain4: '10'
gain5: '10'
gain6: '10'
gain7: '10'
gain8: '10'
gain9: '10'
gain_mode0: 'False'
gain_mode1: 'False'
gain_mode10: 'False'
gain_mode11: 'False'
gain_mode12: 'False'
gain_mode13: 'False'
gain_mode14: 'False'
gain_mode15: 'False'
gain_mode16: 'False'
gain_mode17: 'False'
gain_mode18: 'False'
gain_mode19: 'False'
gain_mode2: 'False'
gain_mode20: 'False'
gain_mode21: 'False'
gain_mode22: 'False'
gain_mode23: 'False'
gain_mode24: 'False'
gain_mode25: 'False'
gain_mode26: 'False'
gain_mode27: 'False'
gain_mode28: 'False'
gain_mode29: 'False'
gain_mode3: 'False'
gain_mode30: 'False'
gain_mode31: 'False'
gain_mode4: 'False'
gain_mode5: 'False'
gain_mode6: 'False'
gain_mode7: 'False'
gain_mode8: 'False'
gain_mode9: 'False'
if_gain0: '20'
if_gain1: '20'
if_gain10: '20'
if_gain11: '20'
if_gain12: '20'
if_gain13: '20'
if_gain14: '20'
if_gain15: '20'
if_gain16: '20'
if_gain17: '20'
if_gain18: '20'
if_gain19: '20'
if_gain2: '20'
if_gain20: '20'
if_gain21: '20'
if_gain22: '20'
if_gain23: '20'
if_gain24: '20'
if_gain25: '20'
if_gain26: '20'
if_gain27: '20'
if_gain28: '20'
if_gain29: '20'
if_gain3: '20'
if_gain30: '20'
if_gain31: '20'
if_gain4: '20'
if_gain5: '20'
if_gain6: '20'
if_gain7: '20'
if_gain8: '20'
if_gain9: '20'
iq_balance_mode0: '2'
iq_balance_mode1: '0'
iq_balance_mode10: '0'
iq_balance_mode11: '0'
iq_balance_mode12: '0'
iq_balance_mode13: '0'
iq_balance_mode14: '0'
iq_balance_mode15: '0'
iq_balance_mode16: '0'
iq_balance_mode17: '0'
iq_balance_mode18: '0'
iq_balance_mode19: '0'
iq_balance_mode2: '0'
iq_balance_mode20: '0'
iq_balance_mode21: '0'
iq_balance_mode22: '0'
iq_balance_mode23: '0'
iq_balance_mode24: '0'
iq_balance_mode25: '0'
iq_balance_mode26: '0'
iq_balance_mode27: '0'
iq_balance_mode28: '0'
iq_balance_mode29: '0'
iq_balance_mode3: '0'
iq_balance_mode30: '0'
iq_balance_mode31: '0'
iq_balance_mode4: '0'
iq_balance_mode5: '0'
iq_balance_mode6: '0'
iq_balance_mode7: '0'
iq_balance_mode8: '0'
iq_balance_mode9: '0'
maxoutbuf: '0'
minoutbuf: '0'
nchan: '1'
num_mboards: '1'
sample_rate: samp_rate
sync: sync
time_source0: ''
time_source1: ''
time_source2: ''
time_source3: ''
time_source4: ''
time_source5: ''
time_source6: ''
time_source7: ''
type: fc32
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: xmlrpc_server_0
id: xmlrpc_server
parameters:
addr: localhost
alias: ''
comment: ''
port: '8080'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
connections:
- [lora_rx_0, out, blocks_message_debug_0, print]
- [lora_rx_1, out, blocks_message_debug_0, print]
- [lora_rx_2, out, blocks_message_debug_0, print]
- [lora_rx_3, out, blocks_message_debug_0, print]
- [low_pass_filter_0, '0', lora_rx_0, '0']
- [low_pass_filter_0, '0', lora_rx_1, '0']
- [low_pass_filter_0, '0', lora_rx_2, '0']
- [low_pass_filter_0, '0', lora_rx_3, '0']
- [osmosdr_source_0, '0', low_pass_filter_0, '0']
metadata:
file_format: 1
grc_version: 3.10.12.0

155
examples/multi_sf_lora_rx.py Executable file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: GPL-3.0
#
# GNU Radio Python Flow Graph
# Title: Multi-SF LoRa Receiver
# Author: gr-mcp
# Description: Simultaneous LoRa decoder for SF7-SF12. 915MHz, BW125k.
# GNU Radio version: 3.10.12.0
from gnuradio import blocks, gr
from gnuradio import filter
from gnuradio.filter import firdes
from gnuradio import gr
from gnuradio.fft import window
import sys
import signal
from argparse import ArgumentParser
from gnuradio.eng_arg import eng_float, intx
from gnuradio import eng_notation
from xmlrpc.server import SimpleXMLRPCServer
import threading
import gnuradio.lora_sdr as lora_sdr
import osmosdr
import time
class multi_sf_lora_rx(gr.top_block):
def __init__(self):
gr.top_block.__init__(self, "Multi-SF LoRa Receiver", catch_exceptions=True)
self.flowgraph_started = threading.Event()
##################################################
# Variables
##################################################
self.samp_rate = samp_rate = 1000000
self.rf_gain = rf_gain = 40
self.lora_bw = lora_bw = 125000
self.center_freq = center_freq = 915e6
##################################################
# Blocks
##################################################
self.xmlrpc_server_0 = SimpleXMLRPCServer(('localhost', 8080), allow_none=True)
self.xmlrpc_server_0.register_instance(self)
self.xmlrpc_server_0_thread = threading.Thread(target=self.xmlrpc_server_0.serve_forever)
self.xmlrpc_server_0_thread.daemon = True
self.xmlrpc_server_0_thread.start()
self.osmosdr_source_0 = osmosdr.source(
args="numchan=" + str(1) + " " + 'rtl=0'
)
self.osmosdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t())
self.osmosdr_source_0.set_sample_rate(samp_rate)
self.osmosdr_source_0.set_center_freq(center_freq, 0)
self.osmosdr_source_0.set_freq_corr(0, 0)
self.osmosdr_source_0.set_dc_offset_mode(2, 0)
self.osmosdr_source_0.set_iq_balance_mode(2, 0)
self.osmosdr_source_0.set_gain_mode(False, 0)
self.osmosdr_source_0.set_gain(rf_gain, 0)
self.osmosdr_source_0.set_if_gain(20, 0)
self.osmosdr_source_0.set_bb_gain(20, 0)
self.osmosdr_source_0.set_antenna('', 0)
self.osmosdr_source_0.set_bandwidth(0, 0)
self.low_pass_filter_0 = filter.fir_filter_ccf(
2,
firdes.low_pass(
1,
samp_rate,
200e3,
50e3,
window.WIN_HAMMING,
6.76))
self.lora_rx_3 = lora_sdr.lora_sdr_lora_rx( bw=lora_bw, cr=1, has_crc=True, impl_head=False, pay_len=255, samp_rate=(int(samp_rate/2)), sf=10, sync_word=[0x12], soft_decoding=True, ldro_mode=2, print_rx=[True,True])
self.lora_rx_2 = lora_sdr.lora_sdr_lora_rx( bw=lora_bw, cr=1, has_crc=True, impl_head=False, pay_len=255, samp_rate=(int(samp_rate/2)), sf=9, sync_word=[0x12], soft_decoding=True, ldro_mode=2, print_rx=[True,True])
self.lora_rx_1 = lora_sdr.lora_sdr_lora_rx( bw=lora_bw, cr=1, has_crc=True, impl_head=False, pay_len=255, samp_rate=(int(samp_rate/2)), sf=8, sync_word=[0x12], soft_decoding=True, ldro_mode=2, print_rx=[True,True])
self.lora_rx_0 = lora_sdr.lora_sdr_lora_rx( bw=lora_bw, cr=1, has_crc=True, impl_head=False, pay_len=255, samp_rate=(int(samp_rate/2)), sf=7, sync_word=[0x12], soft_decoding=True, ldro_mode=2, print_rx=[True,True])
self.blocks_message_debug_0 = blocks.message_debug(True, gr.log_levels.info)
##################################################
# Connections
##################################################
self.msg_connect((self.lora_rx_0, 'out'), (self.blocks_message_debug_0, 'print'))
self.msg_connect((self.lora_rx_1, 'out'), (self.blocks_message_debug_0, 'print'))
self.msg_connect((self.lora_rx_2, 'out'), (self.blocks_message_debug_0, 'print'))
self.msg_connect((self.lora_rx_3, 'out'), (self.blocks_message_debug_0, 'print'))
self.connect((self.low_pass_filter_0, 0), (self.lora_rx_0, 0))
self.connect((self.low_pass_filter_0, 0), (self.lora_rx_1, 0))
self.connect((self.low_pass_filter_0, 0), (self.lora_rx_2, 0))
self.connect((self.low_pass_filter_0, 0), (self.lora_rx_3, 0))
self.connect((self.osmosdr_source_0, 0), (self.low_pass_filter_0, 0))
def get_samp_rate(self):
return self.samp_rate
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.low_pass_filter_0.set_taps(firdes.low_pass(1, self.samp_rate, 200e3, 50e3, window.WIN_HAMMING, 6.76))
self.osmosdr_source_0.set_sample_rate(self.samp_rate)
def get_rf_gain(self):
return self.rf_gain
def set_rf_gain(self, rf_gain):
self.rf_gain = rf_gain
self.osmosdr_source_0.set_gain(self.rf_gain, 0)
def get_lora_bw(self):
return self.lora_bw
def set_lora_bw(self, lora_bw):
self.lora_bw = lora_bw
def get_center_freq(self):
return self.center_freq
def set_center_freq(self, center_freq):
self.center_freq = center_freq
self.osmosdr_source_0.set_center_freq(self.center_freq, 0)
def main(top_block_cls=multi_sf_lora_rx, options=None):
tb = top_block_cls()
def sig_handler(sig=None, frame=None):
tb.stop()
tb.wait()
sys.exit(0)
signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGTERM, sig_handler)
tb.start()
tb.flowgraph_started.set()
try:
input('Press Enter to quit: ')
except EOFError:
pass
tb.stop()
tb.wait()
if __name__ == '__main__':
main()

2
uv.lock generated
View File

@ -352,7 +352,7 @@ wheels = [
[[package]]
name = "gnuradio-mcp"
version = "0.2.0"
version = "2026.2.20"
source = { editable = "." }
dependencies = [
{ name = "fastmcp" },