From ec1d8942e9d4fc0bd65dca36787476319be5e37a Mon Sep 17 00:00:00 2001 From: cah Date: Sun, 18 Jan 2026 08:41:50 -0700 Subject: [PATCH] Initial commit: Vivado MCP Server Co-Authored-By: Claude Opus 4.5 --- __init__.py | 14 + __main__.py | 7 + __pycache__/__init__.cpython-313.pyc | Bin 0 -> 588 bytes __pycache__/__main__.cpython-313.pyc | Bin 0 -> 389 bytes __pycache__/server.cpython-313.pyc | Bin 0 -> 48666 bytes __pycache__/vivado_session.cpython-313.pyc | Bin 0 -> 10910 bytes data/__init__.py | 1 + server.py | 1591 ++++++++++++++++++++ vivado_session.py | 312 ++++ 9 files changed, 1925 insertions(+) create mode 100644 __init__.py create mode 100644 __main__.py create mode 100644 __pycache__/__init__.cpython-313.pyc create mode 100644 __pycache__/__main__.cpython-313.pyc create mode 100644 __pycache__/server.cpython-313.pyc create mode 100644 __pycache__/vivado_session.cpython-313.pyc create mode 100644 data/__init__.py create mode 100644 server.py create mode 100644 vivado_session.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0951a52 --- /dev/null +++ b/__init__.py @@ -0,0 +1,14 @@ +"""Vivado MCP Server - Direct integration with AMD/Xilinx Vivado.""" + +import asyncio +from .server import main as _async_main + +__version__ = "0.1.0" + + +def main(): + """Entry point for the vivado-mcp console script.""" + asyncio.run(_async_main()) + + +__all__ = ["main"] diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..50bb2dc --- /dev/null +++ b/__main__.py @@ -0,0 +1,7 @@ +"""Entry point for running the Vivado MCP server.""" + +import asyncio +from .server import main + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..171ce91e3838acb8e152edfde55816281c5f63aa GIT binary patch literal 588 zcmXw0L2J}N6rM@4$=bNB6ufxQ(TfIbQVV-4MRC2jw1@!_dKf}7ZU(Y5%S(23_tf^% z-nIS*e~1BL755MLCTTv%m+!s2eDBSh^H zM7ku$$SvM?vv&!6KOx7xE*##9q3M8NWI_XL6~0jwEZI!si0qRFR^@^%nv?8I(-}*( zDs>@PnSz{E;Zb1MUQ#YqDs^qcT-82rLT(2BLDt5)9!9yIiYQI;2==nQDAgH8MKaF> zN7JbFZzShZ$%^xEwghvwhVGy!?;)L#zm9)CJ{y1YzYcyn55ZhrQxbT!$9bkx&cVfG z?Uv2XTa&)Y_?+Vt5bSZuso8{ZW5_oq6W7vca+KeLY5S_hUFTt3!Myn;=H8}{T5wQ^)4@Eo>i@>tnt#=2x-LUz9q_oji#UJ7C z5kU_GAyvA-6}Il?V~TlicHg|2DeLuJF!7h9*^9xsEw({6q`Wia4MNZmqRDD*_=_QJMbEv$=&3X=0o7FEV9GUmBOg|S#gj1?6=oXRLu zg}d(5_8=>jJQwC4n>p`LTDzj_rz#hI#8Y2);*KAw*i?N!js?r3fj?QTv7*~i?ZLFD zp5!VX_r66yG=FjJY4(8+zDVsccnF@!tNTgzml(*Q-ZScd D#P(x6 literal 0 HcmV?d00001 diff --git a/__pycache__/server.cpython-313.pyc b/__pycache__/server.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3de013929728d0882ce58f4bf2cb8d9f0f3d03d4 GIT binary patch literal 48666 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 Af&c&j literal 0 HcmV?d00001 diff --git a/__pycache__/vivado_session.cpython-313.pyc b/__pycache__/vivado_session.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a7c7135dc2554717591542ba741c5c3f20d8a1e GIT binary patch literal 10910 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 Zw{ list[dict]: + """Load feature requests from file.""" + if FEATURE_REQUESTS_FILE.exists(): + try: + return json.loads(FEATURE_REQUESTS_FILE.read_text()) + except (json.JSONDecodeError, IOError): + return [] + return [] + + +def save_feature_request(request: dict) -> None: + """Save a feature request to the file.""" + requests = load_feature_requests() + requests.append(request) + FEATURE_REQUESTS_FILE.parent.mkdir(parents=True, exist_ok=True) + FEATURE_REQUESTS_FILE.write_text(json.dumps(requests, indent=2)) + + +def truncate_response(content: str, max_chars: int = MAX_RESPONSE_CHARS) -> dict: + """ + Truncate response content if it exceeds max_chars. + + Returns dict with: + - content: truncated content (if needed) + - truncated: bool indicating if truncation occurred + - total_chars: original content length + - total_lines: original line count + """ + total_chars = len(content) + total_lines = content.count('\n') + 1 + + if total_chars <= max_chars: + return { + "content": content, + "truncated": False, + "total_chars": total_chars, + "total_lines": total_lines + } + + # Truncate at a line boundary if possible + truncated_content = content[:max_chars] + last_newline = truncated_content.rfind('\n') + if last_newline > max_chars * 0.8: # Only use newline if we keep >80% of content + truncated_content = truncated_content[:last_newline] + + truncated_lines = truncated_content.count('\n') + 1 + + return { + "content": truncated_content, + "truncated": True, + "total_chars": total_chars, + "total_lines": total_lines, + "returned_chars": len(truncated_content), + "returned_lines": truncated_lines, + "truncation_message": f"Output truncated ({total_chars:,} chars -> {len(truncated_content):,} chars). Use generate_full_report for complete output." + } + + +def ensure_reports_dir() -> Path: + """Ensure the reports directory exists and clean up old reports.""" + REPORTS_DIR.mkdir(parents=True, exist_ok=True) + + # Clean up old reports (older than REPORT_CACHE_HOURS) + cutoff = datetime.now().timestamp() - (REPORT_CACHE_HOURS * 3600) + for report_file in REPORTS_DIR.glob("*.txt"): + try: + if report_file.stat().st_mtime < cutoff: + report_file.unlink() + # Also remove from cache if present + report_id = report_file.stem + _report_cache.pop(report_id, None) + except OSError: + pass + + return REPORTS_DIR + + +def generate_report_id() -> str: + """Generate a unique report ID.""" + return str(uuid.uuid4())[:8] + + +def get_hierarchy_depth(path: str) -> int: + """Get the depth of a hierarchical path.""" + return path.count('/') + +# Create the MCP server +server = Server("vivado") + + +# ============================================================================ +# Helper functions to parse Vivado output +# ============================================================================ + +def parse_timing_summary(output: str) -> dict: + """Parse timing summary report into structured data.""" + result = { + "wns": None, # Worst Negative Slack + "tns": None, # Total Negative Slack + "whs": None, # Worst Hold Slack + "ths": None, # Total Hold Slack + "wpws": None, # Worst Pulse Width Slack + "tpws": None, # Total Pulse Width Slack + "failing_endpoints": 0, + "met": False, + "raw": output + } + + # Parse WNS/TNS + wns_match = re.search(r"WNS\(ns\)\s*:\s*([-\d.]+)", output) + tns_match = re.search(r"TNS\(ns\)\s*:\s*([-\d.]+)", output) + if wns_match: + result["wns"] = float(wns_match.group(1)) + if tns_match: + result["tns"] = float(tns_match.group(1)) + + # Parse WHS/THS + whs_match = re.search(r"WHS\(ns\)\s*:\s*([-\d.]+)", output) + ths_match = re.search(r"THS\(ns\)\s*:\s*([-\d.]+)", output) + if whs_match: + result["whs"] = float(whs_match.group(1)) + if ths_match: + result["ths"] = float(ths_match.group(1)) + + # Parse failing endpoints + fail_match = re.search(r"(\d+)\s+failing\s+endpoint", output, re.IGNORECASE) + if fail_match: + result["failing_endpoints"] = int(fail_match.group(1)) + + # Check if timing is met + if result["wns"] is not None and result["whs"] is not None: + result["met"] = result["wns"] >= 0 and result["whs"] >= 0 + + return result + + +def parse_utilization(output: str) -> dict: + """Parse utilization report into structured data.""" + result = { + "lut": {"used": 0, "available": 0, "percent": 0}, + "ff": {"used": 0, "available": 0, "percent": 0}, + "bram": {"used": 0, "available": 0, "percent": 0}, + "dsp": {"used": 0, "available": 0, "percent": 0}, + "io": {"used": 0, "available": 0, "percent": 0}, + "raw": output + } + + # Parse different resource types + patterns = { + "lut": r"(?:Slice LUTs|CLB LUTs)\s*\|\s*(\d+)\s*\|\s*\d+\s*\|\s*(\d+)\s*\|\s*([\d.]+)", + "ff": r"(?:Slice Registers|CLB Registers)\s*\|\s*(\d+)\s*\|\s*\d+\s*\|\s*(\d+)\s*\|\s*([\d.]+)", + "bram": r"Block RAM Tile\s*\|\s*(\d+\.?\d*)\s*\|\s*\d+\s*\|\s*(\d+\.?\d*)\s*\|\s*([\d.]+)", + "dsp": r"DSPs?\s*\|\s*(\d+)\s*\|\s*\d+\s*\|\s*(\d+)\s*\|\s*([\d.]+)", + "io": r"(?:Bonded IOB|Bonded User I/O)\s*\|\s*(\d+)\s*\|\s*\d+\s*\|\s*(\d+)\s*\|\s*([\d.]+)" + } + + for resource, pattern in patterns.items(): + match = re.search(pattern, output, re.IGNORECASE) + if match: + result[resource]["used"] = float(match.group(1)) + result[resource]["available"] = float(match.group(2)) + result[resource]["percent"] = float(match.group(3)) + + return result + + +def parse_messages(output: str) -> dict: + """Parse Vivado messages into categorized lists.""" + result = { + "errors": [], + "critical_warnings": [], + "warnings": [], + "info": [], + "raw": output + } + + for line in output.split("\n"): + line = line.strip() + if re.match(r"ERROR:", line): + result["errors"].append(line) + elif re.match(r"CRITICAL WARNING:", line): + result["critical_warnings"].append(line) + elif re.match(r"WARNING:", line): + result["warnings"].append(line) + elif re.match(r"INFO:", line): + result["info"].append(line) + + return result + + +# ============================================================================ +# TOOLS +# ============================================================================ + +@server.list_tools() +async def list_tools() -> list[Tool]: + """List all available Vivado tools.""" + return [ + # Session management + Tool( + name="start_session", + description="Start a persistent Vivado TCL session. Must be called before other commands.", + inputSchema={ + "type": "object", + "properties": { + "vivado_path": { + "type": "string", + "description": "Path to Vivado executable (default: 'vivado' from PATH)" + } + }, + "required": [] + } + ), + Tool( + name="stop_session", + description="Stop the Vivado TCL session and free resources", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + Tool( + name="session_status", + description="Get status and statistics of the current Vivado session", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + + # Project management + Tool( + name="open_project", + description="Open a Vivado project (.xpr file)", + inputSchema={ + "type": "object", + "properties": { + "project_path": { + "type": "string", + "description": "Path to .xpr project file" + } + }, + "required": ["project_path"] + } + ), + Tool( + name="close_project", + description="Close the current project", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + Tool( + name="get_project_info", + description="Get information about the currently open project", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + + # Design flow + Tool( + name="run_synthesis", + description="Run synthesis on the current project", + inputSchema={ + "type": "object", + "properties": { + "jobs": { + "type": "integer", + "description": "Number of parallel jobs (default: 4)" + } + }, + "required": [] + } + ), + Tool( + name="run_implementation", + description="Run implementation (place and route) on the current project", + inputSchema={ + "type": "object", + "properties": { + "jobs": { + "type": "integer", + "description": "Number of parallel jobs (default: 4)" + } + }, + "required": [] + } + ), + Tool( + name="generate_bitstream", + description="Generate bitstream for the implemented design", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + + # Reports and analysis + Tool( + name="get_timing_summary", + description="Get timing summary (WNS, TNS, WHS, THS) - returns structured data", + inputSchema={ + "type": "object", + "properties": { + "report_type": { + "type": "string", + "description": "Type: 'summary' (default), 'setup', 'hold', or 'all'" + }, + "detail_level": { + "type": "string", + "enum": ["summary", "standard", "full"], + "description": "Detail level: 'summary' (parsed metrics only), 'standard' (default), 'full' (include raw report)" + } + }, + "required": [] + } + ), + Tool( + name="get_timing_paths", + description="Get detailed timing paths for failing or critical paths", + inputSchema={ + "type": "object", + "properties": { + "num_paths": { + "type": "integer", + "description": "Number of paths to report (default: 10)" + }, + "slack_threshold": { + "type": "number", + "description": "Only show paths with slack less than this (default: 0 for failing paths)" + }, + "path_type": { + "type": "string", + "description": "Type: 'setup' (default) or 'hold'" + }, + "from_pin": { + "type": "string", + "description": "Filter paths starting from this pin/cell pattern (Vivado -from option)" + }, + "to_pin": { + "type": "string", + "description": "Filter paths ending at this pin/cell pattern (Vivado -to option)" + }, + "through": { + "type": "string", + "description": "Filter paths going through this pin/cell pattern (Vivado -through option)" + }, + "clock": { + "type": "string", + "description": "Filter paths by clock domain name" + } + }, + "required": [] + } + ), + Tool( + name="get_utilization", + description="Get resource utilization report - returns structured data", + inputSchema={ + "type": "object", + "properties": { + "hierarchical": { + "type": "boolean", + "description": "Include hierarchical breakdown (default: false)" + }, + "detail_level": { + "type": "string", + "enum": ["summary", "standard", "full"], + "description": "Detail level: 'summary' (parsed only), 'standard' (default, + top consumers), 'full' (+ raw report)" + }, + "module_filter": { + "type": "string", + "description": "Wildcard pattern to filter modules in hierarchical report" + }, + "threshold_percent": { + "type": "number", + "description": "Only show resources above this utilization percentage (0-100)" + } + }, + "required": [] + } + ), + Tool( + name="get_clocks", + description="Get clock information and constraints", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + Tool( + name="get_messages", + description="Get synthesis/implementation messages (errors, warnings)", + inputSchema={ + "type": "object", + "properties": { + "severity": { + "type": "string", + "description": "Filter by severity: 'all' (default), 'error', 'critical', 'warning'" + } + }, + "required": [] + } + ), + + # Design queries + Tool( + name="get_design_hierarchy", + description="Get the design hierarchy (modules and instances)", + inputSchema={ + "type": "object", + "properties": { + "max_depth": { + "type": "integer", + "description": "Maximum hierarchy depth to return (default: 3)" + }, + "instance_pattern": { + "type": "string", + "description": "Wildcard pattern to filter instances (e.g., '*cpu*', 'core/alu/*')" + } + }, + "required": [] + } + ), + Tool( + name="get_ports", + description="Get top-level ports of the design", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + Tool( + name="get_nets", + description="Search for nets in the design", + inputSchema={ + "type": "object", + "properties": { + "pattern": { + "type": "string", + "description": "Wildcard pattern to match net names (default: '*')" + }, + "limit": { + "type": "integer", + "description": "Maximum number of results (default: 100)" + } + }, + "required": [] + } + ), + Tool( + name="get_cells", + description="Search for cells (instances) in the design", + inputSchema={ + "type": "object", + "properties": { + "pattern": { + "type": "string", + "description": "Wildcard pattern to match cell names (default: '*')" + }, + "limit": { + "type": "integer", + "description": "Maximum number of results (default: 100)" + } + }, + "required": [] + } + ), + + # Raw TCL + Tool( + name="run_tcl", + description="Execute a raw TCL command in Vivado. Use for advanced operations not covered by other tools.", + inputSchema={ + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "TCL command to execute" + } + }, + "required": ["command"] + } + ), + + # Simulation tools + Tool( + name="launch_simulation", + description="Launch behavioral simulation (xsim). Opens the simulator and loads the design.", + inputSchema={ + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["behavioral", "post_synth_func", "post_synth_timing", "post_impl_func", "post_impl_timing"], + "description": "Simulation mode (default: behavioral)" + } + }, + "required": [] + } + ), + Tool( + name="run_simulation", + description="Run the simulation for a specified time", + inputSchema={ + "type": "object", + "properties": { + "time": { + "type": "string", + "description": "Time to run (e.g., '100ns', '1us', '10ms', 'all')" + } + }, + "required": ["time"] + } + ), + Tool( + name="restart_simulation", + description="Restart the simulation from time 0", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + Tool( + name="close_simulation", + description="Close the current simulation", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + Tool( + name="get_simulation_time", + description="Get the current simulation time", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + Tool( + name="get_signal_value", + description="Get the current value of a signal in simulation", + inputSchema={ + "type": "object", + "properties": { + "signal": { + "type": "string", + "description": "Full hierarchical signal path (e.g., '/tb/dut/clk', '/tb/dut/data_out')" + }, + "radix": { + "type": "string", + "enum": ["bin", "hex", "dec", "unsigned", "ascii"], + "description": "Display radix (default: hex)" + } + }, + "required": ["signal"] + } + ), + Tool( + name="get_signal_values", + description="Get current values of multiple signals matching a pattern", + inputSchema={ + "type": "object", + "properties": { + "pattern": { + "type": "string", + "description": "Signal pattern with wildcards (e.g., '/tb/dut/*', '/tb/dut/data*')" + }, + "radix": { + "type": "string", + "enum": ["bin", "hex", "dec", "unsigned", "ascii"], + "description": "Display radix (default: hex)" + } + }, + "required": ["pattern"] + } + ), + Tool( + name="add_signals_to_wave", + description="Add signals to the waveform viewer", + inputSchema={ + "type": "object", + "properties": { + "signals": { + "type": "array", + "items": {"type": "string"}, + "description": "List of signal paths to add (e.g., ['/tb/dut/clk', '/tb/dut/rst'])" + } + }, + "required": ["signals"] + } + ), + Tool( + name="set_simulation_top", + description="Set the top module for simulation", + inputSchema={ + "type": "object", + "properties": { + "top_module": { + "type": "string", + "description": "Name of the testbench module" + }, + "fileset": { + "type": "string", + "description": "Simulation fileset (default: sim_1)" + } + }, + "required": ["top_module"] + } + ), + Tool( + name="get_simulation_objects", + description="List simulation objects (signals, variables) in a scope", + inputSchema={ + "type": "object", + "properties": { + "scope": { + "type": "string", + "description": "Hierarchical scope (e.g., '/tb', '/tb/dut'). Default is root." + }, + "filter": { + "type": "string", + "enum": ["all", "signals", "ports", "internal"], + "description": "Filter by object type (default: all)" + } + }, + "required": [] + } + ), + Tool( + name="get_scopes", + description="List available scopes (hierarchy) in the simulation", + inputSchema={ + "type": "object", + "properties": { + "parent": { + "type": "string", + "description": "Parent scope to list children of (default: root)" + } + }, + "required": [] + } + ), + Tool( + name="step_simulation", + description="Step the simulation by a delta cycle or time step", + inputSchema={ + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Number of steps (default: 1)" + } + }, + "required": [] + } + ), + Tool( + name="add_breakpoint", + description="Add a simulation breakpoint on a signal condition", + inputSchema={ + "type": "object", + "properties": { + "signal": { + "type": "string", + "description": "Signal to monitor" + }, + "condition": { + "type": "string", + "enum": ["posedge", "negedge", "change"], + "description": "Trigger condition (default: change)" + } + }, + "required": ["signal"] + } + ), + Tool( + name="remove_breakpoints", + description="Remove all breakpoints", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + Tool( + name="get_simulation_messages", + description="Get simulation log messages (errors, warnings, info)", + inputSchema={ + "type": "object", + "properties": { + "severity": { + "type": "string", + "enum": ["all", "error", "warning", "info"], + "description": "Filter by severity (default: all)" + } + }, + "required": [] + } + ), + + # Feature requests + Tool( + name="request_feature", + description="Request a new feature or capability for the Vivado MCP server. Use this when you encounter a limitation or wish you had a tool that doesn't exist.", + inputSchema={ + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Short title for the feature request" + }, + "description": { + "type": "string", + "description": "Detailed description of what you need and why" + }, + "use_case": { + "type": "string", + "description": "The specific use case or task you were trying to accomplish" + }, + "priority": { + "type": "string", + "enum": ["low", "medium", "high"], + "description": "How important is this feature? (default: medium)" + } + }, + "required": ["title", "description"] + } + ), + Tool( + name="list_feature_requests", + description="List all feature requests that have been submitted", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + } + ), + + # Report file management + Tool( + name="generate_full_report", + description="Generate a full Vivado report to a file. Use when inline reports are truncated or you need the complete output.", + inputSchema={ + "type": "object", + "properties": { + "report_type": { + "type": "string", + "enum": ["timing", "timing_summary", "utilization", "hierarchy", "clocks", "power", "drc"], + "description": "Type of report to generate" + }, + "options": { + "type": "object", + "description": "Report-specific options (e.g., {'hierarchical': true} for utilization)" + }, + "output_file": { + "type": "string", + "description": "Optional custom output path. Default: /tmp/vivado_mcp/_.txt" + } + }, + "required": ["report_type"] + } + ), + Tool( + name="read_report_section", + description="Read a section of a previously generated report file", + inputSchema={ + "type": "object", + "properties": { + "report_id": { + "type": "string", + "description": "Report ID returned by generate_full_report" + }, + "file_path": { + "type": "string", + "description": "Alternative: direct file path to read" + }, + "start_line": { + "type": "integer", + "description": "Line number to start reading from (1-indexed, default: 1)" + }, + "num_lines": { + "type": "integer", + "description": "Number of lines to read (default: 100)" + }, + "search_pattern": { + "type": "string", + "description": "Regex pattern to find a section (returns lines around first match)" + } + }, + "required": [] + } + ) + ] + + +@server.call_tool() +async def call_tool(name: str, arguments: dict) -> list[TextContent]: + """Handle tool calls.""" + session = get_session() + + # Session management + if name == "start_session": + vivado_path = arguments.get("vivado_path", "vivado") + session.vivado_path = vivado_path + result = session.start() + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "message": result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "stop_session": + result = session.stop() + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "message": result.output + }, indent=2))] + + elif name == "session_status": + stats = session.get_stats() + return [TextContent(type="text", text=json.dumps(stats, indent=2))] + + # Check session is running for remaining commands + if not session.is_running: + return [TextContent(type="text", text=json.dumps({ + "error": "Vivado session not running. Call start_session first.", + "success": False + }, indent=2))] + + # Project management + if name == "open_project": + project_path = arguments.get("project_path", "") + result = session.run_tcl(f"open_project {{{project_path}}}") + if result.success: + session.current_project = project_path + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "output": result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "close_project": + result = session.run_tcl("close_project") + session.current_project = None + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "output": result.output + }, indent=2))] + + elif name == "get_project_info": + commands = [ + "current_project", + "get_property PART [current_project]", + "get_property TARGET_LANGUAGE [current_project]", + "get_property DIRECTORY [current_project]" + ] + results = {} + for cmd in commands: + r = session.run_tcl(cmd) + results[cmd] = r.output + return [TextContent(type="text", text=json.dumps(results, indent=2))] + + # Design flow + elif name == "run_synthesis": + jobs = arguments.get("jobs", 4) + result = session.run_tcl(f"reset_run synth_1; launch_runs synth_1 -jobs {jobs}; wait_on_run synth_1") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "output": result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "run_implementation": + jobs = arguments.get("jobs", 4) + result = session.run_tcl(f"launch_runs impl_1 -jobs {jobs}; wait_on_run impl_1") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "output": result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "generate_bitstream": + result = session.run_tcl("launch_runs impl_1 -to_step write_bitstream; wait_on_run impl_1") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "output": result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + # Reports and analysis + elif name == "get_timing_summary": + report_type = arguments.get("report_type", "summary") + detail_level = arguments.get("detail_level", "standard") + + result = session.run_tcl("report_timing_summary -no_header -return_string") + parsed = parse_timing_summary(result.output) + parsed["success"] = result.success + parsed["elapsed_ms"] = result.elapsed_ms + + # Control output based on detail_level + if detail_level == "summary": + # Remove raw output, keep only parsed metrics + parsed.pop("raw", None) + elif detail_level == "standard": + # Truncate raw output if too large + if "raw" in parsed and len(parsed["raw"]) > MAX_RESPONSE_CHARS // 2: + truncated = truncate_response(parsed["raw"], MAX_RESPONSE_CHARS // 2) + parsed["raw"] = truncated["content"] + if truncated["truncated"]: + parsed["raw_truncated"] = True + parsed["raw_total_chars"] = truncated["total_chars"] + # detail_level == "full": keep complete raw output (but still apply safety truncation) + elif detail_level == "full": + if "raw" in parsed: + truncated = truncate_response(parsed["raw"], MAX_RESPONSE_CHARS) + parsed["raw"] = truncated["content"] + if truncated["truncated"]: + parsed["raw_truncated"] = True + parsed["raw_total_chars"] = truncated["total_chars"] + parsed["truncation_message"] = truncated["truncation_message"] + + return [TextContent(type="text", text=json.dumps(parsed, indent=2))] + + elif name == "get_timing_paths": + num_paths = arguments.get("num_paths", 10) + slack_threshold = arguments.get("slack_threshold", 0) + path_type = arguments.get("path_type", "setup") + from_pin = arguments.get("from_pin") + to_pin = arguments.get("to_pin") + through = arguments.get("through") + clock = arguments.get("clock") + + delay_type = "max" if path_type == "setup" else "min" + cmd = f"report_timing -delay_type {delay_type} -max_paths {num_paths} -slack_lesser_than {slack_threshold}" + + # Add optional filters + if from_pin: + cmd += f" -from {{{from_pin}}}" + if to_pin: + cmd += f" -to {{{to_pin}}}" + if through: + cmd += f" -through {{{through}}}" + if clock: + cmd += f" -filter {{CLOCK == {clock}}}" + + cmd += " -return_string" + result = session.run_tcl(cmd) + + # Apply truncation for large outputs + response = { + "success": result.success, + "elapsed_ms": result.elapsed_ms, + "filters_applied": { + "path_type": path_type, + "num_paths": num_paths, + "slack_threshold": slack_threshold + } + } + + if from_pin: + response["filters_applied"]["from_pin"] = from_pin + if to_pin: + response["filters_applied"]["to_pin"] = to_pin + if through: + response["filters_applied"]["through"] = through + if clock: + response["filters_applied"]["clock"] = clock + + if result.success: + truncated = truncate_response(result.output, MAX_RESPONSE_CHARS) + response["paths"] = truncated["content"] + if truncated["truncated"]: + response["truncated"] = True + response["total_chars"] = truncated["total_chars"] + response["truncation_message"] = truncated["truncation_message"] + else: + response["paths"] = result.output + + return [TextContent(type="text", text=json.dumps(response, indent=2))] + + elif name == "get_utilization": + hierarchical = arguments.get("hierarchical", False) + detail_level = arguments.get("detail_level", "standard") + module_filter = arguments.get("module_filter") + threshold_percent = arguments.get("threshold_percent") + + cmd = "report_utilization -return_string" + if hierarchical: + cmd += " -hierarchical" + if module_filter: + cmd += f" -hierarchical_pattern {{{module_filter}}}" + + result = session.run_tcl(cmd) + parsed = parse_utilization(result.output) + parsed["success"] = result.success + parsed["elapsed_ms"] = result.elapsed_ms + + # Apply threshold filter if specified + if threshold_percent is not None: + for resource in ["lut", "ff", "bram", "dsp", "io"]: + if resource in parsed and parsed[resource]["percent"] < threshold_percent: + parsed[resource]["below_threshold"] = True + + # Control output based on detail_level + if detail_level == "summary": + # Remove raw output, keep only parsed metrics + parsed.pop("raw", None) + elif detail_level == "standard": + # Truncate raw output if too large + if "raw" in parsed and len(parsed["raw"]) > MAX_RESPONSE_CHARS // 2: + truncated = truncate_response(parsed["raw"], MAX_RESPONSE_CHARS // 2) + parsed["raw"] = truncated["content"] + if truncated["truncated"]: + parsed["raw_truncated"] = True + parsed["raw_total_chars"] = truncated["total_chars"] + elif detail_level == "full": + # Keep complete raw output but apply safety truncation + if "raw" in parsed: + truncated = truncate_response(parsed["raw"], MAX_RESPONSE_CHARS) + parsed["raw"] = truncated["content"] + if truncated["truncated"]: + parsed["raw_truncated"] = True + parsed["raw_total_chars"] = truncated["total_chars"] + parsed["truncation_message"] = truncated["truncation_message"] + + return [TextContent(type="text", text=json.dumps(parsed, indent=2))] + + elif name == "get_clocks": + result = session.run_tcl("report_clocks -return_string") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "clocks": result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "get_messages": + severity = arguments.get("severity", "all") + # Get messages from Vivado's message log + result = session.run_tcl("get_msg_config -rules") + parsed = parse_messages(result.output) + if severity != "all": + filtered = { + "error": parsed["errors"], + "critical": parsed["critical_warnings"], + "warning": parsed["warnings"] + }.get(severity, []) + parsed = {severity: filtered, "raw": parsed["raw"]} + parsed["success"] = result.success + return [TextContent(type="text", text=json.dumps(parsed, indent=2))] + + # Design queries + elif name == "get_design_hierarchy": + max_depth = arguments.get("max_depth", 3) + instance_pattern = arguments.get("instance_pattern", "*") + + # Get hierarchical cells with pattern filter + cmd = f"get_cells -hierarchical {{{instance_pattern}}}" + result = session.run_tcl(cmd) + + if result.success and result.output.strip(): + cells = result.output.strip().split() + + # Filter by depth: count '/' separators + filtered_cells = [] + for cell in cells: + depth = get_hierarchy_depth(cell) + if depth <= max_depth: + filtered_cells.append(cell) + + # Build hierarchical structure + hierarchy = {} + for cell in sorted(filtered_cells): + parts = cell.split('/') + current = hierarchy + for i, part in enumerate(parts): + if part not in current: + current[part] = {"_children": {}} + current = current[part]["_children"] + + # Also get module reference for each cell (limited to avoid large output) + cell_refs = {} + sample_cells = filtered_cells[:100] # Limit for performance + for cell in sample_cells: + ref_result = session.run_tcl(f"get_property REF_NAME [get_cells {{{cell}}}]") + if ref_result.success and ref_result.output.strip(): + cell_refs[cell] = ref_result.output.strip() + + response = { + "success": True, + "cells": filtered_cells[:500], # Limit response size + "cell_count": len(filtered_cells), + "cell_modules": cell_refs, + "max_depth": max_depth, + "elapsed_ms": result.elapsed_ms + } + + if len(filtered_cells) > 500: + response["truncated"] = True + response["total_cells"] = len(filtered_cells) + response["message"] = "Cell list truncated. Use instance_pattern to filter or generate_full_report for complete hierarchy." + else: + response = { + "success": result.success, + "cells": [], + "cell_count": 0, + "error": result.output if not result.success else "No cells found", + "elapsed_ms": result.elapsed_ms + } + + return [TextContent(type="text", text=json.dumps(response, indent=2))] + + elif name == "get_ports": + result = session.run_tcl("get_ports *") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "ports": result.output.split() if result.success else [], + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "get_nets": + pattern = arguments.get("pattern", "*") + limit = arguments.get("limit", 100) + result = session.run_tcl(f"lrange [get_nets {{{pattern}}}] 0 {limit-1}") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "nets": result.output.split() if result.success else [], + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "get_cells": + pattern = arguments.get("pattern", "*") + limit = arguments.get("limit", 100) + result = session.run_tcl(f"lrange [get_cells {{{pattern}}}] 0 {limit-1}") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "cells": result.output.split() if result.success else [], + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + # Raw TCL + elif name == "run_tcl": + command = arguments.get("command", "") + result = session.run_tcl(command) + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "output": result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + # Simulation tools + elif name == "launch_simulation": + mode = arguments.get("mode", "behavioral") + mode_map = { + "behavioral": "behav", + "post_synth_func": "synth -type func", + "post_synth_timing": "synth -type timing", + "post_impl_func": "impl -type func", + "post_impl_timing": "impl -type timing" + } + sim_mode = mode_map.get(mode, "behav") + result = session.run_tcl(f"launch_simulation -mode {sim_mode}") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "message": result.output if result.output else f"Simulation launched in {mode} mode", + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "run_simulation": + time_val = arguments.get("time", "100ns") + if time_val.lower() == "all": + result = session.run_tcl("run -all") + else: + result = session.run_tcl(f"run {time_val}") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "output": result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "restart_simulation": + result = session.run_tcl("restart") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "message": "Simulation restarted" if result.success else result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "close_simulation": + result = session.run_tcl("close_sim") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "message": "Simulation closed" if result.success else result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "get_simulation_time": + result = session.run_tcl("current_time") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "time": result.output.strip() if result.success else None, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "get_signal_value": + signal = arguments.get("signal", "") + radix = arguments.get("radix", "hex") + result = session.run_tcl(f"get_value -radix {radix} {{{signal}}}") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "signal": signal, + "value": result.output.strip() if result.success else None, + "radix": radix, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "get_signal_values": + pattern = arguments.get("pattern", "/*") + radix = arguments.get("radix", "hex") + # Get list of signals matching pattern + signals_result = session.run_tcl(f"get_objects -filter {{TYPE == signal || TYPE == port}} {{{pattern}}}") + if signals_result.success and signals_result.output.strip(): + signals = signals_result.output.strip().split() + values = {} + for sig in signals[:50]: # Limit to 50 signals + val_result = session.run_tcl(f"get_value -radix {radix} {{{sig}}}") + if val_result.success: + values[sig] = val_result.output.strip() + return [TextContent(type="text", text=json.dumps({ + "success": True, + "values": values, + "radix": radix, + "elapsed_ms": signals_result.elapsed_ms + }, indent=2))] + return [TextContent(type="text", text=json.dumps({ + "success": False, + "error": "No signals found matching pattern", + "elapsed_ms": signals_result.elapsed_ms + }, indent=2))] + + elif name == "add_signals_to_wave": + signals = arguments.get("signals", []) + if isinstance(signals, str): + signals = [signals] + results = [] + for sig in signals: + result = session.run_tcl(f"add_wave {{{sig}}}") + results.append({"signal": sig, "success": result.success}) + return [TextContent(type="text", text=json.dumps({ + "success": all(r["success"] for r in results), + "results": results + }, indent=2))] + + elif name == "set_simulation_top": + top_module = arguments.get("top_module", "") + fileset = arguments.get("fileset", "sim_1") + result = session.run_tcl(f"set_property top {top_module} [get_filesets {fileset}]") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "message": f"Set simulation top to {top_module}" if result.success else result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "get_simulation_objects": + scope = arguments.get("scope", "/") + obj_filter = arguments.get("filter", "all") + + filter_map = { + "all": "", + "signals": "-filter {TYPE == signal}", + "ports": "-filter {TYPE == port}", + "internal": "-filter {TYPE == signal && IS_PORT == false}" + } + filter_str = filter_map.get(obj_filter, "") + result = session.run_tcl(f"get_objects {filter_str} {{{scope}/*}}") + objects = result.output.strip().split() if result.success and result.output.strip() else [] + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "scope": scope, + "objects": objects, + "count": len(objects), + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "get_scopes": + parent = arguments.get("parent", "/") + result = session.run_tcl(f"get_scopes {{{parent}/*}}") + scopes = result.output.strip().split() if result.success and result.output.strip() else [] + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "parent": parent, + "scopes": scopes, + "count": len(scopes), + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "step_simulation": + count = arguments.get("count", 1) + result = session.run_tcl(f"step {count}") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "output": result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "add_breakpoint": + signal = arguments.get("signal", "") + condition = arguments.get("condition", "change") + cond_map = { + "posedge": "-posedge", + "negedge": "-negedge", + "change": "" + } + cond_str = cond_map.get(condition, "") + result = session.run_tcl(f"add_bp {cond_str} {{{signal}}}") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "signal": signal, + "condition": condition, + "message": result.output if result.output else f"Breakpoint added on {signal}", + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "remove_breakpoints": + result = session.run_tcl("remove_bps -all") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "message": "All breakpoints removed" if result.success else result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "get_simulation_messages": + severity = arguments.get("severity", "all") + if severity == "all": + result = session.run_tcl("get_msg_config -count") + else: + result = session.run_tcl(f"get_msg_config -count -severity {{{severity}}}") + return [TextContent(type="text", text=json.dumps({ + "success": result.success, + "messages": result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + # Feature requests + elif name == "request_feature": + title = arguments.get("title", "") + description = arguments.get("description", "") + use_case = arguments.get("use_case", "") + priority = arguments.get("priority", "medium") + + request = { + "id": len(load_feature_requests()) + 1, + "title": title, + "description": description, + "use_case": use_case, + "priority": priority, + "timestamp": datetime.now().isoformat(), + "status": "pending" + } + save_feature_request(request) + + return [TextContent(type="text", text=json.dumps({ + "success": True, + "message": f"Feature request #{request['id']} submitted: {title}", + "request": request + }, indent=2))] + + elif name == "list_feature_requests": + requests = load_feature_requests() + return [TextContent(type="text", text=json.dumps({ + "success": True, + "total": len(requests), + "requests": requests + }, indent=2))] + + # Report file management tools + elif name == "generate_full_report": + report_type = arguments.get("report_type", "timing") + options = arguments.get("options", {}) + output_file = arguments.get("output_file") + + # Ensure reports directory exists and clean up old files + ensure_reports_dir() + + # Generate report ID and file path + report_id = generate_report_id() + if output_file: + file_path = Path(output_file) + else: + file_path = REPORTS_DIR / f"{report_type}_{report_id}.txt" + + # Build the report command based on type + report_commands = { + "timing": "report_timing -max_paths 100", + "timing_summary": "report_timing_summary", + "utilization": "report_utilization", + "hierarchy": "report_hierarchy", + "clocks": "report_clocks", + "power": "report_power", + "drc": "report_drc" + } + + base_cmd = report_commands.get(report_type, f"report_{report_type}") + + # Add options for specific report types + if report_type == "utilization" and options.get("hierarchical"): + base_cmd += " -hierarchical" + if report_type == "timing" and options.get("num_paths"): + base_cmd = base_cmd.replace("-max_paths 100", f"-max_paths {options['num_paths']}") + + # Use -file option to write directly to file + cmd = f"{base_cmd} -file {{{file_path}}}" + result = session.run_tcl(cmd) + + if result.success: + # Get file info + try: + file_stat = file_path.stat() + line_count = sum(1 for _ in open(file_path)) + + # Store in cache + _report_cache[report_id] = { + "file_path": str(file_path), + "report_type": report_type, + "created": datetime.now().isoformat(), + "size_bytes": file_stat.st_size, + "line_count": line_count + } + + return [TextContent(type="text", text=json.dumps({ + "success": True, + "report_id": report_id, + "file_path": str(file_path), + "report_type": report_type, + "size_bytes": file_stat.st_size, + "line_count": line_count, + "message": f"Report written to {file_path}. Use read_report_section to read portions.", + "elapsed_ms": result.elapsed_ms + }, indent=2))] + except (OSError, IOError) as e: + return [TextContent(type="text", text=json.dumps({ + "success": False, + "error": f"Report generated but could not read file info: {e}", + "file_path": str(file_path), + "elapsed_ms": result.elapsed_ms + }, indent=2))] + else: + return [TextContent(type="text", text=json.dumps({ + "success": False, + "error": result.output, + "elapsed_ms": result.elapsed_ms + }, indent=2))] + + elif name == "read_report_section": + report_id = arguments.get("report_id") + file_path = arguments.get("file_path") + start_line = arguments.get("start_line", 1) + num_lines = arguments.get("num_lines", 100) + search_pattern = arguments.get("search_pattern") + + # Resolve file path + if report_id: + if report_id in _report_cache: + file_path = _report_cache[report_id]["file_path"] + else: + # Try to find file in reports directory + possible_files = list(REPORTS_DIR.glob(f"*_{report_id}.txt")) + if possible_files: + file_path = str(possible_files[0]) + else: + return [TextContent(type="text", text=json.dumps({ + "success": False, + "error": f"Report ID '{report_id}' not found in cache or reports directory" + }, indent=2))] + + if not file_path: + return [TextContent(type="text", text=json.dumps({ + "success": False, + "error": "Either report_id or file_path must be provided" + }, indent=2))] + + try: + file_path = Path(file_path) + if not file_path.exists(): + return [TextContent(type="text", text=json.dumps({ + "success": False, + "error": f"File not found: {file_path}" + }, indent=2))] + + with open(file_path, 'r') as f: + all_lines = f.readlines() + + total_lines = len(all_lines) + + # Handle search pattern + if search_pattern: + pattern = re.compile(search_pattern, re.IGNORECASE) + for i, line in enumerate(all_lines): + if pattern.search(line): + # Found match, return context around it + context_before = num_lines // 4 + context_after = num_lines - context_before + start_line = max(1, i + 1 - context_before) + break + else: + return [TextContent(type="text", text=json.dumps({ + "success": True, + "warning": f"Pattern '{search_pattern}' not found in file", + "total_lines": total_lines, + "file_path": str(file_path) + }, indent=2))] + + # Extract requested lines (1-indexed) + start_idx = max(0, start_line - 1) + end_idx = min(total_lines, start_idx + num_lines) + selected_lines = all_lines[start_idx:end_idx] + + content = ''.join(selected_lines) + + return [TextContent(type="text", text=json.dumps({ + "success": True, + "file_path": str(file_path), + "start_line": start_idx + 1, + "end_line": end_idx, + "total_lines": total_lines, + "returned_lines": len(selected_lines), + "content": content + }, indent=2))] + + except (OSError, IOError) as e: + return [TextContent(type="text", text=json.dumps({ + "success": False, + "error": f"Error reading file: {e}" + }, indent=2))] + + return [TextContent(type="text", text=json.dumps({"error": f"Unknown tool: {name}"}, indent=2))] + + +async def main(): + """Run the MCP server.""" + async with stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options() + ) + + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) diff --git a/vivado_session.py b/vivado_session.py new file mode 100644 index 0000000..93823e1 --- /dev/null +++ b/vivado_session.py @@ -0,0 +1,312 @@ +"""Vivado TCL session manager - maintains persistent Vivado process using pexpect.""" + +import pexpect +import time +import re +from typing import Optional +from dataclasses import dataclass, field +from datetime import datetime +import threading + + +@dataclass +class CommandResult: + """Result from a Vivado TCL command.""" + command: str + output: str + return_value: str + success: bool + elapsed_ms: float + timestamp: str = field(default_factory=lambda: datetime.now().isoformat()) + + +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. + """ + + # Unique marker to detect end of command output (use something that won't appear in normal output) + SENTINEL = "XYZZY_MCP_9f8e7d6c_DONE" + + def __init__(self, vivado_path: str = "vivado", timeout: float = 300.0): + self.vivado_path = vivado_path + self.timeout = timeout + self.child: Optional[pexpect.spawn] = None + self.is_running = False + self.current_project: Optional[str] = None + self._lock = threading.Lock() + + # Statistics + self.stats = { + "session_start": None, + "commands_run": 0, + "total_command_time_ms": 0, + "errors": 0, + "command_history": [] + } + + def start(self) -> CommandResult: + """Start the Vivado TCL session.""" + if self.is_running: + return CommandResult( + command="start", + output="Session already running", + return_value="0", + success=True, + elapsed_ms=0 + ) + + start_time = time.time() + + try: + # Start Vivado with pexpect + 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 + ) + + # Wait for Vivado to start (look for startup banner) + self.child.expect('Start of session', timeout=120) + + # Give it a moment to fully initialize + time.sleep(1) + + # Drain any remaining startup output + try: + self.child.read_nonblocking(size=100000, timeout=1) + except (pexpect.TIMEOUT, pexpect.EOF): + pass + + # Wait for prompt to confirm ready + self.child.sendline("") # Empty command to get prompt + self.child.expect('Vivado%', timeout=10) + + self.is_running = True + self.stats["session_start"] = datetime.now().isoformat() + + elapsed = (time.time() - start_time) * 1000 + + return CommandResult( + command="start", + output="Vivado session started successfully", + return_value="0", + success=True, + elapsed_ms=elapsed + ) + + except pexpect.TIMEOUT: + self.is_running = False + elapsed = (time.time() - start_time) * 1000 + return CommandResult( + command="start", + output="Failed to start Vivado: Timeout waiting for startup", + return_value="1", + success=False, + elapsed_ms=elapsed + ) + except Exception as e: + self.is_running = False + elapsed = (time.time() - start_time) * 1000 + return CommandResult( + command="start", + output=f"Failed to start Vivado: {str(e)}", + return_value="1", + success=False, + elapsed_ms=elapsed + ) + + def run_tcl(self, command: str) -> CommandResult: + """ + Execute a TCL command and return the result. + + Args: + command: TCL command to execute + + Returns: + CommandResult with output and status + """ + if not self.is_running: + return CommandResult( + command=command, + output="Vivado session not running. Call start() first.", + return_value="1", + success=False, + elapsed_ms=0 + ) + + with self._lock: + start_time = time.time() + + try: + # Clear any pending output first + try: + self.child.read_nonblocking(size=100000, timeout=0.1) + except (pexpect.TIMEOUT, pexpect.EOF): + pass + + # Send the command + self.child.sendline(command) + + # Wait for Vivado prompt (indicates command completed) + self.child.expect('Vivado%', timeout=self.timeout) + + # Get the output (everything before the prompt) + raw_output = self.child.before + + # Parse output: extract content after command echo + lines = raw_output.replace('\r', '').split('\n') + clean_lines = [] + found_command = False + + # Normalize command for matching + cmd_normalized = command.strip() + + for line in lines: + stripped = line.strip() + + # Look for the command echo + if not found_command: + if cmd_normalized in stripped: + found_command = True + continue + + # Skip Vivado prompts + if stripped == 'Vivado%' or stripped.startswith('Vivado%'): + continue + + # Skip empty lines + if not stripped: + continue + + clean_lines.append(stripped) + + output = '\n'.join(clean_lines).strip() + + elapsed = (time.time() - start_time) * 1000 + + # Check for errors in output + success = not any(err in output.lower() for err in + ["error:", "invalid command", "can't read", "wrong # args"]) + + # Update stats + self.stats["commands_run"] += 1 + self.stats["total_command_time_ms"] += elapsed + if not success: + self.stats["errors"] += 1 + + result = CommandResult( + command=command, + output=output, + return_value="0" if success else "1", + success=success, + elapsed_ms=elapsed + ) + + # Keep last 100 commands in history + self.stats["command_history"].append({ + "command": command, + "success": success, + "elapsed_ms": elapsed, + "timestamp": result.timestamp + }) + if len(self.stats["command_history"]) > 100: + self.stats["command_history"] = self.stats["command_history"][-100:] + + return result + + except pexpect.TIMEOUT: + elapsed = (time.time() - start_time) * 1000 + self.stats["errors"] += 1 + return CommandResult( + command=command, + output=f"Command timed out after {self.timeout}s", + return_value="1", + success=False, + elapsed_ms=elapsed + ) + except Exception as e: + elapsed = (time.time() - start_time) * 1000 + self.stats["errors"] += 1 + return CommandResult( + command=command, + output=f"Error executing command: {str(e)}", + return_value="1", + success=False, + elapsed_ms=elapsed + ) + + def stop(self) -> CommandResult: + """Stop the Vivado session.""" + if not self.is_running: + return CommandResult( + command="stop", + output="Session not running", + return_value="0", + success=True, + elapsed_ms=0 + ) + + start_time = time.time() + + try: + self.child.sendline('exit') + self.child.expect(pexpect.EOF, timeout=30) + except Exception: + # Force close if graceful exit fails + try: + self.child.close(force=True) + except: + pass + + self.is_running = False + self.current_project = None + elapsed = (time.time() - start_time) * 1000 + + return CommandResult( + command="stop", + output="Vivado session stopped", + return_value="0", + success=True, + elapsed_ms=elapsed + ) + + def get_stats(self) -> dict: + """Get session statistics.""" + stats = self.stats.copy() + stats["is_running"] = self.is_running + stats["current_project"] = self.current_project + if self.stats["commands_run"] > 0: + stats["avg_command_time_ms"] = self.stats["total_command_time_ms"] / self.stats["commands_run"] + return stats + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + + +# Singleton session instance +_session: Optional[VivadoSession] = None + + +def get_session() -> VivadoSession: + """Get or create the global Vivado session.""" + global _session + if _session is None: + _session = VivadoSession() + return _session + + +def reset_session(): + """Reset the global session (stop if running).""" + global _session + if _session is not None and _session.is_running: + _session.stop() + _session = None