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:
parent
41dcebbf6d
commit
8800d35fd4
10
.gitignore
vendored
10
.gitignore
vendored
@ -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
15
.mcp.json
Normal 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
48
docs/astro.config.mjs
Normal 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
6964
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
docs/package.json
Normal file
18
docs/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
355
docs/scripts/generate-api-docs.py
Normal file
355
docs/scripts/generate-api-docs.py
Normal 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()
|
||||
10
docs/src/content.config.ts
Normal file
10
docs/src/content.config.ts
Normal 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(),
|
||||
}),
|
||||
};
|
||||
237
docs/src/content/docs/concepts/architecture.mdx
Normal file
237
docs/src/content/docs/concepts/architecture.mdx
Normal 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
|
||||
```
|
||||
196
docs/src/content/docs/concepts/dynamic-tools.mdx
Normal file
196
docs/src/content/docs/concepts/dynamic-tools.mdx
Normal 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
|
||||
265
docs/src/content/docs/concepts/runtime-communication.mdx
Normal file
265
docs/src/content/docs/concepts/runtime-communication.mdx
Normal 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
|
||||
240
docs/src/content/docs/getting-started/first-flowgraph.mdx
Normal file
240
docs/src/content/docs/getting-started/first-flowgraph.mdx
Normal 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
|
||||
173
docs/src/content/docs/getting-started/installation.mdx
Normal file
173
docs/src/content/docs/getting-started/installation.mdx
Normal 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
|
||||
270
docs/src/content/docs/getting-started/running-in-docker.mdx
Normal file
270
docs/src/content/docs/getting-started/running-in-docker.mdx
Normal 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
|
||||
287
docs/src/content/docs/guides/code-coverage.mdx
Normal file
287
docs/src/content/docs/guides/code-coverage.mdx
Normal 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
|
||||
262
docs/src/content/docs/guides/controlport.mdx
Normal file
262
docs/src/content/docs/guides/controlport.mdx
Normal 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
|
||||
251
docs/src/content/docs/guides/oot-modules.mdx
Normal file
251
docs/src/content/docs/guides/oot-modules.mdx
Normal 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
|
||||
207
docs/src/content/docs/guides/runtime-control.mdx
Normal file
207
docs/src/content/docs/guides/runtime-control.mdx
Normal 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
|
||||
116
docs/src/content/docs/index.mdx
Normal file
116
docs/src/content/docs/index.mdx
Normal 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>
|
||||
204
docs/src/content/docs/reference/docker-images.mdx
Normal file
204
docs/src/content/docs/reference/docker-images.mdx
Normal 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"
|
||||
```
|
||||
249
docs/src/content/docs/reference/oot-catalog.mdx
Normal file
249
docs/src/content/docs/reference/oot-catalog.mdx
Normal 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** |
|
||||
171
docs/src/content/docs/reference/tools-overview.mdx
Normal file
171
docs/src/content/docs/reference/tools-overview.mdx
Normal 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 |
|
||||
168
docs/src/content/docs/reference/tools/blocks.mdx
Normal file
168
docs/src/content/docs/reference/tools/blocks.mdx
Normal 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
|
||||
```
|
||||
147
docs/src/content/docs/reference/tools/codegen.mdx
Normal file
147
docs/src/content/docs/reference/tools/codegen.mdx
Normal 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
|
||||
113
docs/src/content/docs/reference/tools/connections.mdx
Normal file
113
docs/src/content/docs/reference/tools/connections.mdx
Normal 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"
|
||||
)
|
||||
)
|
||||
```
|
||||
268
docs/src/content/docs/reference/tools/controlport.mdx
Normal file
268
docs/src/content/docs/reference/tools/controlport.mdx
Normal 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})
|
||||
```
|
||||
158
docs/src/content/docs/reference/tools/coverage.mdx
Normal file
158
docs/src/content/docs/reference/tools/coverage.mdx
Normal 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
|
||||
```
|
||||
212
docs/src/content/docs/reference/tools/docker.mdx
Normal file
212
docs/src/content/docs/reference/tools/docker.mdx
Normal 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..."
|
||||
```
|
||||
228
docs/src/content/docs/reference/tools/flowgraph.mdx
Normal file
228
docs/src/content/docs/reference/tools/flowgraph.mdx
Normal 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
|
||||
```
|
||||
238
docs/src/content/docs/reference/tools/oot.mdx
Normal file
238
docs/src/content/docs/reference/tools/oot.mdx
Normal 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
|
||||
```
|
||||
173
docs/src/content/docs/reference/tools/platform.mdx
Normal file
173
docs/src/content/docs/reference/tools/platform.mdx
Normal 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
|
||||
# )
|
||||
```
|
||||
170
docs/src/content/docs/reference/tools/runtime-mode.mdx
Normal file
170
docs/src/content/docs/reference/tools/runtime-mode.mdx
Normal 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
|
||||
98
docs/src/content/docs/reference/tools/validation.mdx
Normal file
98
docs/src/content/docs/reference/tools/validation.mdx
Normal 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" |
|
||||
263
docs/src/content/docs/reference/tools/xmlrpc.mdx
Normal file
263
docs/src/content/docs/reference/tools/xmlrpc.mdx
Normal 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
|
||||
```
|
||||
63
docs/src/styles/custom.css
Normal file
63
docs/src/styles/custom.css
Normal 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;
|
||||
}
|
||||
100
examples/combo_test_adsb_lora.py
Normal file
100
examples/combo_test_adsb_lora.py
Normal 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()
|
||||
540
examples/lora_channel_scanner.grc
Normal file
540
examples/lora_channel_scanner.grc
Normal 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
167
examples/lora_channel_scanner.py
Executable 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()
|
||||
673
examples/lora_quality_analyzer.grc
Normal file
673
examples/lora_quality_analyzer.grc
Normal 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
209
examples/lora_quality_analyzer.py
Executable 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()
|
||||
588
examples/multi_sf_lora_rx.grc
Normal file
588
examples/multi_sf_lora_rx.grc
Normal 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
155
examples/multi_sf_lora_rx.py
Executable 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()
|
||||
Loading…
x
Reference in New Issue
Block a user