sync: SHANGHAI-U9 2026-07-27
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"""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 <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 <dir> --resume-json '<json>' [--thread-id ..] [--real]
|
||||
status --project-dir <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()
|
||||
Reference in New Issue
Block a user