feat: add generate_grc_yaml tool for GNU Radio Companion block definitions
Blocks generated by gr-mcp now have a first-class path to GRC's graphical editor. Previously, .block.yml was only produced as a side-effect of full OOT export — now clients can generate YAML standalone to preview, customize, or use without a full OOT module. - New MCP tool: generate_grc_yaml (registered in block dev mode) - New model: GrcYamlResult with yaml_content, notes, filename - Enhanced YAML generation: flags, asserts from min/max bounds, parameter descriptions, docstring extraction - New prompt resource: prompts://block-generation/grc-yaml - 10 new unit tests for GRC YAML generation (475 total, 7 skipped)
This commit is contained in:
parent
3fe862109d
commit
ce785029f4
@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any
|
|||||||
|
|
||||||
from gnuradio_mcp.models import (
|
from gnuradio_mcp.models import (
|
||||||
GeneratedBlockCode,
|
GeneratedBlockCode,
|
||||||
|
GrcYamlResult,
|
||||||
OOTExportResult,
|
OOTExportResult,
|
||||||
OOTSkeletonResult,
|
OOTSkeletonResult,
|
||||||
)
|
)
|
||||||
@ -146,15 +147,15 @@ from importlib import import_module
|
|||||||
{imports}
|
{imports}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
BLOCK_YML_TEMPLATE = '''id: {module_name}_{block_name}
|
BLOCK_YML_TEMPLATE = '''id: {block_id}
|
||||||
label: {block_label}
|
label: {block_label}
|
||||||
category: [{module_name}]
|
category: [{category}]
|
||||||
|
flags: [ python ]
|
||||||
|
|
||||||
templates:
|
templates:
|
||||||
imports: from {module_name} import {block_name}
|
imports: {import_line}
|
||||||
make: {module_name}.{block_name}({make_args})
|
make: {make_line}
|
||||||
{callbacks}
|
{callbacks}
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
{parameters}
|
{parameters}
|
||||||
|
|
||||||
@ -163,7 +164,7 @@ inputs:
|
|||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
{outputs}
|
{outputs}
|
||||||
|
{asserts}
|
||||||
documentation: |-
|
documentation: |-
|
||||||
{documentation}
|
{documentation}
|
||||||
|
|
||||||
@ -410,7 +411,7 @@ class OOTExporterMiddleware:
|
|||||||
# Generate and write .block.yml
|
# Generate and write .block.yml
|
||||||
grc_dir = base_path / "grc"
|
grc_dir = base_path / "grc"
|
||||||
yml_content = self._generate_block_yml(
|
yml_content = self._generate_block_yml(
|
||||||
generated, module_name, block_name
|
generated, module_name, block_name, None
|
||||||
)
|
)
|
||||||
yml_file = grc_dir / f"{module_name}_{block_name}.block.yml"
|
yml_file = grc_dir / f"{module_name}_{block_name}.block.yml"
|
||||||
yml_file.write_text(yml_content)
|
yml_file.write_text(yml_content)
|
||||||
@ -513,70 +514,144 @@ class OOTExporterMiddleware:
|
|||||||
# Block YAML Generation
|
# Block YAML Generation
|
||||||
# ──────────────────────────────────────────
|
# ──────────────────────────────────────────
|
||||||
|
|
||||||
|
def generate_grc_yaml(
|
||||||
|
self,
|
||||||
|
generated: GeneratedBlockCode,
|
||||||
|
module_name: str = "custom",
|
||||||
|
category: str | None = None,
|
||||||
|
) -> GrcYamlResult:
|
||||||
|
"""Generate a GRC YAML block definition (.block.yml).
|
||||||
|
|
||||||
|
Produces a YAML file that makes a Python block usable in
|
||||||
|
GNU Radio Companion's graphical editor. Without this file,
|
||||||
|
the block can only be used programmatically.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
generated: Block code from generate_*_block tools
|
||||||
|
module_name: Module namespace (e.g., "custom" → id becomes "custom_blockname")
|
||||||
|
category: GRC palette category (default: module_name)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GrcYamlResult with YAML content and metadata.
|
||||||
|
"""
|
||||||
|
module_name = self._sanitize_module_name(module_name)
|
||||||
|
block_name = self._sanitize_block_name(generated.block_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yaml_content = self._generate_block_yml(
|
||||||
|
generated, module_name, block_name, category
|
||||||
|
)
|
||||||
|
block_id = f"{module_name}_{block_name}"
|
||||||
|
filename = f"{block_id}.block.yml"
|
||||||
|
|
||||||
|
notes = []
|
||||||
|
if not generated.parameters:
|
||||||
|
notes.append("No parameters defined — block has no configurable properties in GRC.")
|
||||||
|
if not any(
|
||||||
|
m for m in self._parse_block_source(generated.source_code).get("callbacks", [])
|
||||||
|
):
|
||||||
|
notes.append(
|
||||||
|
"No set_*() methods found — parameters won't update at runtime. "
|
||||||
|
"Add setter methods to enable GRC callback support."
|
||||||
|
)
|
||||||
|
|
||||||
|
return GrcYamlResult(
|
||||||
|
success=True,
|
||||||
|
block_id=block_id,
|
||||||
|
yaml_content=yaml_content,
|
||||||
|
filename=filename,
|
||||||
|
notes=notes,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Failed to generate GRC YAML")
|
||||||
|
return GrcYamlResult(
|
||||||
|
success=False,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
def _generate_block_yml(
|
def _generate_block_yml(
|
||||||
self,
|
self,
|
||||||
generated: GeneratedBlockCode,
|
generated: GeneratedBlockCode,
|
||||||
module_name: str,
|
module_name: str,
|
||||||
block_name: str,
|
block_name: str,
|
||||||
|
category: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Generate .block.yml content for a block."""
|
"""Generate .block.yml content for a block."""
|
||||||
# Parse source to extract info
|
|
||||||
block_info = self._parse_block_source(generated.source_code)
|
block_info = self._parse_block_source(generated.source_code)
|
||||||
|
|
||||||
# Build label
|
block_id = f"{module_name}_{block_name}"
|
||||||
block_label = block_name.replace("_", " ").title()
|
block_label = block_name.replace("_", " ").title()
|
||||||
|
cat = category or module_name
|
||||||
|
|
||||||
# Build make args
|
# Import and make lines
|
||||||
|
import_line = f"from {module_name} import {block_name}"
|
||||||
make_args = ", ".join([
|
make_args = ", ".join([
|
||||||
f"{p.name}=${{{p.name}}}" for p in generated.parameters
|
f"{p.name}=${{{p.name}}}" for p in generated.parameters
|
||||||
]) if generated.parameters else ""
|
]) if generated.parameters else ""
|
||||||
|
make_line = f"{module_name}.{block_name}({make_args})"
|
||||||
|
|
||||||
# Build callbacks section
|
# Callbacks from detected setter methods
|
||||||
callbacks = ""
|
callbacks = ""
|
||||||
if block_info.get("callbacks"):
|
if block_info.get("callbacks"):
|
||||||
callbacks = "callbacks:\n" + "\n".join([
|
cb_lines = "\n".join([
|
||||||
f" - set_{name}(${{{name}}})" for name in block_info["callbacks"]
|
f" - set_{name}(${{{name}}})" for name in block_info["callbacks"]
|
||||||
])
|
])
|
||||||
|
callbacks = f" callbacks:\n{cb_lines}\n"
|
||||||
|
|
||||||
# Build parameters section
|
# Parameters
|
||||||
params_yml = ""
|
params_yml = ""
|
||||||
for p in generated.parameters:
|
for p in generated.parameters:
|
||||||
params_yml += f"""- id: {p.name}
|
grc_dtype = self._python_to_grc_dtype(p.dtype)
|
||||||
label: {p.name.replace('_', ' ').title()}
|
params_yml += f"- id: {p.name}\n"
|
||||||
dtype: {self._python_to_grc_dtype(p.dtype)}
|
params_yml += f" label: {p.name.replace('_', ' ').title()}\n"
|
||||||
default: {repr(p.default)}
|
params_yml += f" dtype: {grc_dtype}\n"
|
||||||
"""
|
params_yml += f" default: {repr(p.default)}\n"
|
||||||
|
if p.description:
|
||||||
|
params_yml += f" # {p.description}\n"
|
||||||
|
|
||||||
# Build inputs section
|
# Asserts from parameter min/max bounds
|
||||||
|
asserts_yml = ""
|
||||||
|
assert_lines = []
|
||||||
|
for p in generated.parameters:
|
||||||
|
if p.min_value is not None:
|
||||||
|
assert_lines.append(f"- ${{ {p.name} >= {p.min_value} }}")
|
||||||
|
if p.max_value is not None:
|
||||||
|
assert_lines.append(f"- ${{ {p.name} <= {p.max_value} }}")
|
||||||
|
if assert_lines:
|
||||||
|
asserts_yml = "asserts:\n" + "\n".join(assert_lines) + "\n"
|
||||||
|
|
||||||
|
# Inputs
|
||||||
inputs_yml = ""
|
inputs_yml = ""
|
||||||
for i, inp in enumerate(generated.inputs):
|
for i, inp in enumerate(generated.inputs):
|
||||||
inputs_yml += f"""- label: in{i}
|
inputs_yml += f"- label: in{i}\n"
|
||||||
domain: stream
|
inputs_yml += f" domain: stream\n"
|
||||||
dtype: {inp.dtype}
|
inputs_yml += f" dtype: {inp.dtype}\n"
|
||||||
vlen: {inp.vlen}
|
if inp.vlen > 1:
|
||||||
"""
|
inputs_yml += f" vlen: {inp.vlen}\n"
|
||||||
|
|
||||||
# Build outputs section
|
# Outputs
|
||||||
outputs_yml = ""
|
outputs_yml = ""
|
||||||
for i, out in enumerate(generated.outputs):
|
for i, out in enumerate(generated.outputs):
|
||||||
outputs_yml += f"""- label: out{i}
|
outputs_yml += f"- label: out{i}\n"
|
||||||
domain: stream
|
outputs_yml += f" domain: stream\n"
|
||||||
dtype: {out.dtype}
|
outputs_yml += f" dtype: {out.dtype}\n"
|
||||||
vlen: {out.vlen}
|
if out.vlen > 1:
|
||||||
"""
|
outputs_yml += f" vlen: {out.vlen}\n"
|
||||||
|
|
||||||
# Documentation
|
# Documentation from docstring or generation prompt
|
||||||
doc = block_info.get("doc", generated.generation_prompt or "Custom block")
|
doc = block_info.get("doc", generated.generation_prompt or "Custom block")
|
||||||
|
|
||||||
return BLOCK_YML_TEMPLATE.format(
|
return BLOCK_YML_TEMPLATE.format(
|
||||||
module_name=module_name,
|
block_id=block_id,
|
||||||
block_name=block_name,
|
|
||||||
block_label=block_label,
|
block_label=block_label,
|
||||||
make_args=make_args,
|
category=cat,
|
||||||
|
import_line=import_line,
|
||||||
|
make_line=make_line,
|
||||||
callbacks=callbacks,
|
callbacks=callbacks,
|
||||||
parameters=params_yml or "[]",
|
parameters=params_yml or "[]",
|
||||||
inputs=inputs_yml or "[]",
|
inputs=inputs_yml or "[]",
|
||||||
outputs=outputs_yml or "[]",
|
outputs=outputs_yml or "[]",
|
||||||
|
asserts=asserts_yml,
|
||||||
documentation=doc.replace("\n", "\n "),
|
documentation=doc.replace("\n", "\n "),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -677,6 +677,21 @@ class IQAnalysisResult(BaseModel):
|
|||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class GrcYamlResult(BaseModel):
|
||||||
|
"""Result of GRC YAML block definition generation.
|
||||||
|
|
||||||
|
Contains the YAML content for a .block.yml file that makes a
|
||||||
|
block usable in GNU Radio Companion's graphical editor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
block_id: str = "" # e.g. "custom_my_gain"
|
||||||
|
yaml_content: str = ""
|
||||||
|
filename: str = "" # e.g. "custom_my_gain.block.yml"
|
||||||
|
error: str | None = None
|
||||||
|
notes: list[str] = [] # Warnings or suggestions
|
||||||
|
|
||||||
|
|
||||||
class OOTExportResult(BaseModel):
|
class OOTExportResult(BaseModel):
|
||||||
"""Result of exporting an embedded block to full OOT module.
|
"""Result of exporting an embedded block to full OOT module.
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from gnuradio_mcp.prompts.sync_block import SYNC_BLOCK_PROMPT
|
|||||||
from gnuradio_mcp.prompts.basic_block import BASIC_BLOCK_PROMPT
|
from gnuradio_mcp.prompts.basic_block import BASIC_BLOCK_PROMPT
|
||||||
from gnuradio_mcp.prompts.decoder_chain import DECODER_CHAIN_PROMPT
|
from gnuradio_mcp.prompts.decoder_chain import DECODER_CHAIN_PROMPT
|
||||||
from gnuradio_mcp.prompts.common_patterns import COMMON_PATTERNS_PROMPT
|
from gnuradio_mcp.prompts.common_patterns import COMMON_PATTERNS_PROMPT
|
||||||
|
from gnuradio_mcp.prompts.grc_yaml import GRC_YAML_PROMPT
|
||||||
from gnuradio_mcp.prompts.protocol_templates import (
|
from gnuradio_mcp.prompts.protocol_templates import (
|
||||||
PROTOCOL_TEMPLATES,
|
PROTOCOL_TEMPLATES,
|
||||||
BLUETOOTH_LE_TEMPLATE,
|
BLUETOOTH_LE_TEMPLATE,
|
||||||
@ -24,6 +25,7 @@ __all__ = [
|
|||||||
"BASIC_BLOCK_PROMPT",
|
"BASIC_BLOCK_PROMPT",
|
||||||
"DECODER_CHAIN_PROMPT",
|
"DECODER_CHAIN_PROMPT",
|
||||||
"COMMON_PATTERNS_PROMPT",
|
"COMMON_PATTERNS_PROMPT",
|
||||||
|
"GRC_YAML_PROMPT",
|
||||||
# Protocol templates
|
# Protocol templates
|
||||||
"PROTOCOL_TEMPLATES",
|
"PROTOCOL_TEMPLATES",
|
||||||
"BLUETOOTH_LE_TEMPLATE",
|
"BLUETOOTH_LE_TEMPLATE",
|
||||||
|
|||||||
124
src/gnuradio_mcp/prompts/grc_yaml.py
Normal file
124
src/gnuradio_mcp/prompts/grc_yaml.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
"""GRC YAML block definition guidance for LLM-assisted generation."""
|
||||||
|
|
||||||
|
GRC_YAML_PROMPT = """# GRC Block Definition (.block.yml) Guide
|
||||||
|
|
||||||
|
GNU Radio Companion (GRC) requires a `.block.yml` file for each block to
|
||||||
|
appear in the graphical editor. Without this file, blocks are only
|
||||||
|
available programmatically.
|
||||||
|
|
||||||
|
## When to Generate GRC YAML
|
||||||
|
|
||||||
|
Generate a .block.yml when:
|
||||||
|
- The user will use GNU Radio Companion's graphical editor
|
||||||
|
- The block is being exported to an OOT module
|
||||||
|
- The block needs a GUI properties dialog
|
||||||
|
|
||||||
|
You do NOT need a .block.yml for:
|
||||||
|
- Blocks used only in Python scripts
|
||||||
|
- Embedded Python blocks (epy_block) — GRC handles these internally
|
||||||
|
- Temporary/test blocks
|
||||||
|
|
||||||
|
## Key Fields
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: module_block # Unique ID, format: {module}_{block}
|
||||||
|
label: Block Name # Display name in GRC palette
|
||||||
|
category: [Module Name] # GRC palette category
|
||||||
|
flags: [ python ] # Implementation language
|
||||||
|
|
||||||
|
templates:
|
||||||
|
imports: from module import block # Python import
|
||||||
|
make: module.block(gain=${gain}) # Constructor with ${param} substitution
|
||||||
|
callbacks: # Runtime update methods
|
||||||
|
- set_gain(${gain})
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
- id: gain
|
||||||
|
label: Gain
|
||||||
|
dtype: real # real, int, string, bool, enum, raw, complex
|
||||||
|
default: 1.0
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
- label: in0
|
||||||
|
domain: stream # stream (samples) or message (PDUs)
|
||||||
|
dtype: float # float, complex, byte, short, int
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
- label: out0
|
||||||
|
domain: stream
|
||||||
|
dtype: float
|
||||||
|
|
||||||
|
asserts: # Parameter validation
|
||||||
|
- ${ gain >= 0 }
|
||||||
|
|
||||||
|
documentation: |-
|
||||||
|
Block description shown in GRC tooltips.
|
||||||
|
|
||||||
|
file_format: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## GRC dtype vs Python dtype
|
||||||
|
|
||||||
|
| Python type | GRC parameter dtype | GRC port dtype |
|
||||||
|
|-------------|--------------------:|---------------:|
|
||||||
|
| float | real | float |
|
||||||
|
| int | int | int |
|
||||||
|
| str | string | — |
|
||||||
|
| bool | bool | — |
|
||||||
|
| complex | complex | complex |
|
||||||
|
| bytes | — | byte |
|
||||||
|
|
||||||
|
## Callbacks (Runtime Updates)
|
||||||
|
|
||||||
|
For parameters to update at runtime in GRC, the block must have
|
||||||
|
setter methods and the YAML must list them as callbacks:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python block
|
||||||
|
def set_gain(self, gain):
|
||||||
|
self.gain = gain
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# YAML definition
|
||||||
|
templates:
|
||||||
|
callbacks:
|
||||||
|
- set_gain(${gain})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enum Parameters
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: mode
|
||||||
|
label: Mode
|
||||||
|
dtype: enum
|
||||||
|
default: 'low'
|
||||||
|
options: ['low', 'medium', 'high']
|
||||||
|
option_labels: ['Low Power', 'Medium Power', 'High Power']
|
||||||
|
```
|
||||||
|
|
||||||
|
## Message Ports
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
inputs:
|
||||||
|
- domain: message
|
||||||
|
id: pdu_in
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
- domain: message
|
||||||
|
id: pdu_out
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Location
|
||||||
|
|
||||||
|
Install .block.yml files to `$GR_DATA_DIR/grc/blocks/` (typically
|
||||||
|
`/usr/share/gnuradio/grc/blocks/`). OOT modules handle this via
|
||||||
|
CMake — use `export_block_to_oot()` for the full workflow.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Generate block code: `generate_sync_block(...)`
|
||||||
|
2. Generate GRC YAML: `generate_grc_yaml(source_code, block_name, ...)`
|
||||||
|
3. (Optional) Export to OOT: `export_block_to_oot(...)` — includes YAML
|
||||||
|
"""
|
||||||
@ -14,6 +14,7 @@ from gnuradio_mcp.models import (
|
|||||||
BlockTestResult,
|
BlockTestResult,
|
||||||
DecoderPipelineModel,
|
DecoderPipelineModel,
|
||||||
GeneratedBlockCode,
|
GeneratedBlockCode,
|
||||||
|
GrcYamlResult,
|
||||||
IQAnalysisResult,
|
IQAnalysisResult,
|
||||||
OOTExportResult,
|
OOTExportResult,
|
||||||
OOTSkeletonResult,
|
OOTSkeletonResult,
|
||||||
@ -654,6 +655,65 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
return self._protocol_analyzer.get_missing_oot_modules(pipeline)
|
return self._protocol_analyzer.get_missing_oot_modules(pipeline)
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
# GRC YAML Generation
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
|
||||||
|
def generate_grc_yaml(
|
||||||
|
self,
|
||||||
|
source_code: str,
|
||||||
|
block_name: str,
|
||||||
|
module_name: str = "custom",
|
||||||
|
inputs: list[dict[str, Any]] | None = None,
|
||||||
|
outputs: list[dict[str, Any]] | None = None,
|
||||||
|
parameters: list[dict[str, Any]] | None = None,
|
||||||
|
category: str | None = None,
|
||||||
|
) -> GrcYamlResult:
|
||||||
|
"""Generate a GRC YAML block definition for GNU Radio Companion.
|
||||||
|
|
||||||
|
Produces a .block.yml file that makes a Python block appear in
|
||||||
|
GRC's block palette with configurable parameters, typed ports,
|
||||||
|
and runtime callbacks. Without this file, blocks can only be
|
||||||
|
used programmatically — not in GRC's graphical editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_code: Python source code of the block
|
||||||
|
block_name: Block name (e.g., "my_gain")
|
||||||
|
module_name: Module namespace (default "custom")
|
||||||
|
inputs: Input port specs [{"dtype": "float", "vlen": 1}]
|
||||||
|
If not provided, extracted from code (defaults to 1x float)
|
||||||
|
outputs: Output port specs (same format as inputs)
|
||||||
|
parameters: Block params [{"name": "gain", "dtype": "float", "default": 1.0}]
|
||||||
|
category: GRC palette category (default: module_name)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GrcYamlResult with YAML content. Save as
|
||||||
|
grc/{module}_{block}.block.yml in your OOT module.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
For blocks to appear in GRC, the .block.yml must be
|
||||||
|
installed to $GR_DATA_DIR/grc/blocks/ (typically
|
||||||
|
/usr/share/gnuradio/grc/blocks/). OOT modules do this
|
||||||
|
via CMake — see export_block_to_oot() for the full workflow.
|
||||||
|
"""
|
||||||
|
input_items = [SignatureItem(**inp) for inp in (inputs or [{"dtype": "float", "vlen": 1}])]
|
||||||
|
output_items = [SignatureItem(**out) for out in (outputs or [{"dtype": "float", "vlen": 1}])]
|
||||||
|
param_items = [BlockParameter(**p) for p in (parameters or [])]
|
||||||
|
|
||||||
|
generated = GeneratedBlockCode(
|
||||||
|
source_code=source_code,
|
||||||
|
block_name=block_name,
|
||||||
|
block_class="sync_block",
|
||||||
|
inputs=input_items,
|
||||||
|
outputs=output_items,
|
||||||
|
parameters=param_items,
|
||||||
|
is_valid=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._oot_exporter.generate_grc_yaml(
|
||||||
|
generated, module_name, category
|
||||||
|
)
|
||||||
|
|
||||||
# ──────────────────────────────────────────
|
# ──────────────────────────────────────────
|
||||||
# OOT Module Export
|
# OOT Module Export
|
||||||
# ──────────────────────────────────────────
|
# ──────────────────────────────────────────
|
||||||
|
|||||||
@ -104,6 +104,7 @@ class McpBlockDevProvider:
|
|||||||
- validate_block_code: Static code analysis
|
- validate_block_code: Static code analysis
|
||||||
- test_block_in_docker: Isolated testing (if Docker available)
|
- test_block_in_docker: Isolated testing (if Docker available)
|
||||||
- inject_block: Add generated block to flowgraph
|
- inject_block: Add generated block to flowgraph
|
||||||
|
- generate_grc_yaml: Create .block.yml for GNU Radio Companion
|
||||||
- parse_protocol_spec: Extract protocol params from description
|
- parse_protocol_spec: Extract protocol params from description
|
||||||
- generate_decoder_chain: Generate block pipeline from protocol
|
- generate_decoder_chain: Generate block pipeline from protocol
|
||||||
- analyze_iq_file: Detect signals and modulation in IQ captures
|
- analyze_iq_file: Detect signals and modulation in IQ captures
|
||||||
@ -117,6 +118,7 @@ class McpBlockDevProvider:
|
|||||||
- Create protocol-specific decoders
|
- Create protocol-specific decoders
|
||||||
- Build and test new DSP algorithms
|
- Build and test new DSP algorithms
|
||||||
- Analyze captured signals and auto-generate decoders
|
- Analyze captured signals and auto-generate decoders
|
||||||
|
- Generate GRC block definitions for graphical editor use
|
||||||
- Export blocks to distributable OOT modules
|
- Export blocks to distributable OOT modules
|
||||||
"""
|
"""
|
||||||
if self._block_dev_enabled:
|
if self._block_dev_enabled:
|
||||||
@ -205,6 +207,9 @@ class McpBlockDevProvider:
|
|||||||
# Signal analysis tools (Phase 4)
|
# Signal analysis tools (Phase 4)
|
||||||
self._add_tool("analyze_iq_file", p.analyze_iq_file)
|
self._add_tool("analyze_iq_file", p.analyze_iq_file)
|
||||||
|
|
||||||
|
# GRC YAML generation
|
||||||
|
self._add_tool("generate_grc_yaml", p.generate_grc_yaml)
|
||||||
|
|
||||||
# OOT export tools (Phase 5)
|
# OOT export tools (Phase 5)
|
||||||
self._add_tool("generate_oot_skeleton", p.generate_oot_skeleton)
|
self._add_tool("generate_oot_skeleton", p.generate_oot_skeleton)
|
||||||
self._add_tool("export_block_to_oot", p.export_block_to_oot)
|
self._add_tool("export_block_to_oot", p.export_block_to_oot)
|
||||||
@ -474,6 +479,7 @@ class McpBlockDevProvider:
|
|||||||
BASIC_BLOCK_PROMPT,
|
BASIC_BLOCK_PROMPT,
|
||||||
COMMON_PATTERNS_PROMPT,
|
COMMON_PATTERNS_PROMPT,
|
||||||
DECODER_CHAIN_PROMPT,
|
DECODER_CHAIN_PROMPT,
|
||||||
|
GRC_YAML_PROMPT,
|
||||||
SYNC_BLOCK_PROMPT,
|
SYNC_BLOCK_PROMPT,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -513,7 +519,16 @@ class McpBlockDevProvider:
|
|||||||
def get_common_patterns_prompt() -> str:
|
def get_common_patterns_prompt() -> str:
|
||||||
return COMMON_PATTERNS_PROMPT
|
return COMMON_PATTERNS_PROMPT
|
||||||
|
|
||||||
logger.info("Registered 4 prompt template resources")
|
@self._mcp.resource(
|
||||||
|
"prompts://block-generation/grc-yaml",
|
||||||
|
name="grc_yaml_prompt",
|
||||||
|
description="Guide for generating GRC YAML block definitions (.block.yml)",
|
||||||
|
mime_type="text/markdown",
|
||||||
|
)
|
||||||
|
def get_grc_yaml_prompt() -> str:
|
||||||
|
return GRC_YAML_PROMPT
|
||||||
|
|
||||||
|
logger.info("Registered 5 prompt template resources")
|
||||||
|
|
||||||
# ──────────────────────────────────────────
|
# ──────────────────────────────────────────
|
||||||
# Factory
|
# Factory
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from gnuradio_mcp.middlewares.oot_exporter import OOTExporterMiddleware
|
from gnuradio_mcp.middlewares.oot_exporter import OOTExporterMiddleware
|
||||||
from gnuradio_mcp.models import GeneratedBlockCode, SignatureItem
|
from gnuradio_mcp.models import BlockParameter, GeneratedBlockCode, SignatureItem
|
||||||
|
|
||||||
|
|
||||||
class TestOOTExporterMiddleware:
|
class TestOOTExporterMiddleware:
|
||||||
@ -317,3 +317,122 @@ class TestOOTExporterYAMLGeneration:
|
|||||||
assert "label:" in yaml_content
|
assert "label:" in yaml_content
|
||||||
assert "category:" in yaml_content
|
assert "category:" in yaml_content
|
||||||
assert "templates:" in yaml_content
|
assert "templates:" in yaml_content
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenerateGrcYaml:
|
||||||
|
"""Tests for the standalone generate_grc_yaml method."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def exporter(self):
|
||||||
|
return OOTExporterMiddleware()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def gain_block(self):
|
||||||
|
return GeneratedBlockCode(
|
||||||
|
source_code='''
|
||||||
|
import numpy
|
||||||
|
from gnuradio import gr
|
||||||
|
|
||||||
|
class blk(gr.sync_block):
|
||||||
|
"""Configurable gain block."""
|
||||||
|
def __init__(self, gain=1.0):
|
||||||
|
gr.sync_block.__init__(
|
||||||
|
self, name="configurable_gain",
|
||||||
|
in_sig=[numpy.float32], out_sig=[numpy.float32],
|
||||||
|
)
|
||||||
|
self.gain = gain
|
||||||
|
|
||||||
|
def set_gain(self, gain):
|
||||||
|
self.gain = gain
|
||||||
|
|
||||||
|
def work(self, input_items, output_items):
|
||||||
|
output_items[0][:] = input_items[0] * self.gain
|
||||||
|
return len(output_items[0])
|
||||||
|
''',
|
||||||
|
block_name="configurable_gain",
|
||||||
|
block_class="sync_block",
|
||||||
|
inputs=[SignatureItem(dtype="float", vlen=1)],
|
||||||
|
outputs=[SignatureItem(dtype="float", vlen=1)],
|
||||||
|
parameters=[
|
||||||
|
BlockParameter(name="gain", dtype="float", default=1.0,
|
||||||
|
description="Gain factor", min_value=0.0),
|
||||||
|
],
|
||||||
|
is_valid=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_returns_success(self, exporter, gain_block):
|
||||||
|
"""generate_grc_yaml returns a successful result."""
|
||||||
|
result = exporter.generate_grc_yaml(gain_block)
|
||||||
|
assert result.success is True
|
||||||
|
assert result.yaml_content
|
||||||
|
assert result.filename == "custom_configurable_gain.block.yml"
|
||||||
|
assert result.block_id == "custom_configurable_gain"
|
||||||
|
|
||||||
|
def test_yaml_has_flags(self, exporter, gain_block):
|
||||||
|
"""Generated YAML includes flags: [ python ]."""
|
||||||
|
result = exporter.generate_grc_yaml(gain_block)
|
||||||
|
assert "flags: [ python ]" in result.yaml_content
|
||||||
|
|
||||||
|
def test_yaml_has_callbacks(self, exporter, gain_block):
|
||||||
|
"""Setter methods are detected as GRC callbacks."""
|
||||||
|
result = exporter.generate_grc_yaml(gain_block)
|
||||||
|
assert "set_gain(${gain})" in result.yaml_content
|
||||||
|
assert "callbacks:" in result.yaml_content
|
||||||
|
|
||||||
|
def test_yaml_has_asserts_from_min_value(self, exporter, gain_block):
|
||||||
|
"""Parameter min_value generates GRC asserts."""
|
||||||
|
result = exporter.generate_grc_yaml(gain_block)
|
||||||
|
assert "gain >= 0.0" in result.yaml_content
|
||||||
|
|
||||||
|
def test_yaml_has_parameter_dtype_mapping(self, exporter, gain_block):
|
||||||
|
"""Python 'float' maps to GRC 'real' dtype."""
|
||||||
|
result = exporter.generate_grc_yaml(gain_block)
|
||||||
|
assert "dtype: real" in result.yaml_content
|
||||||
|
|
||||||
|
def test_yaml_has_documentation(self, exporter, gain_block):
|
||||||
|
"""Docstring is included as documentation."""
|
||||||
|
result = exporter.generate_grc_yaml(gain_block)
|
||||||
|
assert "Configurable gain block" in result.yaml_content
|
||||||
|
|
||||||
|
def test_custom_module_name(self, exporter, gain_block):
|
||||||
|
"""Module name changes block ID and import."""
|
||||||
|
result = exporter.generate_grc_yaml(gain_block, module_name="dsp")
|
||||||
|
assert result.block_id == "dsp_configurable_gain"
|
||||||
|
assert "from dsp import configurable_gain" in result.yaml_content
|
||||||
|
|
||||||
|
def test_custom_category(self, exporter, gain_block):
|
||||||
|
"""Category can be overridden."""
|
||||||
|
result = exporter.generate_grc_yaml(gain_block, category="Signal Processing")
|
||||||
|
assert "category: [Signal Processing]" in result.yaml_content
|
||||||
|
|
||||||
|
def test_notes_for_no_setters(self, exporter):
|
||||||
|
"""Notes warn about missing setter methods."""
|
||||||
|
block = GeneratedBlockCode(
|
||||||
|
source_code='''
|
||||||
|
import numpy
|
||||||
|
from gnuradio import gr
|
||||||
|
|
||||||
|
class blk(gr.sync_block):
|
||||||
|
def __init__(self, gain=1.0):
|
||||||
|
gr.sync_block.__init__(self, name="no_setters",
|
||||||
|
in_sig=[numpy.float32], out_sig=[numpy.float32])
|
||||||
|
self.gain = gain
|
||||||
|
|
||||||
|
def work(self, input_items, output_items):
|
||||||
|
output_items[0][:] = input_items[0] * self.gain
|
||||||
|
return len(output_items[0])
|
||||||
|
''',
|
||||||
|
block_name="no_setters",
|
||||||
|
block_class="sync_block",
|
||||||
|
inputs=[SignatureItem(dtype="float", vlen=1)],
|
||||||
|
outputs=[SignatureItem(dtype="float", vlen=1)],
|
||||||
|
parameters=[BlockParameter(name="gain", dtype="float", default=1.0)],
|
||||||
|
is_valid=True,
|
||||||
|
)
|
||||||
|
result = exporter.generate_grc_yaml(block)
|
||||||
|
assert any("set_*()" in note for note in result.notes)
|
||||||
|
|
||||||
|
def test_file_format_1(self, exporter, gain_block):
|
||||||
|
"""YAML includes file_format: 1."""
|
||||||
|
result = exporter.generate_grc_yaml(gain_block)
|
||||||
|
assert "file_format: 1" in result.yaml_content
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user