Add comprehensive documentation and comments
- Add detailed docstrings and inline comments to all source files - Create README with installation, usage, and Claude recreation guide - Add pyproject.toml for pip installation - Add MIT LICENSE file - Document architecture, design patterns, and MCP server structure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b0eace2e46
commit
879ed18f67
7 changed files with 1315 additions and 251 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Corey Hahn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
265
README.md
Normal file
265
README.md
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
# Vivado MCP Server
|
||||
|
||||
A Model Context Protocol (MCP) server that enables AI assistants like Claude to directly interact with AMD/Xilinx Vivado FPGA development tools.
|
||||
|
||||
## Features
|
||||
|
||||
- **Session Management**: Start/stop persistent Vivado TCL sessions (avoids 30s startup per command)
|
||||
- **Project Management**: Open/close Vivado projects (.xpr files)
|
||||
- **Design Flow**: Run synthesis, implementation, and bitstream generation
|
||||
- **Reports & Analysis**: Get timing summaries, utilization reports, and design analysis
|
||||
- **Design Queries**: Explore hierarchy, ports, nets, and cells
|
||||
- **Simulation**: Control Vivado's integrated simulator (xsim)
|
||||
- **Raw TCL**: Execute arbitrary Vivado TCL commands for advanced operations
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.10+
|
||||
- AMD/Xilinx Vivado installed (tested with 2023.2+)
|
||||
- Vivado must be in your PATH, or specify the full path when starting a session
|
||||
|
||||
## Installation
|
||||
|
||||
### From GitHub
|
||||
|
||||
```bash
|
||||
git clone https://github.com/coreyhahn/vivado_mcp.git
|
||||
cd vivado_mcp
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### Configure Claude Code
|
||||
|
||||
Add to your Claude Code MCP configuration (`~/.claude/claude_desktop_config.json` or project-level `.mcp.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"vivado": {
|
||||
"command": "vivado-mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or if you want to specify the Python interpreter:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"vivado": {
|
||||
"command": "python",
|
||||
"args": ["-m", "vivado_mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once configured, Claude can interact with Vivado through natural language. Example workflow:
|
||||
|
||||
1. **Start Vivado session**: "Start a Vivado session"
|
||||
2. **Open project**: "Open my project at /path/to/project.xpr"
|
||||
3. **Run synthesis**: "Synthesize the design"
|
||||
4. **Check timing**: "What's the timing summary? Is timing met?"
|
||||
5. **Check utilization**: "Show me the resource utilization"
|
||||
6. **Close session**: "Stop the Vivado session"
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Session Management
|
||||
- `start_session` - Start a persistent Vivado TCL session
|
||||
- `stop_session` - Stop the Vivado session
|
||||
- `session_status` - Get session statistics
|
||||
|
||||
### Project Management
|
||||
- `open_project` - Open a Vivado project (.xpr)
|
||||
- `close_project` - Close the current project
|
||||
- `get_project_info` - Get project information (part, directory, etc.)
|
||||
|
||||
### Design Flow
|
||||
- `run_synthesis` - Run synthesis
|
||||
- `run_implementation` - Run place and route
|
||||
- `generate_bitstream` - Generate bitstream
|
||||
|
||||
### Reports & Analysis
|
||||
- `get_timing_summary` - Get timing summary (WNS, TNS, WHS, THS)
|
||||
- `get_timing_paths` - Get detailed timing paths for failing/critical paths
|
||||
- `get_utilization` - Get resource utilization (LUTs, FFs, BRAMs, DSPs)
|
||||
- `get_clocks` - Get clock information
|
||||
- `get_messages` - Get synthesis/implementation messages
|
||||
|
||||
### Design Queries
|
||||
- `get_design_hierarchy` - Get module/instance hierarchy
|
||||
- `get_ports` - Get top-level ports
|
||||
- `get_nets` - Search for nets
|
||||
- `get_cells` - Search for cells/instances
|
||||
|
||||
### Simulation
|
||||
- `launch_simulation` - Launch behavioral/post-synth/post-impl simulation
|
||||
- `run_simulation` - Run simulation for specified time
|
||||
- `restart_simulation` - Restart from time 0
|
||||
- `close_simulation` - Close the simulator
|
||||
- `get_simulation_time` - Get current simulation time
|
||||
- `get_signal_value` - Get a signal's current value
|
||||
- `get_signal_values` - Get multiple signal values by pattern
|
||||
- `add_signals_to_wave` - Add signals to waveform viewer
|
||||
- `set_simulation_top` - Set the testbench module
|
||||
- `get_simulation_objects` - List signals in a scope
|
||||
- `get_scopes` - List hierarchy scopes
|
||||
- `step_simulation` - Step simulation
|
||||
- `add_breakpoint` - Add signal breakpoint
|
||||
- `remove_breakpoints` - Remove all breakpoints
|
||||
|
||||
### Advanced
|
||||
- `run_tcl` - Execute raw TCL commands
|
||||
- `generate_full_report` - Generate full reports to file
|
||||
- `read_report_section` - Read portions of large reports
|
||||
- `request_feature` - Request new features
|
||||
- `list_feature_requests` - List submitted requests
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ MCP Protocol ┌─────────────────┐
|
||||
│ Claude Code │◄────(JSON-RPC)────────►│ Vivado MCP │
|
||||
│ (AI Client) │ over stdio │ Server │
|
||||
└─────────────────┘ └────────┬────────┘
|
||||
│
|
||||
│ pexpect
|
||||
│ (TCL commands)
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Vivado Process │
|
||||
│ (TCL mode) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
The server maintains a persistent Vivado process in TCL mode. Commands are sent via pexpect and output is captured by waiting for the Vivado prompt. This avoids the ~30 second startup overhead that would occur if Vivado were launched for each command.
|
||||
|
||||
## Recreating This MCP Server with Claude
|
||||
|
||||
This MCP server was created entirely through conversation with Claude. Here's how you can create similar MCP servers:
|
||||
|
||||
### 1. Start with a Clear Goal
|
||||
|
||||
Tell Claude what you want to build:
|
||||
> "I want to create an MCP server that lets you control Vivado FPGA tools. You should be able to start Vivado, open projects, run synthesis, check timing, etc."
|
||||
|
||||
### 2. Describe the Architecture
|
||||
|
||||
Explain the key technical challenges:
|
||||
> "Vivado takes 30 seconds to start, so we need a persistent session. Vivado has a TCL interface we can use. We need to parse Vivado's text output into structured data."
|
||||
|
||||
### 3. Iterate on Tools
|
||||
|
||||
Start with basic tools and add more:
|
||||
1. Session management (start/stop)
|
||||
2. Project management
|
||||
3. Design flow commands
|
||||
4. Reports and queries
|
||||
5. Simulation control
|
||||
|
||||
### 4. Key Design Patterns Used
|
||||
|
||||
**Singleton Session**: Only one Vivado process runs at a time
|
||||
```python
|
||||
_session: Optional[VivadoSession] = None
|
||||
|
||||
def get_session() -> VivadoSession:
|
||||
global _session
|
||||
if _session is None:
|
||||
_session = VivadoSession()
|
||||
return _session
|
||||
```
|
||||
|
||||
**pexpect for Process Management**: Keeps Vivado alive between commands
|
||||
```python
|
||||
self.child = pexpect.spawn(
|
||||
f'{self.vivado_path} -mode tcl -nojournal -nolog',
|
||||
encoding='utf-8',
|
||||
timeout=self.timeout
|
||||
)
|
||||
self.child.expect('Vivado%', timeout=10) # Wait for prompt
|
||||
```
|
||||
|
||||
**Output Parsing**: Convert text reports to structured JSON
|
||||
```python
|
||||
def parse_timing_summary(output: str) -> dict:
|
||||
wns_match = re.search(r"WNS\(ns\)\s*:\s*([-\d.]+)", output)
|
||||
if wns_match:
|
||||
result["wns"] = float(wns_match.group(1))
|
||||
```
|
||||
|
||||
**Response Truncation**: Handle large outputs gracefully
|
||||
```python
|
||||
def truncate_response(content: str, max_chars: int) -> dict:
|
||||
if len(content) > max_chars:
|
||||
return {"content": content[:max_chars], "truncated": True}
|
||||
```
|
||||
|
||||
### 5. MCP Server Structure
|
||||
|
||||
Every MCP server needs:
|
||||
|
||||
```python
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import Tool, TextContent
|
||||
|
||||
server = Server("your-server-name")
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> list[Tool]:
|
||||
return [Tool(name="...", description="...", inputSchema={...})]
|
||||
|
||||
@server.call_tool()
|
||||
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||
# Handle tool calls
|
||||
return [TextContent(type="text", text=json.dumps(result))]
|
||||
|
||||
async def main():
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(read_stream, write_stream,
|
||||
server.create_initialization_options())
|
||||
```
|
||||
|
||||
### 6. Prompt for Creating Your Own MCP Server
|
||||
|
||||
Use this prompt template with Claude:
|
||||
|
||||
```
|
||||
I want to create an MCP server for [YOUR TOOL].
|
||||
|
||||
Background:
|
||||
- [Tool] is a [description] that [what it does]
|
||||
- It has a [CLI/API/etc] interface that accepts [commands/requests]
|
||||
- Key operations I want to support: [list operations]
|
||||
|
||||
Technical considerations:
|
||||
- [Startup time, persistent state, output formats, etc.]
|
||||
|
||||
Please help me create an MCP server with:
|
||||
1. Session/connection management
|
||||
2. Core operations as tools
|
||||
3. Proper error handling
|
||||
4. Structured JSON responses
|
||||
5. Comprehensive code comments
|
||||
|
||||
Start with the basic structure and we'll iterate from there.
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Please feel free to submit issues and pull requests.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Created with [Claude](https://claude.ai) (Anthropic)
|
||||
- Uses the [Model Context Protocol](https://modelcontextprotocol.io) specification
|
||||
- Integrates with [AMD/Xilinx Vivado](https://www.xilinx.com/products/design-tools/vivado.html)
|
||||
68
__init__.py
68
__init__.py
|
|
@ -1,14 +1,76 @@
|
|||
"""Vivado MCP Server - Direct integration with AMD/Xilinx Vivado."""
|
||||
"""
|
||||
Vivado MCP Server - Direct integration with AMD/Xilinx Vivado.
|
||||
|
||||
This package provides a Model Context Protocol (MCP) server that allows
|
||||
AI assistants like Claude to directly interact with AMD/Xilinx Vivado
|
||||
FPGA development tools.
|
||||
|
||||
Features:
|
||||
- Session Management: Start/stop persistent Vivado TCL sessions
|
||||
- Project Management: Open/close Vivado projects (.xpr files)
|
||||
- Design Flow: Run synthesis, implementation, and bitstream generation
|
||||
- Reports: Get timing summaries, utilization, and design analysis
|
||||
- Design Queries: Explore design hierarchy, ports, nets, and cells
|
||||
- Simulation: Control Vivado's integrated simulator (xsim)
|
||||
- Raw TCL: Execute arbitrary Vivado TCL commands
|
||||
|
||||
Installation:
|
||||
pip install -e .
|
||||
|
||||
Or add to your Claude Code MCP configuration:
|
||||
{
|
||||
"mcpServers": {
|
||||
"vivado": {
|
||||
"command": "python",
|
||||
"args": ["-m", "vivado_mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Usage:
|
||||
The server is typically launched by an MCP client (like Claude Code).
|
||||
For manual testing:
|
||||
|
||||
python -m vivado_mcp
|
||||
|
||||
Example workflow (from an AI assistant):
|
||||
1. start_session - Launch Vivado
|
||||
2. open_project - Open a .xpr file
|
||||
3. run_synthesis - Synthesize the design
|
||||
4. get_timing_summary - Check if timing is met
|
||||
5. get_utilization - Check resource usage
|
||||
6. stop_session - Clean up
|
||||
|
||||
Requirements:
|
||||
- Python 3.10+
|
||||
- mcp>=1.0.0 (Model Context Protocol library)
|
||||
- pexpect (for Vivado process management)
|
||||
- AMD/Xilinx Vivado installed and in PATH
|
||||
|
||||
Author: Created with Claude (Anthropic)
|
||||
License: MIT
|
||||
Version: 0.1.0
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from .server import main as _async_main
|
||||
|
||||
# Package version
|
||||
__version__ = "0.1.0"
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for the vivado-mcp console script."""
|
||||
"""
|
||||
Entry point for the vivado-mcp console script.
|
||||
|
||||
This function is called when running:
|
||||
- vivado-mcp (after pip install)
|
||||
- python -m vivado_mcp
|
||||
|
||||
It starts the async MCP server event loop.
|
||||
"""
|
||||
asyncio.run(_async_main())
|
||||
|
||||
|
||||
__all__ = ["main"]
|
||||
# Public API - what gets imported with "from vivado_mcp import *"
|
||||
__all__ = ["main", "__version__"]
|
||||
|
|
|
|||
18
__main__.py
18
__main__.py
|
|
@ -1,7 +1,23 @@
|
|||
"""Entry point for running the Vivado MCP server."""
|
||||
"""
|
||||
Entry point for running the Vivado MCP server as a module.
|
||||
|
||||
This allows running the server with:
|
||||
python -m vivado_mcp
|
||||
|
||||
Which is equivalent to:
|
||||
vivado-mcp (after pip install)
|
||||
|
||||
The server communicates via stdin/stdout using the MCP protocol,
|
||||
so it's typically launched by an MCP client like Claude Code rather
|
||||
than run directly from the command line.
|
||||
|
||||
For testing, you can run it directly, but you'll need to send
|
||||
properly formatted JSON-RPC messages to stdin.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from .server import main
|
||||
|
||||
# Run the async main function when this module is executed directly
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
|
|||
46
pyproject.toml
Normal file
46
pyproject.toml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "vivado-mcp"
|
||||
version = "0.1.0"
|
||||
description = "MCP server for AMD/Xilinx Vivado FPGA development - enables AI assistants to control Vivado"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{name = "Created with Claude", email = "noreply@anthropic.com"}
|
||||
]
|
||||
keywords = ["mcp", "vivado", "fpga", "xilinx", "amd", "claude", "ai"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
||||
]
|
||||
dependencies = [
|
||||
"mcp>=1.0.0",
|
||||
"pexpect>=4.8.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
vivado-mcp = "vivado_mcp:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/coreyhahn/vivado_mcp"
|
||||
Repository = "https://github.com/coreyhahn/vivado_mcp"
|
||||
Issues = "https://github.com/coreyhahn/vivado_mcp/issues"
|
||||
|
||||
# Since the repo root is the package itself, we map the package name to "."
|
||||
[tool.setuptools]
|
||||
packages = ["vivado_mcp"]
|
||||
package-dir = {"vivado_mcp" = "."}
|
||||
|
||||
# Include data files
|
||||
[tool.setuptools.package-data]
|
||||
vivado_mcp = ["data/*.json"]
|
||||
|
|
@ -1,4 +1,51 @@
|
|||
"""Vivado TCL session manager - maintains persistent Vivado process using pexpect."""
|
||||
"""
|
||||
Vivado TCL Session Manager - Maintains a persistent Vivado process using pexpect.
|
||||
|
||||
This module provides the core Vivado interaction layer for the MCP server.
|
||||
It manages a persistent Vivado TCL session, avoiding the ~30 second startup
|
||||
overhead that would occur if Vivado were launched for each command.
|
||||
|
||||
Architecture:
|
||||
The VivadoSession class spawns Vivado in TCL mode (-mode tcl) using pexpect.
|
||||
Commands are sent via sendline() and output is captured by waiting for the
|
||||
Vivado prompt (Vivado%). The session stays alive between commands, maintaining
|
||||
state (open projects, synthesized designs, etc.).
|
||||
|
||||
Key Design Decisions:
|
||||
1. Singleton Pattern: A global _session instance is used to ensure only one
|
||||
Vivado process runs at a time. Use get_session() to access it.
|
||||
|
||||
2. Thread Safety: A threading lock protects command execution to prevent
|
||||
interleaved commands if multiple async tasks try to use Vivado.
|
||||
|
||||
3. Prompt-Based Parsing: We wait for "Vivado%" prompt to know when a command
|
||||
completes. Output between command send and prompt is captured.
|
||||
|
||||
4. Error Detection: Success/failure is determined by checking for error
|
||||
keywords in the output (ERROR:, invalid command, etc.).
|
||||
|
||||
5. Statistics Tracking: Command count, timing, and error counts are tracked
|
||||
for debugging and performance analysis.
|
||||
|
||||
Usage:
|
||||
from vivado_session import get_session
|
||||
|
||||
session = get_session()
|
||||
session.start() # Launch Vivado
|
||||
|
||||
result = session.run_tcl("open_project /path/to/project.xpr")
|
||||
if result.success:
|
||||
print(f"Project opened in {result.elapsed_ms}ms")
|
||||
|
||||
session.stop() # Clean shutdown
|
||||
|
||||
Dependencies:
|
||||
- pexpect: For spawning and interacting with Vivado process
|
||||
- Vivado: Must be installed and in PATH (or specify path explicitly)
|
||||
|
||||
Author: Created with Claude (Anthropic)
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
import pexpect
|
||||
import time
|
||||
|
|
@ -9,9 +56,32 @@ from datetime import datetime
|
|||
import threading
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DATA CLASSES
|
||||
# =============================================================================
|
||||
|
||||
@dataclass
|
||||
class CommandResult:
|
||||
"""Result from a Vivado TCL command."""
|
||||
"""
|
||||
Result from executing a Vivado TCL command.
|
||||
|
||||
This dataclass encapsulates all information about a command execution,
|
||||
making it easy to check success, access output, and measure performance.
|
||||
|
||||
Attributes:
|
||||
command: The TCL command that was executed
|
||||
output: The captured output from Vivado (excluding prompts)
|
||||
return_value: "0" for success, "1" for failure (string for JSON compat)
|
||||
success: Boolean indicating if the command succeeded
|
||||
elapsed_ms: Time taken to execute the command in milliseconds
|
||||
timestamp: ISO format timestamp of when the command completed
|
||||
|
||||
Example:
|
||||
result = session.run_tcl("get_property PART [current_project]")
|
||||
if result.success:
|
||||
print(f"Target part: {result.output}")
|
||||
print(f"Took {result.elapsed_ms:.1f}ms")
|
||||
"""
|
||||
command: str
|
||||
output: str
|
||||
return_value: str
|
||||
|
|
@ -20,36 +90,96 @@ class CommandResult:
|
|||
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# VIVADO SESSION CLASS
|
||||
# =============================================================================
|
||||
|
||||
class VivadoSession:
|
||||
"""
|
||||
Manages a persistent Vivado TCL session using pexpect.
|
||||
|
||||
Vivado is started once and kept running. Commands are sent and output
|
||||
is captured using pexpect's expect/sendline interface.
|
||||
is captured using pexpect's expect/sendline interface. This avoids the
|
||||
~30 second startup time that would be incurred for each command.
|
||||
|
||||
The session maintains state between commands, so you can open a project,
|
||||
run synthesis, and then query results - all using the same Vivado instance.
|
||||
|
||||
Attributes:
|
||||
vivado_path: Path to the Vivado executable
|
||||
timeout: Maximum time to wait for command completion (seconds)
|
||||
child: The pexpect spawn object (Vivado process)
|
||||
is_running: Whether Vivado is currently running
|
||||
current_project: Path to currently open project (if any)
|
||||
stats: Dictionary of session statistics
|
||||
|
||||
Thread Safety:
|
||||
A lock (_lock) protects command execution. Multiple threads can
|
||||
safely call run_tcl(), though commands will be serialized.
|
||||
|
||||
Example:
|
||||
with VivadoSession() as session:
|
||||
session.run_tcl("open_project /path/to/project.xpr")
|
||||
result = session.run_tcl("report_timing_summary -return_string")
|
||||
print(result.output)
|
||||
# Vivado is automatically stopped when exiting the context
|
||||
"""
|
||||
|
||||
# Unique marker to detect end of command output (use something that won't appear in normal output)
|
||||
# Unique marker that won't appear in normal Vivado output
|
||||
# Used internally for sentinel-based parsing (not currently used but reserved)
|
||||
SENTINEL = "XYZZY_MCP_9f8e7d6c_DONE"
|
||||
|
||||
def __init__(self, vivado_path: str = "vivado", timeout: float = 300.0):
|
||||
"""
|
||||
Initialize the Vivado session manager.
|
||||
|
||||
Args:
|
||||
vivado_path: Path to Vivado executable. Defaults to "vivado" which
|
||||
assumes it's in the system PATH. Can be an absolute path
|
||||
like "/tools/Xilinx/Vivado/2023.2/bin/vivado".
|
||||
timeout: Maximum time in seconds to wait for any command to complete.
|
||||
Defaults to 300s (5 minutes) to handle long operations like
|
||||
synthesis and implementation.
|
||||
"""
|
||||
self.vivado_path = vivado_path
|
||||
self.timeout = timeout
|
||||
self.child: Optional[pexpect.spawn] = None
|
||||
self.is_running = False
|
||||
self.current_project: Optional[str] = None
|
||||
|
||||
# Thread lock for command execution
|
||||
# Ensures only one command runs at a time even with async callers
|
||||
self._lock = threading.Lock()
|
||||
|
||||
# Statistics
|
||||
# Statistics tracking for debugging and performance analysis
|
||||
self.stats = {
|
||||
"session_start": None,
|
||||
"commands_run": 0,
|
||||
"total_command_time_ms": 0,
|
||||
"errors": 0,
|
||||
"command_history": []
|
||||
"session_start": None, # ISO timestamp when session started
|
||||
"commands_run": 0, # Total commands executed
|
||||
"total_command_time_ms": 0, # Sum of all command times
|
||||
"errors": 0, # Count of failed commands
|
||||
"command_history": [] # Last 100 commands (for debugging)
|
||||
}
|
||||
|
||||
def start(self) -> CommandResult:
|
||||
"""Start the Vivado TCL session."""
|
||||
"""
|
||||
Start the Vivado TCL session.
|
||||
|
||||
This spawns a new Vivado process in TCL mode with:
|
||||
- No journal file (-nojournal): Avoids cluttering directory
|
||||
- No log file (-nolog): Output goes to pexpect instead
|
||||
|
||||
The function waits for Vivado's startup banner ("Start of session")
|
||||
and then confirms readiness by waiting for the "Vivado%" prompt.
|
||||
|
||||
Returns:
|
||||
CommandResult with success=True if Vivado started successfully,
|
||||
or success=False with error message if startup failed.
|
||||
|
||||
Note:
|
||||
If already running, returns success immediately without restarting.
|
||||
Vivado startup typically takes 20-30 seconds.
|
||||
"""
|
||||
# Don't restart if already running
|
||||
if self.is_running:
|
||||
return CommandResult(
|
||||
command="start",
|
||||
|
|
@ -62,30 +192,36 @@ class VivadoSession:
|
|||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Start Vivado with pexpect
|
||||
# Spawn Vivado in TCL mode
|
||||
# -mode tcl: Interactive TCL shell (no GUI)
|
||||
# -nojournal: Don't create vivado.jou files
|
||||
# -nolog: Don't create vivado.log files
|
||||
self.child = pexpect.spawn(
|
||||
f'{self.vivado_path} -mode tcl -nojournal -nolog',
|
||||
encoding='utf-8',
|
||||
timeout=self.timeout,
|
||||
echo=False # Don't echo commands back
|
||||
echo=False # Don't echo commands back to us
|
||||
)
|
||||
|
||||
# Wait for Vivado to start (look for startup banner)
|
||||
# Wait for Vivado to display its startup banner
|
||||
# This indicates Vivado has loaded and is ready to accept commands
|
||||
self.child.expect('Start of session', timeout=120)
|
||||
|
||||
# Give it a moment to fully initialize
|
||||
# Brief pause to let Vivado fully initialize
|
||||
time.sleep(1)
|
||||
|
||||
# Drain any remaining startup output
|
||||
# Drain any remaining startup output to clear the buffer
|
||||
try:
|
||||
self.child.read_nonblocking(size=100000, timeout=1)
|
||||
except (pexpect.TIMEOUT, pexpect.EOF):
|
||||
pass
|
||||
pass # Expected - no more data to read
|
||||
|
||||
# Wait for prompt to confirm ready
|
||||
self.child.sendline("") # Empty command to get prompt
|
||||
# Send empty command to confirm we get a prompt back
|
||||
# This validates that Vivado is responsive
|
||||
self.child.sendline("")
|
||||
self.child.expect('Vivado%', timeout=10)
|
||||
|
||||
# Mark session as running and record start time
|
||||
self.is_running = True
|
||||
self.stats["session_start"] = datetime.now().isoformat()
|
||||
|
||||
|
|
@ -100,6 +236,7 @@ class VivadoSession:
|
|||
)
|
||||
|
||||
except pexpect.TIMEOUT:
|
||||
# Vivado didn't respond in time
|
||||
self.is_running = False
|
||||
elapsed = (time.time() - start_time) * 1000
|
||||
return CommandResult(
|
||||
|
|
@ -110,6 +247,7 @@ class VivadoSession:
|
|||
elapsed_ms=elapsed
|
||||
)
|
||||
except Exception as e:
|
||||
# Other errors (file not found, permissions, etc.)
|
||||
self.is_running = False
|
||||
elapsed = (time.time() - start_time) * 1000
|
||||
return CommandResult(
|
||||
|
|
@ -124,12 +262,39 @@ class VivadoSession:
|
|||
"""
|
||||
Execute a TCL command and return the result.
|
||||
|
||||
This is the primary interface for interacting with Vivado. The command
|
||||
is sent to the Vivado TCL shell, and output is captured by waiting for
|
||||
the next "Vivado%" prompt.
|
||||
|
||||
Args:
|
||||
command: TCL command to execute
|
||||
command: TCL command to execute. Can be any valid Vivado TCL command.
|
||||
Examples:
|
||||
- "open_project /path/to/project.xpr"
|
||||
- "report_timing_summary -return_string"
|
||||
- "get_property PART [current_project]"
|
||||
|
||||
Returns:
|
||||
CommandResult with output and status
|
||||
CommandResult containing:
|
||||
- output: The command's output (stdout from Vivado)
|
||||
- success: True if no error keywords were found in output
|
||||
- elapsed_ms: Execution time in milliseconds
|
||||
|
||||
Thread Safety:
|
||||
This method is thread-safe. A lock ensures only one command
|
||||
executes at a time.
|
||||
|
||||
Output Parsing:
|
||||
The raw pexpect output includes the echoed command and prompts.
|
||||
This method strips those to return only the meaningful output.
|
||||
|
||||
Error Detection:
|
||||
Success is determined by checking for error keywords in output:
|
||||
- "error:" - Vivado error messages
|
||||
- "invalid command" - TCL syntax errors
|
||||
- "can't read" - Variable/file access errors
|
||||
- "wrong # args" - Argument count errors
|
||||
"""
|
||||
# Check session is running
|
||||
if not self.is_running:
|
||||
return CommandResult(
|
||||
command=command,
|
||||
|
|
@ -139,47 +304,52 @@ class VivadoSession:
|
|||
elapsed_ms=0
|
||||
)
|
||||
|
||||
# Serialize command execution with a lock
|
||||
with self._lock:
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Clear any pending output first
|
||||
# Clear any pending output from previous commands
|
||||
# This ensures we only capture this command's output
|
||||
try:
|
||||
self.child.read_nonblocking(size=100000, timeout=0.1)
|
||||
except (pexpect.TIMEOUT, pexpect.EOF):
|
||||
pass
|
||||
pass # Expected - buffer was empty
|
||||
|
||||
# Send the command
|
||||
# Send the command to Vivado
|
||||
self.child.sendline(command)
|
||||
|
||||
# Wait for Vivado prompt (indicates command completed)
|
||||
# Wait for the Vivado prompt indicating command completion
|
||||
# The prompt appears after Vivado finishes processing
|
||||
self.child.expect('Vivado%', timeout=self.timeout)
|
||||
|
||||
# Get the output (everything before the prompt)
|
||||
# Get everything that was output before the prompt
|
||||
raw_output = self.child.before
|
||||
|
||||
# Parse output: extract content after command echo
|
||||
# Parse the output to extract meaningful content
|
||||
# Raw output includes: command echo, actual output, whitespace
|
||||
lines = raw_output.replace('\r', '').split('\n')
|
||||
clean_lines = []
|
||||
found_command = False
|
||||
|
||||
# Normalize command for matching
|
||||
# Normalize command for matching (handle whitespace differences)
|
||||
cmd_normalized = command.strip()
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
|
||||
# Look for the command echo
|
||||
# Skip lines until we find the echoed command
|
||||
# Everything before is leftover from previous operations
|
||||
if not found_command:
|
||||
if cmd_normalized in stripped:
|
||||
found_command = True
|
||||
continue
|
||||
|
||||
# Skip Vivado prompts
|
||||
# Skip Vivado prompts in output
|
||||
if stripped == 'Vivado%' or stripped.startswith('Vivado%'):
|
||||
continue
|
||||
|
||||
# Skip empty lines
|
||||
# Skip empty lines for cleaner output
|
||||
if not stripped:
|
||||
continue
|
||||
|
||||
|
|
@ -189,11 +359,12 @@ class VivadoSession:
|
|||
|
||||
elapsed = (time.time() - start_time) * 1000
|
||||
|
||||
# Check for errors in output
|
||||
# Determine success by checking for error indicators
|
||||
# Vivado prefixes errors with specific keywords
|
||||
success = not any(err in output.lower() for err in
|
||||
["error:", "invalid command", "can't read", "wrong # args"])
|
||||
|
||||
# Update stats
|
||||
# Update statistics
|
||||
self.stats["commands_run"] += 1
|
||||
self.stats["total_command_time_ms"] += elapsed
|
||||
if not success:
|
||||
|
|
@ -207,7 +378,7 @@ class VivadoSession:
|
|||
elapsed_ms=elapsed
|
||||
)
|
||||
|
||||
# Keep last 100 commands in history
|
||||
# Add to command history (keep last 100 for debugging)
|
||||
self.stats["command_history"].append({
|
||||
"command": command,
|
||||
"success": success,
|
||||
|
|
@ -220,6 +391,7 @@ class VivadoSession:
|
|||
return result
|
||||
|
||||
except pexpect.TIMEOUT:
|
||||
# Command took too long - might be hung or very long operation
|
||||
elapsed = (time.time() - start_time) * 1000
|
||||
self.stats["errors"] += 1
|
||||
return CommandResult(
|
||||
|
|
@ -230,6 +402,7 @@ class VivadoSession:
|
|||
elapsed_ms=elapsed
|
||||
)
|
||||
except Exception as e:
|
||||
# Unexpected error during command execution
|
||||
elapsed = (time.time() - start_time) * 1000
|
||||
self.stats["errors"] += 1
|
||||
return CommandResult(
|
||||
|
|
@ -241,7 +414,19 @@ class VivadoSession:
|
|||
)
|
||||
|
||||
def stop(self) -> CommandResult:
|
||||
"""Stop the Vivado session."""
|
||||
"""
|
||||
Stop the Vivado session gracefully.
|
||||
|
||||
Sends the "exit" command to Vivado and waits for the process to
|
||||
terminate. If graceful exit fails, force-closes the process.
|
||||
|
||||
Returns:
|
||||
CommandResult with success=True (stopping always "succeeds"
|
||||
even if we had to force-close)
|
||||
|
||||
Note:
|
||||
Safe to call even if session is not running.
|
||||
"""
|
||||
if not self.is_running:
|
||||
return CommandResult(
|
||||
command="stop",
|
||||
|
|
@ -254,15 +439,18 @@ class VivadoSession:
|
|||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Send exit command for graceful shutdown
|
||||
self.child.sendline('exit')
|
||||
# Wait for process to terminate (EOF on stdout)
|
||||
self.child.expect(pexpect.EOF, timeout=30)
|
||||
except Exception:
|
||||
# Force close if graceful exit fails
|
||||
# If graceful exit fails, force-terminate the process
|
||||
try:
|
||||
self.child.close(force=True)
|
||||
except:
|
||||
pass
|
||||
pass # Best effort - process might already be dead
|
||||
|
||||
# Update state
|
||||
self.is_running = False
|
||||
self.current_project = None
|
||||
elapsed = (time.time() - start_time) * 1000
|
||||
|
|
@ -276,28 +464,82 @@ class VivadoSession:
|
|||
)
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""Get session statistics."""
|
||||
"""
|
||||
Get session statistics for monitoring and debugging.
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- is_running: Whether session is active
|
||||
- current_project: Path to open project (or None)
|
||||
- session_start: ISO timestamp when session started
|
||||
- commands_run: Total commands executed
|
||||
- total_command_time_ms: Sum of all command times
|
||||
- errors: Count of failed commands
|
||||
- avg_command_time_ms: Average command time (if commands > 0)
|
||||
- command_history: Last 100 commands with timing info
|
||||
"""
|
||||
stats = self.stats.copy()
|
||||
stats["is_running"] = self.is_running
|
||||
stats["current_project"] = self.current_project
|
||||
|
||||
# Calculate average command time if we have data
|
||||
if self.stats["commands_run"] > 0:
|
||||
stats["avg_command_time_ms"] = self.stats["total_command_time_ms"] / self.stats["commands_run"]
|
||||
stats["avg_command_time_ms"] = (
|
||||
self.stats["total_command_time_ms"] / self.stats["commands_run"]
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Context manager entry - start the session.
|
||||
|
||||
Example:
|
||||
with VivadoSession() as session:
|
||||
session.run_tcl("...")
|
||||
"""
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""
|
||||
Context manager exit - stop the session.
|
||||
|
||||
Ensures Vivado is properly shut down even if an exception occurred.
|
||||
"""
|
||||
self.stop()
|
||||
|
||||
|
||||
# Singleton session instance
|
||||
# =============================================================================
|
||||
# SINGLETON SESSION MANAGEMENT
|
||||
# =============================================================================
|
||||
|
||||
# Global singleton session instance
|
||||
# Using a singleton ensures only one Vivado process runs at a time
|
||||
_session: Optional[VivadoSession] = None
|
||||
|
||||
|
||||
def get_session() -> VivadoSession:
|
||||
"""Get or create the global Vivado session."""
|
||||
"""
|
||||
Get or create the global Vivado session.
|
||||
|
||||
This function implements the singleton pattern for VivadoSession.
|
||||
The first call creates a new session; subsequent calls return the
|
||||
same instance.
|
||||
|
||||
Returns:
|
||||
The global VivadoSession instance
|
||||
|
||||
Example:
|
||||
session = get_session()
|
||||
session.start()
|
||||
# ... use session ...
|
||||
session.stop()
|
||||
|
||||
Note:
|
||||
The session is created lazily (on first access) and is NOT
|
||||
automatically started. Call session.start() explicitly.
|
||||
"""
|
||||
global _session
|
||||
if _session is None:
|
||||
_session = VivadoSession()
|
||||
|
|
@ -305,7 +547,18 @@ def get_session() -> VivadoSession:
|
|||
|
||||
|
||||
def reset_session():
|
||||
"""Reset the global session (stop if running)."""
|
||||
"""
|
||||
Reset the global session (stop if running and clear instance).
|
||||
|
||||
Use this to force a fresh Vivado session, for example after
|
||||
recovering from an error or when changing Vivado versions.
|
||||
|
||||
This function:
|
||||
1. Stops the current session if running
|
||||
2. Clears the singleton instance
|
||||
|
||||
The next call to get_session() will create a fresh instance.
|
||||
"""
|
||||
global _session
|
||||
if _session is not None and _session.is_running:
|
||||
_session.stop()
|
||||
|
|
|
|||
Loading…
Reference in a new issue