"""In-process driver for the zynq-imx6-flow LangGraph workflow. Builds the compiled graph from the project's ``app`` package and drives it with a persistent SqliteSaver — no ``langgraph dev`` server. Mirrors the sibling fpga-imx6-flow driver: exit code 0 = done, 1 = error, 2 = interrupt fired. Commands: start --project-dir [--program-mode bitstream|debug_flash] [--ps-mode baremetal|petalinux] [--board-type xmod|zynq_only] [--bit-path ..] [--elf-path ..] [--flash-path ..] [--thread-id ..] [--real] [--max-iterations N] resume --project-dir --resume-json '' [--thread-id ..] [--real] status --project-dir [--thread-id ..] Output: one JSON line on stdout describing status + the pending interrupt(s). Resume payloads: * Single pending interrupt -> pass its payload directly, e.g. '{"meets": true}'. * Multiple pending (the three-way modify fan-out: modify_fpga + modify_imx6 + modify_ps can fire in one superstep) -> pass a map keyed by each interrupt's "action", e.g. '{"modify_fpga": {...}, "modify_imx6": {...}, "modify_ps": {...}}'. You may resume a subset; unresumed interrupts stay pending and round_done (defer=True) still waits for every active branch. Run via the env python directly or ``conda run --no-capture-output`` to avoid the Windows GBK console re-encode crash on the Chinese interrupt payloads. """ from __future__ import annotations import argparse import json import os import re import sys from pathlib import Path try: sys.stdout.reconfigure(encoding="utf-8") # emit UTF-8 even on a GBK console except Exception: pass def _env_lookup(key: str, default: str | None = None) -> str | None: """Read KEY from the OS environment, else from a ``KEY = value`` line in ~/skills_env.md. Machine-local paths are never hardcoded in a skill; see the same helper in fpga_flash/scripts/vivado_env.py.""" value = os.environ.get(key) if value: return value path = os.path.join(os.path.expanduser("~"), "skills_env.md") try: with open(path, encoding="utf-8") as handle: for line in handle: match = re.match(rf"\s*{re.escape(key)}\s*=\s*(.+?)\s*(?:#.*)?$", line) if match: found = match.group(1).strip() if found and not found.startswith("<"): return found except OSError: pass return default #: The zynq-imx6-flow checkout: ZYNQ_IMX6_FLOW_ROOT in ~/skills_env.md. DEFAULT_PROJECT = _env_lookup("ZYNQ_IMX6_FLOW_ROOT") def _emit(obj: dict, code: int) -> None: print(json.dumps(obj, ensure_ascii=False)) sys.exit(code) def _pending(snap): return [intr for task in snap.tasks for intr in task.interrupts] def main() -> None: ap = argparse.ArgumentParser(description="Drive the zynq-imx6-flow graph in-process.") ap.add_argument("command", choices=["start", "resume", "status"]) ap.add_argument("--project-dir", default=DEFAULT_PROJECT) ap.add_argument("--thread-id", default="zynq-skill-1") ap.add_argument("--resume-json", default="") ap.add_argument("--program-mode", default="bitstream", choices=["bitstream", "debug_flash"]) ap.add_argument("--ps-mode", default="baremetal", choices=["baremetal", "petalinux"]) ap.add_argument("--board-type", default="xmod", choices=["xmod", "zynq_only"]) ap.add_argument("--bit-path", default="") ap.add_argument("--elf-path", default="") ap.add_argument("--flash-path", default="") ap.add_argument("--real", action="store_true", help="drive real hardware (tools_* must be wired; default is MOCK dry-run)") ap.add_argument("--max-iterations", type=int, default=8) args = ap.parse_args() if not args.project_dir: _emit({"status": "error", "error": "zynq-imx6-flow 仓库路径未配置:在 ~/skills_env.md 增加 " "`ZYNQ_IMX6_FLOW_ROOT = <检出目录>`,或用 --project-dir 指定"}, 1) project = Path(args.project_dir) if not (project / "app" / "main_graph.py").exists(): _emit({"status": "error", "error": f"app/main_graph.py not found under {project}; check --project-dir"}, 1) # MOCK by default so the design loop can be dry-run with no hardware attached. os.environ["ZYNQ_IMX6_MOCK"] = "0" if args.real else "1" sys.path.insert(0, str(project)) try: from langgraph.checkpoint.sqlite import SqliteSaver from langgraph.types import Command from app.config import initial_state from app.main_graph import build_graph except Exception as exc: # noqa: BLE001 _emit({"status": "error", "error": f"import failed: {exc}. Run /langgraph-vivado-install to set up the env."}, 1) runs = project / "runs" runs.mkdir(exist_ok=True) db = str(runs / "skill_checkpoint.sqlite") cfg = {"configurable": {"thread_id": args.thread_id}} with SqliteSaver.from_conn_string(db) as cp: graph = build_graph(checkpointer=cp) try: if args.command == "start": init = initial_state( program_mode=args.program_mode, ps_mode=args.ps_mode, board_type=args.board_type, max_iterations=args.max_iterations, ) if args.bit_path: init["bit_path"] = args.bit_path if args.elf_path: init["elf_path"] = args.elf_path if args.flash_path: init["flash_path"] = args.flash_path graph.invoke(init, cfg) elif args.command == "resume": payload = json.loads(args.resume_json) if args.resume_json else {} pend = _pending(graph.get_state(cfg)) if len(pend) <= 1: graph.invoke(Command(resume=payload), cfg) else: # Multiple interrupts pending (three-way modify fan-out). payload # must be a map keyed by each interrupt's "action" (or its id). resume_map = {} for intr in pend: val = intr.value if isinstance(intr.value, dict) else {} action = val.get("action") if action in payload: resume_map[intr.id] = payload[action] elif intr.id in payload: resume_map[intr.id] = payload[intr.id] if not resume_map: _emit({"status": "error", "error": f"resume-json keys {list(payload)} matched none of the " f"pending actions {[i.value.get('action') for i in pend]}"}, 1) graph.invoke(Command(resume=resume_map), cfg) snap = graph.get_state(cfg) except Exception as exc: # noqa: BLE001 _emit({"status": "error", "error": str(exc)}, 1) pend = _pending(snap) if snap.next: _emit({ "status": "interrupt", "thread_id": args.thread_id, "mock": not args.real, "next": list(snap.next), "iteration": snap.values.get("iteration"), "interrupt": pend[0].value if pend else None, "pending_interrupts": [i.value for i in pend], "pending_count": len(pend), }, 2) else: _emit({ "status": "done", "thread_id": args.thread_id, "iteration": snap.values.get("iteration"), "meets_requirement": snap.values.get("meets_requirement"), "steps": len(snap.values.get("history", [])), }, 0) if __name__ == "__main__": main()