diff --git a/README.md b/README.md
index 4e22e03..23ef677 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
diff --git a/README_EN.md b/README_EN.md
index b9d9055..5689a90 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -1,5 +1,5 @@
-
+
diff --git a/actions/__init__.py b/actions/__init__.py
deleted file mode 100644
index 2b8d462..0000000
--- a/actions/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# actions 包占位,便于导入
diff --git a/actions/at_command.py b/actions/at_command.py
deleted file mode 100644
index c255131..0000000
--- a/actions/at_command.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Dict
-
-from protocols.registry import ProtocolRegistry
-
-
-def run(task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any:
- channel_name = task.get("channel")
- if not channel_name or channel_name not in channels:
- raise KeyError(f"channel not found: {channel_name}")
-
- cmd = task.get("cmd") or task.get("command")
- if not cmd:
- raise ValueError("at_command.cmd is required")
-
- timeout = float(task.get("timeout", 2.0))
- terminator = task.get("terminator", "\r\n")
- ok = str(task.get("ok", "OK"))
- error = str(task.get("error", "ERROR"))
- echo = bool(task.get("echo", True))
-
- protocol_cls = ProtocolRegistry.get("at")
- protocol = protocol_cls(channels[channel_name], logger)
- return protocol.execute(cmd=str(cmd), timeout=timeout, terminator=terminator, ok=ok, error=error, echo=echo)
diff --git a/actions/builtin_actions.py b/actions/builtin_actions.py
deleted file mode 100644
index f1312e1..0000000
--- a/actions/builtin_actions.py
+++ /dev/null
@@ -1,110 +0,0 @@
-from __future__ import annotations
-
-import time
-from typing import Any, Dict
-
-from actions.registry import ActionRegistry
-from dsl.expression import eval_expr
-
-
-def _eval(ctx, val):
- if isinstance(val, str) and "$" in val:
- return eval_expr(val, ctx.vars_snapshot())
- return val
-
-
-def action_set(ctx, args: Dict[str, Any]):
- for key, val in args.items():
- ctx.set_var(key, _eval(ctx, val))
- return ctx.vars_snapshot()
-
-
-def action_log(ctx, args: Dict[str, Any]):
- msg = args.get("message") or args.get("msg") or ""
- if isinstance(msg, str) and "$" in msg:
- msg = eval_expr(msg, ctx.vars_snapshot())
- ctx.logger.info(str(msg))
-
-
-def action_wait(ctx, args: Dict[str, Any]):
- ms = int(args.get("ms", 0))
- time.sleep(ms / 1000.0)
-
-
-def action_wait_for_event(ctx, args: Dict[str, Any]):
- expected = args.get("event")
- timeout = float(args.get("timeout", 1.0))
- end = time.time() + timeout
- while time.time() < end:
- evt = ctx.next_event(timeout=0.1)
- if evt is None:
- continue
- if expected is None or evt == expected:
- ctx.set_var("event", evt)
- return evt
- return None
-
-
-def register_builtin_actions():
- ActionRegistry.register("set", action_set)
- ActionRegistry.register("log", action_log)
- ActionRegistry.register("send_text", action_send_text)
- ActionRegistry.register("read_line", action_read_line)
- ActionRegistry.register("read_stream", action_read_stream)
- ActionRegistry.register("wait", action_wait)
- ActionRegistry.register("wait_for_event", action_wait_for_event)
-
-
-def action_send_text(ctx, args: Dict[str, Any]):
- text = args.get("text", args.get("data", ""))
- if isinstance(text, (bytes, bytearray)):
- payload = bytes(text)
- else:
- payload = str(text)
- append_cr = bool(args.get("append_cr", False))
- append_lf = bool(args.get("append_lf", False))
- if append_cr:
- payload = payload + (b"\r" if isinstance(payload, (bytes, bytearray)) else "\r")
- if append_lf:
- payload = payload + (b"\n" if isinstance(payload, (bytes, bytearray)) else "\n")
- ctx.channel_write(payload)
- return {"text": text, "append_cr": append_cr, "append_lf": append_lf}
-
-
-def action_read_line(ctx, args: Dict[str, Any]):
- terminator = args.get("terminator", "\n")
- timeout = float(args.get("timeout", 1.0))
- raw = ctx.channel.read_until(
- terminator.encode() if isinstance(terminator, str) else bytes(terminator),
- timeout=timeout,
- )
- text = raw.decode(errors="ignore").strip()
- ctx.set_var("last_line_rx", text)
- ctx.set_var("last_line_rx_raw", raw.hex().upper())
- return {"text": text, "hex": raw.hex().upper()}
-
-
-def action_read_stream(ctx, args: Dict[str, Any]):
- duration_ms = int(args.get("duration_ms", args.get("duration", 1000)))
- chunk_size = int(args.get("chunk_size", 256))
- timeout = float(args.get("timeout", 0.2))
- log_hex = bool(args.get("log_hex", True))
- end = time.time() + max(0.0, duration_ms / 1000.0)
- last_text = ""
- last_hex = ""
- while time.time() < end:
- chunk = ctx.channel.read(chunk_size, timeout=timeout)
- if not chunk:
- continue
- last_hex = chunk.hex().upper()
- try:
- last_text = chunk.decode(errors="ignore")
- except Exception:
- last_text = ""
- if log_hex:
- ctx.logger.info(f"RX(hex): {last_hex}")
- if last_text.strip():
- ctx.logger.info(f"RX(text): {last_text.strip()}")
- ctx.set_var("last_stream_rx", last_text.strip())
- ctx.set_var("last_stream_rx_raw", last_hex)
- return {"text": last_text.strip(), "hex": last_hex}
diff --git a/actions/chart_actions.py b/actions/chart_actions.py
deleted file mode 100644
index 693c289..0000000
--- a/actions/chart_actions.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from __future__ import annotations
-
-import time
-from typing import Any, Dict
-
-from actions.chart_bridge import chart_bridge
-from actions.registry import ActionRegistry
-
-
-def _eval(ctx, val: Any) -> Any:
- if hasattr(ctx, "eval_value"):
- return ctx.eval_value(val)
- return val
-
-
-def action_chart_add(ctx, args: Dict[str, Any]):
- """Push a data point to chart system. Args: bind (str), value (num or expr), ts (seconds, optional)."""
- bind = args.get("bind")
- if not bind:
- raise ValueError("chart_add requires bind")
- raw_val = args.get("value", args.get("val"))
- if raw_val is None:
- raise ValueError("chart_add requires value")
- ts_arg = args.get("ts") or args.get("timestamp")
- ts = float(_eval(ctx, ts_arg)) if ts_arg is not None else time.time()
- try:
- val = float(_eval(ctx, raw_val))
- except Exception as exc:
- raise ValueError(f"chart_add value invalid: {exc}") from exc
- payload = {"ts": ts, str(bind): val}
- if hasattr(ctx, "record_chart"):
- try:
- ctx.record_chart(payload)
- except Exception:
- pass
- if chart_bridge is None:
- ctx.logger.warning("chart bridge unavailable (Qt not loaded)")
- return payload
- chart_bridge.sig_data.emit(payload)
- return {"ts": ts, "bind": str(bind), "value": val}
-
-
-def action_chart_add3d(ctx, args: Dict[str, Any]):
- """Push a 3D point. Args: x,y,z (expr), ts(optional), bind_x/y/z(optional keys, default x/y/z)."""
- bx = str(args.get("bind_x", "x"))
- by = str(args.get("bind_y", "y"))
- bz = str(args.get("bind_z", "z"))
- if args.get("x") is None or args.get("y") is None or args.get("z") is None:
- raise ValueError("chart_add3d requires x, y, z")
- x_val = _eval(ctx, args.get("x"))
- y_val = _eval(ctx, args.get("y"))
- z_val = _eval(ctx, args.get("z"))
- ts_arg = args.get("ts") or args.get("timestamp")
- ts = float(_eval(ctx, ts_arg)) if ts_arg is not None else time.time()
- payload = {"ts": ts, bx: x_val, by: y_val, bz: z_val}
- if hasattr(ctx, "record_chart"):
- try:
- ctx.record_chart(payload)
- except Exception:
- pass
- if chart_bridge is None:
- ctx.logger.warning("chart bridge unavailable (Qt not loaded)")
- return payload
- chart_bridge.sig_data.emit(payload)
- return payload
-
-
-def register_chart_actions() -> None:
- ActionRegistry.register("chart_add", action_chart_add)
- ActionRegistry.register("chart_add3d", action_chart_add3d)
diff --git a/actions/data_actions.py b/actions/data_actions.py
deleted file mode 100644
index 321f1d6..0000000
--- a/actions/data_actions.py
+++ /dev/null
@@ -1,149 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Dict, Iterable, List, Tuple
-
-from actions.registry import ActionRegistry
-from dsl.expression import eval_expr
-
-
-def _vars_snapshot(ctx) -> Dict[str, Any]:
- if hasattr(ctx, "vars_snapshot"):
- return ctx.vars_snapshot()
- if hasattr(ctx, "vars"):
- return dict(ctx.vars)
- return {}
-
-
-def _parse_inline_actions(items: Any) -> List[Tuple[str, Dict[str, Any]]]:
- if items is None:
- return []
- if not isinstance(items, list):
- raise ValueError("inline actions must be a list")
- parsed: List[Tuple[str, Dict[str, Any]]] = []
- for item in items:
- if not isinstance(item, dict):
- raise ValueError(f"invalid inline action: {item}")
- if "action" in item:
- name = item["action"]
- args = item.get("args", {}) or {}
- if not isinstance(args, dict):
- raise ValueError(f"action.args must be a mapping: {item}")
- parsed.append((str(name), args))
- continue
- if "set" in item:
- args = item.get("set") or {}
- if not isinstance(args, dict):
- raise ValueError(f"set must be a mapping: {item}")
- parsed.append(("set", args))
- continue
- if "log" in item:
- parsed.append(("log", {"message": item.get("log")}))
- continue
- if "wait" in item:
- wait_cfg = item.get("wait")
- args = wait_cfg if isinstance(wait_cfg, dict) else {"ms": wait_cfg}
- parsed.append(("wait", args))
- continue
- if "wait_for_event" in item:
- wfe = item.get("wait_for_event")
- args = wfe if isinstance(wfe, dict) else {"event": wfe}
- parsed.append(("wait_for_event", args))
- continue
- if "if" in item:
- if_cfg = item.get("if") or {}
- if not isinstance(if_cfg, dict):
- raise ValueError(f"if must be a mapping: {item}")
- parsed.append(("if", if_cfg))
- continue
- raise ValueError(f"unknown inline action type: {item}")
- return parsed
-
-
-def action_if(ctx, args: Dict[str, Any]) -> Dict[str, Any]:
- cond_expr = args.get("when") or args.get("cond")
- if not cond_expr:
- raise ValueError("if requires 'when'")
- cond = bool(eval_expr(str(cond_expr), _vars_snapshot(ctx)))
- then_items = args.get("then") or args.get("do") or []
- else_items = args.get("else") or args.get("otherwise") or []
- items = then_items if cond else else_items
- actions = _parse_inline_actions(items)
- for name, a in actions:
- ctx.run_action(name, a)
- return {"when": str(cond_expr), "taken": "then" if cond else "else", "count": len(actions)}
-
-
-def _iterable_or_error(value: Any, *, name: str) -> Iterable[Any]:
- if isinstance(value, (list, tuple)):
- return value
- raise ValueError(f"{name} must be a list/tuple")
-
-
-def _item_env(item: Any, index: int) -> Dict[str, Any]:
- env: Dict[str, Any] = {"item": item, "index": index}
- if isinstance(item, dict):
- for k, v in item.items():
- if isinstance(k, str) and k.isidentifier():
- env[f"item.{k}"] = v
- return env
-
-
-def action_list_filter(ctx, args: Dict[str, Any]) -> List[Any]:
- src = args.get("src") or args.get("items") or args.get("in")
- if src is None:
- raise ValueError("list_filter requires 'src'")
- src_val = ctx.eval_value(src) if hasattr(ctx, "eval_value") else src
- if isinstance(src, str) and "$" not in src and hasattr(ctx, "vars") and src in ctx.vars:
- src_val = ctx.vars[src]
- where = args.get("where") or args.get("when")
- if not where:
- raise ValueError("list_filter requires 'where'")
- limit = args.get("limit")
- out: List[Any] = []
- base = _vars_snapshot(ctx)
- for idx, item in enumerate(_iterable_or_error(src_val, name="src")):
- env = dict(base)
- env.update(_item_env(item, idx))
- if bool(eval_expr(str(where), env)):
- out.append(item)
- if limit is not None and len(out) >= int(limit):
- break
- dst = args.get("dst") or args.get("out")
- if dst and hasattr(ctx, "set_var"):
- ctx.set_var(str(dst), out)
- return out
-
-
-def action_list_map(ctx, args: Dict[str, Any]) -> List[Any]:
- src = args.get("src") or args.get("items") or args.get("in")
- if src is None:
- raise ValueError("list_map requires 'src'")
- src_val = ctx.eval_value(src) if hasattr(ctx, "eval_value") else src
- if isinstance(src, str) and "$" not in src and hasattr(ctx, "vars") and src in ctx.vars:
- src_val = ctx.vars[src]
- expr = args.get("expr") or args.get("map") or args.get("value")
- if expr is None:
- raise ValueError("list_map requires 'expr'")
- where = args.get("where") or args.get("when")
- limit = args.get("limit")
- out: List[Any] = []
- base = _vars_snapshot(ctx)
- for idx, item in enumerate(_iterable_or_error(src_val, name="src")):
- env = dict(base)
- env.update(_item_env(item, idx))
- if where and not bool(eval_expr(str(where), env)):
- continue
- out.append(eval_expr(str(expr), env))
- if limit is not None and len(out) >= int(limit):
- break
- dst = args.get("dst") or args.get("out")
- if dst and hasattr(ctx, "set_var"):
- ctx.set_var(str(dst), out)
- return out
-
-
-def register_data_actions() -> None:
- ActionRegistry.register("if", action_if)
- ActionRegistry.register("list_filter", action_list_filter)
- ActionRegistry.register("list_map", action_list_map)
-
diff --git a/actions/modbus_request.py b/actions/modbus_request.py
deleted file mode 100644
index 5b6e943..0000000
--- a/actions/modbus_request.py
+++ /dev/null
@@ -1,46 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Dict
-
-from protocols.registry import ProtocolRegistry
-
-_MAP = {
- "rtu": "modbus_rtu",
- "ascii": "modbus_ascii",
- "tcp": "modbus_tcp",
-}
-
-
-def run(task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any:
- protocol_key = _MAP.get(str(task.get("protocol", "")).lower())
- if not protocol_key:
- raise ValueError("modbus_request.protocol 必须为 rtu / ascii / tcp")
-
- channel_name = task.get("channel")
- if not channel_name or channel_name not in channels:
- raise KeyError(f"未找到通道: {channel_name}")
-
- function = int(task.get("function"))
- address = int(task.get("address"))
- quantity = int(task.get("quantity", 1))
- values = task.get("values")
- unit_id = int(task.get("unit_id", 1))
-
- protocol_cls = ProtocolRegistry.get(protocol_key)
- protocol = protocol_cls(channels[channel_name], logger)
-
- if protocol_key == "modbus_tcp":
- timeout_s = float(task.get("timeout", 2.0))
- return protocol.execute(function=function, address=address, quantity=quantity, values=values, unit_id=unit_id, timeout=timeout_s)
-
- retries = int(task.get("retries", 3))
- timeout_ms = int(task.get("timeout", 1000))
- return protocol.execute(
- function=function,
- address=address,
- quantity=quantity,
- values=values,
- unit_id=unit_id,
- retries=retries,
- timeout=timeout_ms,
- )
diff --git a/actions/record_actions.py b/actions/record_actions.py
deleted file mode 100644
index 6ff44a2..0000000
--- a/actions/record_actions.py
+++ /dev/null
@@ -1,81 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Dict
-
-from actions.registry import ActionRegistry
-from runtime.experiment_recorder import ExperimentRecorder
-
-
-def _eval_str(ctx, value: Any) -> str:
- v = ctx.eval_value(value) if hasattr(ctx, "eval_value") else value
- return str(v) if v is not None else ""
-
-
-def action_record_start(ctx, args: Dict[str, Any]):
- if getattr(ctx, "recorder", None) is not None:
- # Already recording; return current path if available.
- rec = ctx.recorder
- return str(getattr(rec, "paths", {}).root) if rec else None
-
- raw_dir = args.get("dir")
- base_dir = _eval_str(ctx, raw_dir) if raw_dir is not None else None
- name = _eval_str(ctx, args.get("name", "run")) or "run"
- script_text = args.get("script_text")
- script_path = args.get("script_path")
-
- if script_text is not None and hasattr(ctx, "eval_value"):
- script_text = ctx.eval_value(script_text)
- script_text = str(script_text) if script_text else getattr(ctx, "script_text", None)
-
- if script_path is not None and hasattr(ctx, "eval_value"):
- script_path = ctx.eval_value(script_path)
- script_path = str(script_path) if script_path else getattr(ctx, "script_path", None)
-
- rec = ExperimentRecorder(base_dir=base_dir, name=name, script_text=script_text, script_path=script_path)
- root = rec.start()
-
- if hasattr(ctx, "attach_recorder"):
- ctx.attach_recorder(rec)
- else:
- ctx.recorder = rec
- if hasattr(ctx, "logger"):
- try:
- ctx.logger.info(f"[REC] start -> {root}")
- except Exception:
- pass
- if hasattr(ctx, "set_var"):
- try:
- ctx.set_var("record_dir", str(root))
- except Exception:
- pass
- return str(root)
-
-
-def action_record_stop(ctx, args: Dict[str, Any]):
- rec = getattr(ctx, "recorder", None)
- if rec is None:
- return None
- vars_snapshot = None
- if hasattr(ctx, "vars_snapshot"):
- try:
- vars_snapshot = ctx.vars_snapshot()
- except Exception:
- vars_snapshot = None
- try:
- rec.close(vars_snapshot=vars_snapshot)
- finally:
- if hasattr(ctx, "detach_recorder"):
- ctx.detach_recorder()
- else:
- ctx.recorder = None
- if hasattr(ctx, "logger"):
- try:
- ctx.logger.info(f"[REC] stop -> {rec.paths.root}")
- except Exception:
- pass
- return str(rec.paths.root)
-
-
-def register_record_actions() -> None:
- ActionRegistry.register("record_start", action_record_start)
- ActionRegistry.register("record_stop", action_record_stop)
diff --git a/actions/registry.py b/actions/registry.py
deleted file mode 100644
index 1be019e..0000000
--- a/actions/registry.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Callable, Dict
-
-
-class ActionRegistry:
- actions: Dict[str, Callable[..., Any]] = {}
-
- @classmethod
- def register(cls, name: str, fn: Callable[..., Any]) -> None:
- cls.actions[name] = fn
-
- @classmethod
- def get(cls, name: str) -> Callable[..., Any]:
- if name not in cls.actions:
- raise KeyError(f"动作未注册: {name}")
- return cls.actions[name]
diff --git a/actions/schema_protocol.py b/actions/schema_protocol.py
deleted file mode 100644
index 06d3ab5..0000000
--- a/actions/schema_protocol.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from __future__ import annotations
-
-from functools import lru_cache
-from typing import Any, Dict
-
-from actions.registry import ActionRegistry
-from protocols.schema_runtime import ProtocolSchema
-
-
-@lru_cache(maxsize=16)
-def _load_schema(path: str) -> ProtocolSchema:
- return ProtocolSchema.load(path)
-
-
-def action_send_frame(ctx, args: Dict[str, Any]):
- schema_path = args.get("schema")
- frame = args.get("frame")
- if not schema_path or not frame:
- raise ValueError("send_frame requires schema and frame")
- values = {k: ctx.eval_value(v) for k, v in (args.get("values") or {}).items()}
- schema = _load_schema(str(schema_path))
- packet = schema.build(frame, values)
- ctx.channel_write(packet)
- ctx.set_var("last_frame_tx", {"frame": frame, "values": values, "hex": packet.hex().upper()})
- return packet
-
-
-def action_expect_frame(ctx, args: Dict[str, Any]):
- schema_path = args.get("schema")
- frame = args.get("frame")
- timeout = float(args.get("timeout", 2.0))
- save_as = args.get("save_as", "last_frame_rx")
- if not schema_path or not frame:
- raise ValueError("expect_frame requires schema and frame")
-
- schema = _load_schema(str(schema_path))
- fd = schema.frames.get(frame)
- if fd is None:
- raise KeyError(f"unknown frame: {frame}")
-
- data = b""
- if fd.tail:
- data = ctx.channel.read_until(fd.tail, timeout=timeout)
- else:
- fixed = fd.fixed_length()
- if fixed is None:
- raise ValueError("expect_frame requires tail or fixed frame length")
- data = ctx.channel.read_exact(fixed, timeout=timeout) # type: ignore[attr-defined]
-
- if not data:
- raise TimeoutError("expect_frame timeout")
-
- parsed = schema.parse(frame, data)
- ctx.set_var(save_as, parsed)
- ctx.set_var("last_frame_rx_raw", data.hex().upper())
- return parsed
-
-
-def register_schema_protocol_actions() -> None:
- ActionRegistry.register("send_frame", action_send_frame)
- ActionRegistry.register("expect_frame", action_expect_frame)
diff --git a/actions/scpi_command.py b/actions/scpi_command.py
deleted file mode 100644
index 7e9d530..0000000
--- a/actions/scpi_command.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Dict
-
-from protocols.registry import ProtocolRegistry
-
-
-def run(task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any:
- channel_name = task.get("channel")
- if not channel_name or channel_name not in channels:
- raise KeyError(f"channel not found: {channel_name}")
-
- cmd = task.get("cmd") or task.get("command")
- if not cmd:
- raise ValueError("scpi_command.cmd is required")
-
- timeout = float(task.get("timeout", 2.0))
- terminator = task.get("terminator", "\n")
- expect_response = task.get("expect_response")
- strip = bool(task.get("strip", True))
-
- protocol_cls = ProtocolRegistry.get("scpi")
- protocol = protocol_cls(channels[channel_name], logger)
- return protocol.execute(
- cmd=str(cmd),
- expect_response=expect_response,
- timeout=timeout,
- terminator=terminator,
- strip=strip,
- )
diff --git a/actions/xmodem_send.py b/actions/xmodem_send.py
deleted file mode 100644
index a634bea..0000000
--- a/actions/xmodem_send.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Dict
-
-from protocols.registry import ProtocolRegistry
-
-
-def run(task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any:
- channel_name = task.get("channel")
- if not channel_name or channel_name not in channels:
- raise KeyError(f"未找到通道: {channel_name}")
- file_path = task.get("file") or task.get("path")
- if not file_path:
- raise ValueError("xmodem_send 需要 file/path 参数")
-
- retries = int(task.get("retries", 10))
- start_timeout = float(task.get("start_timeout", 10.0))
-
- protocol_cls = ProtocolRegistry.get("xmodem")
- protocol = protocol_cls(channels[channel_name], logger)
- return protocol.execute(file_path=file_path, retries=retries, start_timeout=start_timeout)
diff --git a/actions/ymodem_send.py b/actions/ymodem_send.py
deleted file mode 100644
index 4c5846d..0000000
--- a/actions/ymodem_send.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Dict
-
-from protocols.registry import ProtocolRegistry
-
-
-def run(task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any:
- channel_name = task.get("channel")
- if not channel_name or channel_name not in channels:
- raise KeyError(f"未找到通道: {channel_name}")
- file_path = task.get("file") or task.get("path")
- if not file_path:
- raise ValueError("ymodem_send 需要 file/path 参数")
-
- retries = int(task.get("retries", 10))
- start_timeout = float(task.get("start_timeout", 10.0))
-
- protocol_cls = ProtocolRegistry.get("ymodem")
- protocol = protocol_cls(channels[channel_name], logger)
- return protocol.execute(file_path=file_path, retries=retries, start_timeout=start_timeout)
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..f1ee259
--- /dev/null
+++ b/app/__init__.py
@@ -0,0 +1 @@
+# Application entrypoints package.
diff --git a/dsl_main.py b/app/dsl_main.py
similarity index 88%
rename from dsl_main.py
rename to app/dsl_main.py
index 43d6dca..399ab86 100644
--- a/dsl_main.py
+++ b/app/dsl_main.py
@@ -3,7 +3,7 @@
import argparse
import sys
-from runtime.runner import run_dsl
+from dsl_runtime.engine.runner import run_dsl
def main(argv=None) -> int:
diff --git a/main_web.py b/app/main_web.py
similarity index 91%
rename from main_web.py
rename to app/main_web.py
index 626c6b1..e2e8ee2 100644
--- a/main_web.py
+++ b/app/main_web.py
@@ -17,12 +17,12 @@
except ImportError: # pragma: no cover
from PyQt6.QtWidgets import QApplication # type: ignore
-from core.communication_manager import CommunicationManager
-from core.event_bus import EventBus
-from core.packet_engine import PacketAnalysisEngine
-from core.plugin_manager import PluginManager
-from core.protocol_loader import ProtocolLoader
-from desktop.web_window import WebWindow
+from infra.comm.communication_manager import CommunicationManager
+from infra.common.event_bus import EventBus
+from app.packet_engine import PacketAnalysisEngine
+from app.plugin_manager import PluginManager
+from infra.protocol.protocol_loader import ProtocolLoader
+from ui.desktop.web_window import WebWindow
class _TeeStream:
@@ -64,8 +64,8 @@ def flush(self) -> None:
def _setup_run_logging() -> Path:
- base_dir = Path(__file__).resolve().parent
- user_root = Path(os.environ.get("LOCALAPPDATA", base_dir))
+ root_dir = Path(__file__).resolve().parents[1]
+ user_root = Path(os.environ.get("LOCALAPPDATA", root_dir))
log_dir = user_root / "ProtoFlow" / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
timestamp = time.strftime("%Y%m%d_%H%M%S")
@@ -97,7 +97,7 @@ def _log_unraisable(unraisable): # type: ignore[override]
def _ensure_repo_cwd() -> None:
- base_dir = Path(__file__).resolve().parent
+ base_dir = Path(__file__).resolve().parents[1]
try:
os.chdir(base_dir)
except OSError:
diff --git a/core/packet_engine.py b/app/packet_engine.py
similarity index 98%
rename from core/packet_engine.py
rename to app/packet_engine.py
index 59da002..ecf694c 100644
--- a/core/packet_engine.py
+++ b/app/packet_engine.py
@@ -11,8 +11,8 @@
from dataclasses import dataclass
from typing import Any, Dict, Iterable, List, Optional, Tuple
-from core.event_bus import EventBus
-from core.protocol_loader import crc16_modbus
+from infra.common.event_bus import EventBus
+from infra.protocol.protocol_loader import crc16_modbus
@dataclass
diff --git a/core/plugin_manager.py b/app/plugin_manager.py
similarity index 97%
rename from core/plugin_manager.py
rename to app/plugin_manager.py
index 0e990fd..b77e20e 100644
--- a/core/plugin_manager.py
+++ b/app/plugin_manager.py
@@ -10,8 +10,8 @@
from types import ModuleType
from typing import Dict, List, Optional
-from core.event_bus import EventBus
-from utils.path_utils import resolve_resource_path
+from infra.common.event_bus import EventBus
+from infra.common.utils.path_utils import resolve_resource_path
class PluginManager:
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
index e378077..bbe4919 100644
--- a/docs/USER_GUIDE.md
+++ b/docs/USER_GUIDE.md
@@ -8,7 +8,7 @@ ProtoFlow 是面向嵌入式/工控/自动化测试的通信自动化运行时
## 2. 安装与运行
- 依赖:`pip install pyyaml`,使用串口需 `pip install pyserial`。
-- 入口:`python dsl_main.py `
+- 入口:`python app/dsl_main.py `
- 输入:符合 DSL 规范的 YAML 脚本。
- 输出:日志(INFO/DEBUG),状态机执行的事件流;动作可产生下行数据,通道可回传事件。
@@ -104,7 +104,7 @@ stateDiagram-v2
- `list_filter`: 列表过滤(`src`/`where`,可选 `dst`)。
- `list_map`: 列表映射(`src`/`expr`,可选 `dst`,可选 `where`)。
- 协议动作:XMODEM/Modbus 等(下文详述)。
-- 自定义动作:在 Python 中 `ActionRegistry.register("name", fn)` 注册,`fn(ctx, args)` 使用 `ctx.channel_write` / `ctx.set_var` / `ctx.vars_snapshot`。
+- 自定义动作:继承 `DslActionBase`,实现 `execute(ctx, args)` 并定义参数 `schema`,使用 `ActionRegistry.register("name", MyAction())` 注册。
## 9. XMODEM 动作
- `send_xmodem_block`:发送指定块号(128B,自动 0x1A 填充),参数 `block: "$block"`。
@@ -118,7 +118,7 @@ stateDiagram-v2
- 预留动作:`modbus_read` / `modbus_write`(当前 DSL Runner 未实现,仅文档占位)
- 参数:`protocol: rtu|ascii|tcp`,`function`,`address`,`quantity`,`values`(写),`unit_id`。
- 差异:RTU(CRC16,二进制);ASCII(LRC,文本帧);TCP(MBAP,无 CRC)。
-说明:仓库中已实现 Modbus 协议驱动(`protocols/modbus_*.py`),并可在 `main_runtime.py` 的 tasks 模式中调用;若要在 DSL 中使用需新增对应动作注册。
+说明:仓库中已实现 Modbus 协议驱动(`infra/protocol/modbus_*.py`),可在 DSL 动作中调用(已注册 `modbus_read/modbus_write`)。
## 12. 事件系统(Events)
- 来源:通道 `read_event`(UART/TCP 读取到的字节,默认字符;无法解码则 HEX 字符串)。
@@ -267,15 +267,31 @@ state_machine:
## 15. 扩展指南
- 添加新动作:
```python
- from actions.registry import ActionRegistry
- def my_action(ctx, args):
- # ctx.channel_write / ctx.set_var / ctx.vars_snapshot()
- ...
- ActionRegistry.register("my_action", my_action)
+ from dsl_runtime.actions.base import DslActionBase
+ from dsl_runtime.actions.registry import ActionRegistry
+
+ class MyAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="my_action",
+ schema={
+ "required": ["foo"],
+ "optional": {"bar": 1},
+ "types": {"foo": "string", "bar": "number"},
+ "aliases": {"baz": "foo"},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args):
+ # ctx.channel_write / ctx.set_var / ctx.vars_snapshot()
+ ...
+
+ ActionRegistry.register("my_action", MyAction())
```
-- 添加新协议动作:在 `actions/*.py` 中封装协议逻辑,调用协议封包构造器(如 XMODEM/Modbus)。
+- 添加新协议动作:在 `dsl_runtime/actions/*.py` 中封装协议逻辑,调用协议封包构造器(如 XMODEM/Modbus)。
- 添加新协议适配:实现协议封包/解析,供动作调用。
-- 扩展 DSL:修改 `dsl/parser.py` / `dsl/ast_nodes.py` / `dsl/executor.py` 增加新语法字段,保持向后兼容。
+- 扩展 DSL:修改 `dsl_runtime/lang/parser.py` / `dsl_runtime/lang/ast_nodes.py` / `dsl_runtime/lang/executor.py` 增加新语法字段,保持向后兼容。
- 让 AI 编写 DSL:提供章节 7/8 模板,明确事件名、超时、变量命名,AI 可按样例生成 YAML。
## 16. 附录
diff --git a/docs/USER_GUIDE_EN.md b/docs/USER_GUIDE_EN.md
index d3fa77b..f051e7d 100644
--- a/docs/USER_GUIDE_EN.md
+++ b/docs/USER_GUIDE_EN.md
@@ -6,7 +6,7 @@ Pipeline: `YAML DSL → state machine → actions → protocol adapter → chann
## 2. Installation & Run
- Dependencies: `pip install pyyaml`; for serial use `pip install pyserial`.
-- Entry point: `python dsl_main.py `
+- Entry point: `python app/dsl_main.py `
- Input: YAML script that follows the DSL spec.
- Output: logs (INFO/DEBUG), state-machine event trace; actions can emit outbound data, channels can raise events.
@@ -97,7 +97,7 @@ stateDiagram-v2
- `list_filter`: filter a list into a new list (`src`, `where`, optional `dst`).
- `list_map`: map a list into a new list (`src`, `expr`, optional `dst`, optional `where`).
- Protocol actions: XMODEM/Modbus etc. (see below).
-- Custom actions: in Python `ActionRegistry.register("name", fn)`, where `fn(ctx, args)` can use `ctx.channel_write` / `ctx.set_var` / `ctx.vars_snapshot`.
+- Custom actions: subclass `DslActionBase`, implement `execute(ctx, args)` with a parameter `schema`, then register the instance via `ActionRegistry.register("name", MyAction())`.
### 8.2 Data Processing (filter/transform)
`if` (recommended to reduce extra states when you only need to filter/branch inside `do`):
@@ -160,13 +160,18 @@ do:
- `expect_frame`: reads by tail or fixed length, parses, stores result in `save_as` (default `last_frame_rx`), raw hex in `last_frame_rx_raw`.
3) Register more custom actions (also applied at startup):
```python
- from actions.registry import ActionRegistry
+ from dsl_runtime.actions.base import DslActionBase
+ from dsl_runtime.actions.registry import ActionRegistry
- def my_action(ctx, args):
- # e.g., write custom bytes or combine multiple steps
- ctx.channel_write(b"hello")
+ class MyAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(name="my_action", schema={"allow_extra": False})
- ActionRegistry.register("my_action", my_action)
+ def execute(self, ctx, args):
+ # e.g., write custom bytes or combine multiple steps
+ ctx.channel_write(b"hello")
+
+ ActionRegistry.register("my_action", MyAction())
```
Then call in DSL: `- action: my_action`.
@@ -182,7 +187,7 @@ Currently examples are XMODEM-focused; YMODEM can be added similarly with action
- Reserved actions: `modbus_read` / `modbus_write` (not implemented in current DSL runner; docs placeholder)
- Args: `protocol: rtu|ascii|tcp`, `function`, `address`, `quantity`, `values` (for write), `unit_id`.
- Differences: RTU (CRC16, binary); ASCII (LRC, text frame); TCP (MBAP, no CRC).
-- Note: Modbus protocol drivers exist under `protocols/modbus_*.py` and are callable from `main_runtime.py` tasks mode; adding DSL actions requires registering them.
+- Note: Modbus protocol drivers exist under `infra/protocol/modbus_*.py` and are available via DSL actions (`modbus_read/modbus_write`).
## 12. Event System
- Sources: channel `read_event` (UART/TCP bytes; default decoded to text, fallback HEX string).
@@ -331,15 +336,15 @@ state_machine:
## 15. Extension Guide
- Add new action:
```python
- from actions.registry import ActionRegistry
+ from dsl_runtime.actions.registry import ActionRegistry
def my_action(ctx, args):
# ctx.channel_write / ctx.set_var / ctx.vars_snapshot()
...
- ActionRegistry.register("my_action", my_action)
+ ActionRegistry.register("my_action", MyAction())
```
-- Add new protocol actions: encapsulate protocol logic in `actions/*.py`, call protocol pack/unpack helpers (e.g., XMODEM/Modbus).
+- Add new protocol actions: encapsulate protocol logic in `dsl_runtime/actions/*.py`, call protocol pack/unpack helpers (e.g., XMODEM/Modbus).
- Add new protocol adapter: implement packet build/parse for actions to call.
-- Extend DSL: edit `dsl/parser.py` / `dsl/ast_nodes.py` / `dsl/executor.py` to add syntax (keep backward compatibility).
+- Extend DSL: edit `dsl_runtime/lang/parser.py` / `dsl_runtime/lang/ast_nodes.py` / `dsl_runtime/lang/executor.py` to add syntax (keep backward compatibility).
- Let an AI draft DSL: provide templates from sections 7/8 with event names, timeouts, variable names; an AI can generate YAML by example.
## 16. Appendix
diff --git a/dsl_runtime/__init__.py b/dsl_runtime/__init__.py
new file mode 100644
index 0000000..4a6b95c
--- /dev/null
+++ b/dsl_runtime/__init__.py
@@ -0,0 +1 @@
+# DSL runtime package.
diff --git a/dsl_runtime/actions/__init__.py b/dsl_runtime/actions/__init__.py
new file mode 100644
index 0000000..a67640c
--- /dev/null
+++ b/dsl_runtime/actions/__init__.py
@@ -0,0 +1 @@
+# DSL action modules package.
diff --git a/dsl_runtime/actions/base.py b/dsl_runtime/actions/base.py
new file mode 100644
index 0000000..2c9fc8a
--- /dev/null
+++ b/dsl_runtime/actions/base.py
@@ -0,0 +1,99 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any, Dict, Tuple, Type
+
+
+class ActionValidationError(ValueError):
+ pass
+
+
+_TYPE_ALIASES: Dict[str, Tuple[Type[Any], ...]] = {
+ "number": (int, float),
+ "string": (str,),
+ "bool": (bool,),
+ "mapping": (dict,),
+ "list": (list, tuple),
+}
+
+
+@dataclass
+class DslActionBase:
+ name: str
+ schema: Dict[str, Any] | None = None
+
+ def run(self, ctx, args: Dict[str, Any] | None) -> Any:
+ payload = self.validate_args(args)
+ return self.execute(ctx, payload)
+
+ def execute(self, ctx, args: Dict[str, Any]) -> Any:
+ raise NotImplementedError()
+
+ def validate_args(self, args: Dict[str, Any] | None) -> Dict[str, Any]:
+ if args is None:
+ args = {}
+ if not isinstance(args, dict):
+ raise ActionValidationError("action args must be a mapping")
+ schema = self.schema or {}
+ aliases = schema.get("aliases", {}) or {}
+ required = list(schema.get("required", []) or [])
+ optional = dict(schema.get("optional", {}) or {})
+ types = dict(schema.get("types", {}) or {})
+ allow_extra = bool(schema.get("allow_extra", True))
+
+ normalized = dict(args)
+ for alias, target in aliases.items():
+ if alias in normalized and target not in normalized:
+ normalized[target] = normalized.pop(alias)
+
+ for key, spec in optional.items():
+ if key not in normalized:
+ if isinstance(spec, dict) and "default" in spec:
+ normalized[key] = self._copy_default(spec.get("default"))
+ else:
+ normalized[key] = self._copy_default(spec)
+
+ for key in required:
+ if key not in normalized:
+ raise ActionValidationError(f"missing required arg: {key}")
+
+ if not allow_extra:
+ allowed = set(required) | set(optional) | set(types)
+ extra = [k for k in normalized.keys() if k not in allowed]
+ if extra:
+ raise ActionValidationError(f"unknown args: {', '.join(extra)}")
+
+ for key, type_spec in types.items():
+ if key not in normalized:
+ continue
+ value = normalized.get(key)
+ if value is None:
+ continue
+ expected = self._resolve_type_spec(type_spec)
+ if expected is not None and not isinstance(value, expected):
+ raise ActionValidationError(
+ f"arg '{key}' has invalid type: {type(value).__name__}"
+ )
+ return normalized
+
+ @staticmethod
+ def _resolve_type_spec(type_spec: Any) -> Tuple[Type[Any], ...] | None:
+ if type_spec is None:
+ return None
+ if isinstance(type_spec, tuple):
+ return type_spec
+ if isinstance(type_spec, list):
+ return tuple(type_spec)
+ if isinstance(type_spec, str):
+ return _TYPE_ALIASES.get(type_spec)
+ if isinstance(type_spec, type):
+ return (type_spec,)
+ return None
+
+ @staticmethod
+ def _copy_default(value: Any) -> Any:
+ if isinstance(value, dict):
+ return dict(value)
+ if isinstance(value, list):
+ return list(value)
+ return value
diff --git a/actions/chart_bridge.py b/dsl_runtime/actions/chart_bridge.py
similarity index 100%
rename from actions/chart_bridge.py
rename to dsl_runtime/actions/chart_bridge.py
diff --git a/dsl_runtime/actions/dsl_builtin_actions.py b/dsl_runtime/actions/dsl_builtin_actions.py
new file mode 100644
index 0000000..e10bbad
--- /dev/null
+++ b/dsl_runtime/actions/dsl_builtin_actions.py
@@ -0,0 +1,199 @@
+from __future__ import annotations
+
+import time
+from typing import Any, Dict
+
+from dsl_runtime.actions.base import DslActionBase
+from dsl_runtime.actions.registry import ActionRegistry
+from dsl_runtime.lang.expression import eval_expr
+
+
+def _eval(ctx, val):
+ if isinstance(val, str) and "$" in val:
+ return eval_expr(val, ctx.vars_snapshot())
+ return val
+
+
+class SetAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="set",
+ schema={
+ "allow_extra": True,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ for key, val in args.items():
+ ctx.set_var(key, _eval(ctx, val))
+ return ctx.vars_snapshot()
+
+
+class LogAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="log",
+ schema={
+ "aliases": {"msg": "message"},
+ "optional": {"message": ""},
+ "types": {"message": (str, int, float, bool, bytes, bytearray)},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ msg = args.get("message") or ""
+ if isinstance(msg, str) and "$" in msg:
+ msg = eval_expr(msg, ctx.vars_snapshot())
+ ctx.logger.info(str(msg))
+
+
+class WaitAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="wait",
+ schema={
+ "optional": {"ms": 0},
+ "types": {"ms": "number"},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ ms = int(args.get("ms", 0))
+ time.sleep(ms / 1000.0)
+
+
+class WaitForEventAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="wait_for_event",
+ schema={
+ "optional": {"event": None, "timeout": 1.0},
+ "types": {"timeout": "number"},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ expected = args.get("event")
+ timeout = float(args.get("timeout", 1.0))
+ end = time.time() + timeout
+ while time.time() < end:
+ evt = ctx.next_event(timeout=0.1)
+ if evt is None:
+ continue
+ if expected is None or evt == expected:
+ ctx.set_var("event", evt)
+ return evt
+ return None
+
+
+class SendTextAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="send_text",
+ schema={
+ "aliases": {"data": "text"},
+ "optional": {"text": "", "append_cr": False, "append_lf": False},
+ "types": {"append_cr": bool, "append_lf": bool},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ text = args.get("text", "")
+ if isinstance(text, (bytes, bytearray)):
+ payload = bytes(text)
+ else:
+ payload = str(text)
+ append_cr = bool(args.get("append_cr", False))
+ append_lf = bool(args.get("append_lf", False))
+ if append_cr:
+ payload = payload + (b"\r" if isinstance(payload, (bytes, bytearray)) else "\r")
+ if append_lf:
+ payload = payload + (b"\n" if isinstance(payload, (bytes, bytearray)) else "\n")
+ ctx.channel_write(payload)
+ return {"text": text, "append_cr": append_cr, "append_lf": append_lf}
+
+
+class ReadLineAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="read_line",
+ schema={
+ "optional": {"terminator": "\n", "timeout": 1.0},
+ "types": {"timeout": "number"},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ terminator = args.get("terminator", "\n")
+ timeout = float(args.get("timeout", 1.0))
+ raw = ctx.channel.read_until(
+ terminator.encode() if isinstance(terminator, str) else bytes(terminator),
+ timeout=timeout,
+ )
+ text = raw.decode(errors="ignore").strip()
+ ctx.set_var("last_line_rx", text)
+ ctx.set_var("last_line_rx_raw", raw.hex().upper())
+ return {"text": text, "hex": raw.hex().upper()}
+
+
+class ReadStreamAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="read_stream",
+ schema={
+ "aliases": {"duration": "duration_ms"},
+ "optional": {
+ "duration_ms": 1000,
+ "chunk_size": 256,
+ "timeout": 0.2,
+ "log_hex": True,
+ },
+ "types": {
+ "duration_ms": "number",
+ "chunk_size": "number",
+ "timeout": "number",
+ "log_hex": bool,
+ },
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ duration_ms = int(args.get("duration_ms", 1000))
+ chunk_size = int(args.get("chunk_size", 256))
+ timeout = float(args.get("timeout", 0.2))
+ log_hex = bool(args.get("log_hex", True))
+ end = time.time() + max(0.0, duration_ms / 1000.0)
+ last_text = ""
+ last_hex = ""
+ while time.time() < end:
+ chunk = ctx.channel.read(chunk_size, timeout=timeout)
+ if not chunk:
+ continue
+ last_hex = chunk.hex().upper()
+ try:
+ last_text = chunk.decode(errors="ignore")
+ except Exception:
+ last_text = ""
+ if log_hex:
+ ctx.logger.info(f"RX(hex): {last_hex}")
+ if last_text.strip():
+ ctx.logger.info(f"RX(text): {last_text.strip()}")
+ ctx.set_var("last_stream_rx", last_text.strip())
+ ctx.set_var("last_stream_rx_raw", last_hex)
+ return {"text": last_text.strip(), "hex": last_hex}
+
+
+def register_builtin_actions() -> None:
+ ActionRegistry.register("set", SetAction())
+ ActionRegistry.register("log", LogAction())
+ ActionRegistry.register("send_text", SendTextAction())
+ ActionRegistry.register("read_line", ReadLineAction())
+ ActionRegistry.register("read_stream", ReadStreamAction())
+ ActionRegistry.register("wait", WaitAction())
+ ActionRegistry.register("wait_for_event", WaitForEventAction())
diff --git a/dsl_runtime/actions/dsl_chart_actions.py b/dsl_runtime/actions/dsl_chart_actions.py
new file mode 100644
index 0000000..f030f8e
--- /dev/null
+++ b/dsl_runtime/actions/dsl_chart_actions.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+import time
+from typing import Any, Dict
+
+from dsl_runtime.actions.base import DslActionBase
+from dsl_runtime.actions.chart_bridge import chart_bridge
+from dsl_runtime.actions.registry import ActionRegistry
+
+
+def _eval(ctx, val: Any) -> Any:
+ if hasattr(ctx, "eval_value"):
+ return ctx.eval_value(val)
+ return val
+
+
+class ChartAddAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="chart_add",
+ schema={
+ "required": ["bind", "value"],
+ "aliases": {"val": "value", "timestamp": "ts"},
+ "optional": {"ts": None},
+ "types": {"ts": "number"},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ bind = args.get("bind")
+ raw_val = args.get("value")
+ ts_arg = args.get("ts")
+ ts = float(_eval(ctx, ts_arg)) if ts_arg is not None else time.time()
+ try:
+ val = float(_eval(ctx, raw_val))
+ except Exception as exc:
+ raise ValueError(f"chart_add value invalid: {exc}") from exc
+ payload = {"ts": ts, str(bind): val}
+ if hasattr(ctx, "record_chart"):
+ try:
+ ctx.record_chart(payload)
+ except Exception:
+ pass
+ if chart_bridge is None:
+ ctx.logger.warning("chart bridge unavailable (Qt not loaded)")
+ return payload
+ chart_bridge.sig_data.emit(payload)
+ return {"ts": ts, "bind": str(bind), "value": val}
+
+
+class ChartAdd3dAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="chart_add3d",
+ schema={
+ "required": ["x", "y", "z"],
+ "aliases": {"timestamp": "ts"},
+ "optional": {"ts": None, "bind_x": "x", "bind_y": "y", "bind_z": "z"},
+ "types": {"ts": "number"},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ bx = str(args.get("bind_x", "x"))
+ by = str(args.get("bind_y", "y"))
+ bz = str(args.get("bind_z", "z"))
+ x_val = _eval(ctx, args.get("x"))
+ y_val = _eval(ctx, args.get("y"))
+ z_val = _eval(ctx, args.get("z"))
+ ts_arg = args.get("ts")
+ ts = float(_eval(ctx, ts_arg)) if ts_arg is not None else time.time()
+ payload = {"ts": ts, bx: x_val, by: y_val, bz: z_val}
+ if hasattr(ctx, "record_chart"):
+ try:
+ ctx.record_chart(payload)
+ except Exception:
+ pass
+ if chart_bridge is None:
+ ctx.logger.warning("chart bridge unavailable (Qt not loaded)")
+ return payload
+ chart_bridge.sig_data.emit(payload)
+ return payload
+
+
+def register_chart_actions() -> None:
+ ActionRegistry.register("chart_add", ChartAddAction())
+ ActionRegistry.register("chart_add3d", ChartAdd3dAction())
diff --git a/dsl_runtime/actions/dsl_data_actions.py b/dsl_runtime/actions/dsl_data_actions.py
new file mode 100644
index 0000000..b133382
--- /dev/null
+++ b/dsl_runtime/actions/dsl_data_actions.py
@@ -0,0 +1,175 @@
+from __future__ import annotations
+
+from typing import Any, Dict, Iterable, List, Tuple
+
+from dsl_runtime.actions.base import DslActionBase
+from dsl_runtime.actions.registry import ActionRegistry
+from dsl_runtime.lang.expression import eval_expr
+
+
+def _vars_snapshot(ctx) -> Dict[str, Any]:
+ if hasattr(ctx, "vars_snapshot"):
+ return ctx.vars_snapshot()
+ if hasattr(ctx, "vars"):
+ return dict(ctx.vars)
+ return {}
+
+
+def _parse_inline_actions(items: Any) -> List[Tuple[str, Dict[str, Any]]]:
+ if items is None:
+ return []
+ if not isinstance(items, list):
+ raise ValueError("inline actions must be a list")
+ parsed: List[Tuple[str, Dict[str, Any]]] = []
+ for item in items:
+ if not isinstance(item, dict):
+ raise ValueError(f"invalid inline action: {item}")
+ if "action" in item:
+ name = item["action"]
+ args = item.get("args", {}) or {}
+ if not isinstance(args, dict):
+ raise ValueError(f"action.args must be a mapping: {item}")
+ parsed.append((str(name), args))
+ continue
+ if "set" in item:
+ args = item.get("set") or {}
+ if not isinstance(args, dict):
+ raise ValueError(f"set must be a mapping: {item}")
+ parsed.append(("set", args))
+ continue
+ if "log" in item:
+ parsed.append(("log", {"message": item.get("log")}))
+ continue
+ if "wait" in item:
+ wait_cfg = item.get("wait")
+ args = wait_cfg if isinstance(wait_cfg, dict) else {"ms": wait_cfg}
+ parsed.append(("wait", args))
+ continue
+ if "wait_for_event" in item:
+ wfe = item.get("wait_for_event")
+ args = wfe if isinstance(wfe, dict) else {"event": wfe}
+ parsed.append(("wait_for_event", args))
+ continue
+ if "if" in item:
+ if_cfg = item.get("if") or {}
+ if not isinstance(if_cfg, dict):
+ raise ValueError(f"if must be a mapping: {item}")
+ parsed.append(("if", if_cfg))
+ continue
+ raise ValueError(f"unknown inline action type: {item}")
+ return parsed
+
+
+class IfAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="if",
+ schema={
+ "aliases": {"cond": "when", "do": "then", "otherwise": "else"},
+ "required": ["when"],
+ "optional": {"then": [], "else": []},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]) -> Dict[str, Any]:
+ cond_expr = args.get("when")
+ cond = bool(eval_expr(str(cond_expr), _vars_snapshot(ctx)))
+ then_items = args.get("then") or []
+ else_items = args.get("else") or []
+ items = then_items if cond else else_items
+ actions = _parse_inline_actions(items)
+ for name, a in actions:
+ ctx.run_action(name, a)
+ return {"when": str(cond_expr), "taken": "then" if cond else "else", "count": len(actions)}
+
+
+def _iterable_or_error(value: Any, *, name: str) -> Iterable[Any]:
+ if isinstance(value, (list, tuple)):
+ return value
+ raise ValueError(f"{name} must be a list/tuple")
+
+
+def _item_env(item: Any, index: int) -> Dict[str, Any]:
+ env: Dict[str, Any] = {"item": item, "index": index}
+ if isinstance(item, dict):
+ for k, v in item.items():
+ if isinstance(k, str) and k.isidentifier():
+ env[f"item.{k}"] = v
+ return env
+
+
+class ListFilterAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="list_filter",
+ schema={
+ "aliases": {"items": "src", "in": "src", "when": "where", "out": "dst"},
+ "required": ["src", "where"],
+ "optional": {"limit": None, "dst": None},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]) -> List[Any]:
+ src = args.get("src")
+ src_val = ctx.eval_value(src) if hasattr(ctx, "eval_value") else src
+ if isinstance(src, str) and "$" not in src and hasattr(ctx, "vars") and src in ctx.vars:
+ src_val = ctx.vars[src]
+ where = args.get("where")
+ limit = args.get("limit")
+ out: List[Any] = []
+ base = _vars_snapshot(ctx)
+ for idx, item in enumerate(_iterable_or_error(src_val, name="src")):
+ env = dict(base)
+ env.update(_item_env(item, idx))
+ if bool(eval_expr(str(where), env)):
+ out.append(item)
+ if limit is not None and len(out) >= int(limit):
+ break
+ dst = args.get("dst")
+ if dst and hasattr(ctx, "set_var"):
+ ctx.set_var(str(dst), out)
+ return out
+
+
+class ListMapAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="list_map",
+ schema={
+ "aliases": {"items": "src", "in": "src", "map": "expr", "value": "expr", "when": "where", "out": "dst"},
+ "required": ["src", "expr"],
+ "optional": {"where": None, "limit": None, "dst": None},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]) -> List[Any]:
+ src = args.get("src")
+ src_val = ctx.eval_value(src) if hasattr(ctx, "eval_value") else src
+ if isinstance(src, str) and "$" not in src and hasattr(ctx, "vars") and src in ctx.vars:
+ src_val = ctx.vars[src]
+ expr = args.get("expr")
+ where = args.get("where")
+ limit = args.get("limit")
+ out: List[Any] = []
+ base = _vars_snapshot(ctx)
+ for idx, item in enumerate(_iterable_or_error(src_val, name="src")):
+ env = dict(base)
+ env.update(_item_env(item, idx))
+ if where and not bool(eval_expr(str(where), env)):
+ continue
+ out.append(eval_expr(str(expr), env))
+ if limit is not None and len(out) >= int(limit):
+ break
+ dst = args.get("dst")
+ if dst and hasattr(ctx, "set_var"):
+ ctx.set_var(str(dst), out)
+ return out
+
+
+def register_data_actions() -> None:
+ ActionRegistry.register("if", IfAction())
+ ActionRegistry.register("list_filter", ListFilterAction())
+ ActionRegistry.register("list_map", ListMapAction())
diff --git a/actions/protocol_actions.py b/dsl_runtime/actions/dsl_protocol_actions.py
similarity index 50%
rename from actions/protocol_actions.py
rename to dsl_runtime/actions/dsl_protocol_actions.py
index 6f8b579..0e8947c 100644
--- a/actions/protocol_actions.py
+++ b/dsl_runtime/actions/dsl_protocol_actions.py
@@ -2,11 +2,12 @@
from typing import Dict
-from actions.registry import ActionRegistry
-from protocols.registry import ProtocolRegistry
-from protocols import modbus_ascii, modbus_rtu, modbus_tcp # noqa: F401
-from utils.crc16 import crc16_xmodem
-from utils.file_utils import get_file_meta, read_block
+from dsl_runtime.actions.base import DslActionBase
+from dsl_runtime.actions.registry import ActionRegistry
+from infra.protocol.registry import ProtocolRegistry
+from infra.protocol import modbus_ascii, modbus_rtu, modbus_tcp # noqa: F401
+from infra.common.utils.crc16 import crc16_xmodem
+from infra.common.utils.file_utils import get_file_meta, read_block
class XMODEMPacketBuilder:
@@ -25,24 +26,42 @@ def build_eot() -> bytes:
return bytes([0x04])
-def send_xmodem_block(ctx, args: Dict[str, object]):
- meta = get_file_meta(ctx)
- # Cache file metadata so DSL can reference $file.size / $file.block_count in transitions.
- ctx.set_var("file", meta)
- # Also flatten for both dot and underscore style access
- ctx.set_var("file.block_count", meta.get("block_count"))
- ctx.set_var("file.size", meta.get("size"))
- ctx.set_var("file_block_count", meta.get("block_count"))
- ctx.set_var("file_size", meta.get("size"))
- block = int(ctx.eval_value(args.get("block", 1)))
- data = read_block(meta["path"], block, 128)
- packet = XMODEMPacketBuilder.build_block(block, data)
- ctx.channel_write(packet)
- ctx.set_var("last_sent_block", block)
+class SendXmodemBlockAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="send_xmodem_block",
+ schema={
+ "optional": {"block": 1},
+ "types": {"block": "number"},
+ "allow_extra": False,
+ },
+ )
+ def execute(self, ctx, args: Dict[str, object]):
+ meta = get_file_meta(ctx)
+ ctx.set_var("file", meta)
+ ctx.set_var("file.block_count", meta.get("block_count"))
+ ctx.set_var("file.size", meta.get("size"))
+ ctx.set_var("file_block_count", meta.get("block_count"))
+ ctx.set_var("file_size", meta.get("size"))
+ block = int(ctx.eval_value(args.get("block", 1)))
+ data = read_block(meta["path"], block, 128)
+ packet = XMODEMPacketBuilder.build_block(block, data)
+ ctx.channel_write(packet)
+ ctx.set_var("last_sent_block", block)
+
+
+class SendEotAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="send_eot",
+ schema={
+ "allow_extra": False,
+ },
+ )
-def send_eot(ctx, args: Dict[str, object]):
- ctx.channel_write(XMODEMPacketBuilder.build_eot())
+ def execute(self, ctx, args: Dict[str, object]):
+ ctx.channel_write(XMODEMPacketBuilder.build_eot())
_MODBUS_MAP = {
@@ -131,16 +150,58 @@ def _run_modbus(ctx, args: Dict[str, object]):
return result
-def modbus_read(ctx, args: Dict[str, object]):
- return _run_modbus(ctx, args)
+class ModbusReadAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="modbus_read",
+ schema={
+ "required": ["function", "address"],
+ "optional": {
+ "protocol": "rtu",
+ "channel": None,
+ "quantity": None,
+ "values": None,
+ "value": None,
+ "unit_id": 1,
+ "timeout": None,
+ "retries": 3,
+ "save_as": None,
+ },
+ "allow_extra": False,
+ },
+ )
+ def execute(self, ctx, args: Dict[str, object]):
+ return _run_modbus(ctx, args)
+
+
+class ModbusWriteAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="modbus_write",
+ schema={
+ "required": ["function", "address"],
+ "optional": {
+ "protocol": "rtu",
+ "channel": None,
+ "quantity": None,
+ "values": None,
+ "value": None,
+ "unit_id": 1,
+ "timeout": None,
+ "retries": 3,
+ "save_as": None,
+ },
+ "allow_extra": False,
+ },
+ )
-def modbus_write(ctx, args: Dict[str, object]):
- return _run_modbus(ctx, args)
+ def execute(self, ctx, args: Dict[str, object]):
+ return _run_modbus(ctx, args)
def register_protocol_actions():
- ActionRegistry.register("send_xmodem_block", send_xmodem_block)
- ActionRegistry.register("send_eot", send_eot)
- ActionRegistry.register("modbus_read", modbus_read)
- ActionRegistry.register("modbus_write", modbus_write)
+ ActionRegistry.register("send_xmodem_block", SendXmodemBlockAction())
+ ActionRegistry.register("send_eot", SendEotAction())
+ ActionRegistry.register("modbus_read", ModbusReadAction())
+ ActionRegistry.register("modbus_write", ModbusWriteAction())
diff --git a/dsl_runtime/actions/dsl_protocol_schema_actions.py b/dsl_runtime/actions/dsl_protocol_schema_actions.py
new file mode 100644
index 0000000..3d5b10f
--- /dev/null
+++ b/dsl_runtime/actions/dsl_protocol_schema_actions.py
@@ -0,0 +1,82 @@
+from __future__ import annotations
+
+from functools import lru_cache
+from typing import Any, Dict
+
+from dsl_runtime.actions.base import DslActionBase
+from dsl_runtime.actions.registry import ActionRegistry
+from infra.protocol.schema_runtime import ProtocolSchema
+
+
+@lru_cache(maxsize=16)
+def _load_schema(path: str) -> ProtocolSchema:
+ return ProtocolSchema.load(path)
+
+
+class SendFrameAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="send_frame",
+ schema={
+ "required": ["schema", "frame"],
+ "optional": {"values": {}},
+ "types": {"values": "mapping"},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ schema_path = args.get("schema")
+ frame = args.get("frame")
+ values = {k: ctx.eval_value(v) for k, v in (args.get("values") or {}).items()}
+ schema = _load_schema(str(schema_path))
+ packet = schema.build(frame, values)
+ ctx.channel_write(packet)
+ ctx.set_var("last_frame_tx", {"frame": frame, "values": values, "hex": packet.hex().upper()})
+ return packet
+
+
+class ExpectFrameAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="expect_frame",
+ schema={
+ "required": ["schema", "frame"],
+ "optional": {"timeout": 2.0, "save_as": "last_frame_rx"},
+ "types": {"timeout": "number"},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ schema_path = args.get("schema")
+ frame = args.get("frame")
+ timeout = float(args.get("timeout", 2.0))
+ save_as = args.get("save_as", "last_frame_rx")
+
+ schema = _load_schema(str(schema_path))
+ fd = schema.frames.get(frame)
+ if fd is None:
+ raise KeyError(f"unknown frame: {frame}")
+
+ data = b""
+ if fd.tail:
+ data = ctx.channel.read_until(fd.tail, timeout=timeout)
+ else:
+ fixed = fd.fixed_length()
+ if fixed is None:
+ raise ValueError("expect_frame requires tail or fixed frame length")
+ data = ctx.channel.read_exact(fixed, timeout=timeout) # type: ignore[attr-defined]
+
+ if not data:
+ raise TimeoutError("expect_frame timeout")
+
+ parsed = schema.parse(frame, data)
+ ctx.set_var(save_as, parsed)
+ ctx.set_var("last_frame_rx_raw", data.hex().upper())
+ return parsed
+
+
+def register_schema_protocol_actions() -> None:
+ ActionRegistry.register("send_frame", SendFrameAction())
+ ActionRegistry.register("expect_frame", ExpectFrameAction())
diff --git a/dsl_runtime/actions/dsl_record_actions.py b/dsl_runtime/actions/dsl_record_actions.py
new file mode 100644
index 0000000..42cf11d
--- /dev/null
+++ b/dsl_runtime/actions/dsl_record_actions.py
@@ -0,0 +1,100 @@
+from __future__ import annotations
+
+from typing import Any, Dict
+
+from dsl_runtime.actions.base import DslActionBase
+from dsl_runtime.actions.registry import ActionRegistry
+from dsl_runtime.engine.experiment_recorder import ExperimentRecorder
+
+
+def _eval_str(ctx, value: Any) -> str:
+ v = ctx.eval_value(value) if hasattr(ctx, "eval_value") else value
+ return str(v) if v is not None else ""
+
+
+class RecordStartAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="record_start",
+ schema={
+ "optional": {"dir": None, "name": "run", "script_text": None, "script_path": None},
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ if getattr(ctx, "recorder", None) is not None:
+ rec = ctx.recorder
+ return str(getattr(rec, "paths", {}).root) if rec else None
+
+ raw_dir = args.get("dir")
+ base_dir = _eval_str(ctx, raw_dir) if raw_dir is not None else None
+ name = _eval_str(ctx, args.get("name", "run")) or "run"
+ script_text = args.get("script_text")
+ script_path = args.get("script_path")
+
+ if script_text is not None and hasattr(ctx, "eval_value"):
+ script_text = ctx.eval_value(script_text)
+ script_text = str(script_text) if script_text else getattr(ctx, "script_text", None)
+
+ if script_path is not None and hasattr(ctx, "eval_value"):
+ script_path = ctx.eval_value(script_path)
+ script_path = str(script_path) if script_path else getattr(ctx, "script_path", None)
+
+ rec = ExperimentRecorder(base_dir=base_dir, name=name, script_text=script_text, script_path=script_path)
+ root = rec.start()
+
+ if hasattr(ctx, "attach_recorder"):
+ ctx.attach_recorder(rec)
+ else:
+ ctx.recorder = rec
+ if hasattr(ctx, "logger"):
+ try:
+ ctx.logger.info(f"[REC] start -> {root}")
+ except Exception:
+ pass
+ if hasattr(ctx, "set_var"):
+ try:
+ ctx.set_var("record_dir", str(root))
+ except Exception:
+ pass
+ return str(root)
+
+
+class RecordStopAction(DslActionBase):
+ def __init__(self) -> None:
+ super().__init__(
+ name="record_stop",
+ schema={
+ "allow_extra": False,
+ },
+ )
+
+ def execute(self, ctx, args: Dict[str, Any]):
+ rec = getattr(ctx, "recorder", None)
+ if rec is None:
+ return None
+ vars_snapshot = None
+ if hasattr(ctx, "vars_snapshot"):
+ try:
+ vars_snapshot = ctx.vars_snapshot()
+ except Exception:
+ vars_snapshot = None
+ try:
+ rec.close(vars_snapshot=vars_snapshot)
+ finally:
+ if hasattr(ctx, "detach_recorder"):
+ ctx.detach_recorder()
+ else:
+ ctx.recorder = None
+ if hasattr(ctx, "logger"):
+ try:
+ ctx.logger.info(f"[REC] stop -> {rec.paths.root}")
+ except Exception:
+ pass
+ return str(rec.paths.root)
+
+
+def register_record_actions() -> None:
+ ActionRegistry.register("record_start", RecordStartAction())
+ ActionRegistry.register("record_stop", RecordStopAction())
diff --git a/dsl_runtime/actions/registry.py b/dsl_runtime/actions/registry.py
new file mode 100644
index 0000000..0e58ee2
--- /dev/null
+++ b/dsl_runtime/actions/registry.py
@@ -0,0 +1,21 @@
+from __future__ import annotations
+
+from typing import Dict
+
+from dsl_runtime.actions.base import DslActionBase
+
+
+class ActionRegistry:
+ actions: Dict[str, DslActionBase] = {}
+
+ @classmethod
+ def register(cls, name: str, action: DslActionBase) -> None:
+ if not isinstance(action, DslActionBase):
+ raise TypeError("action must be a DslActionBase")
+ cls.actions[name] = action
+
+ @classmethod
+ def get(cls, name: str) -> DslActionBase:
+ if name not in cls.actions:
+ raise KeyError(f"action not registered: {name}")
+ return cls.actions[name]
diff --git a/dsl_runtime/engine/__init__.py b/dsl_runtime/engine/__init__.py
new file mode 100644
index 0000000..0eae68f
--- /dev/null
+++ b/dsl_runtime/engine/__init__.py
@@ -0,0 +1 @@
+# DSL runtime engine package.
diff --git a/runtime/channels.py b/dsl_runtime/engine/channels.py
similarity index 100%
rename from runtime/channels.py
rename to dsl_runtime/engine/channels.py
diff --git a/runtime/chart_runtime.py b/dsl_runtime/engine/chart_runtime.py
similarity index 100%
rename from runtime/chart_runtime.py
rename to dsl_runtime/engine/chart_runtime.py
diff --git a/runtime/context.py b/dsl_runtime/engine/context.py
similarity index 94%
rename from runtime/context.py
rename to dsl_runtime/engine/context.py
index c3ccd3f..70945a3 100644
--- a/runtime/context.py
+++ b/dsl_runtime/engine/context.py
@@ -4,9 +4,9 @@
import queue
from typing import Any, Dict, Optional
-from actions.registry import ActionRegistry
-from dsl.expression import eval_expr
-from runtime.experiment_recorder import ExperimentRecorder, JsonlLogHandler
+from dsl_runtime.actions.registry import ActionRegistry
+from dsl_runtime.lang.expression import eval_expr
+from dsl_runtime.engine.experiment_recorder import ExperimentRecorder, JsonlLogHandler
class RuntimeContext:
@@ -64,7 +64,8 @@ def eval_value(self, value: Any) -> Any:
return value
def run_action(self, name: str, args: Dict[str, Any]) -> Any:
- fn = ActionRegistry.get(name)
+ action = ActionRegistry.get(name)
+ runner = action.run
recorder_before = self._recorder
if recorder_before:
if name == "record_stop":
@@ -72,9 +73,9 @@ def run_action(self, name: str, args: Dict[str, Any]) -> Any:
recorder_before.record_action(name=name, args=args or {}, result={"event": "stop"})
except Exception:
pass
- return fn(self, args or {})
+ return runner(self, args or {})
try:
- result = fn(self, args or {})
+ result = runner(self, args or {})
recorder_before.record_action(name=name, args=args or {}, result=result)
return result
except Exception as exc:
@@ -83,7 +84,7 @@ def run_action(self, name: str, args: Dict[str, Any]) -> Any:
# Allow record_start to be tracked after it attaches a recorder.
try:
- result = fn(self, args or {})
+ result = runner(self, args or {})
except Exception as exc:
recorder_after = self._recorder
if recorder_after:
diff --git a/runtime/experiment_recorder.py b/dsl_runtime/engine/experiment_recorder.py
similarity index 100%
rename from runtime/experiment_recorder.py
rename to dsl_runtime/engine/experiment_recorder.py
diff --git a/runtime/runner.py b/dsl_runtime/engine/runner.py
similarity index 59%
rename from runtime/runner.py
rename to dsl_runtime/engine/runner.py
index 08322e1..53cb945 100644
--- a/runtime/runner.py
+++ b/dsl_runtime/engine/runner.py
@@ -2,16 +2,16 @@
import logging
-from actions.builtin_actions import register_builtin_actions
-from actions.protocol_actions import register_protocol_actions
-from actions.schema_protocol import register_schema_protocol_actions
-from actions.chart_actions import register_chart_actions
-from actions.record_actions import register_record_actions
-from actions.data_actions import register_data_actions
-from dsl.executor import StateMachineExecutor
-from dsl.parser import parse_script
-from runtime.channels import build_channels
-from runtime.context import RuntimeContext
+from dsl_runtime.actions.dsl_builtin_actions import register_builtin_actions
+from dsl_runtime.actions.dsl_protocol_actions import register_protocol_actions
+from dsl_runtime.actions.dsl_protocol_schema_actions import register_schema_protocol_actions
+from dsl_runtime.actions.dsl_chart_actions import register_chart_actions
+from dsl_runtime.actions.dsl_record_actions import register_record_actions
+from dsl_runtime.actions.dsl_data_actions import register_data_actions
+from dsl_runtime.lang.executor import StateMachineExecutor
+from dsl_runtime.lang.parser import parse_script
+from dsl_runtime.engine.channels import build_channels
+from dsl_runtime.engine.context import RuntimeContext
def _register_actions() -> None:
diff --git a/dsl_runtime/lang/__init__.py b/dsl_runtime/lang/__init__.py
new file mode 100644
index 0000000..7f1ebd1
--- /dev/null
+++ b/dsl_runtime/lang/__init__.py
@@ -0,0 +1 @@
+# DSL language package.
diff --git a/dsl/ast_nodes.py b/dsl_runtime/lang/ast_nodes.py
similarity index 100%
rename from dsl/ast_nodes.py
rename to dsl_runtime/lang/ast_nodes.py
diff --git a/dsl/executor.py b/dsl_runtime/lang/executor.py
similarity index 94%
rename from dsl/executor.py
rename to dsl_runtime/lang/executor.py
index 0e41fb0..deaf528 100644
--- a/dsl/executor.py
+++ b/dsl_runtime/lang/executor.py
@@ -3,9 +3,9 @@
import time
from typing import Optional
-from dsl.ast_nodes import ScriptAST, State
-from dsl.expression import eval_expr
-from runtime.context import RuntimeContext
+from dsl_runtime.lang.ast_nodes import ScriptAST, State
+from dsl_runtime.lang.expression import eval_expr
+from dsl_runtime.engine.context import RuntimeContext
class StateMachineExecutor:
diff --git a/dsl/expression.py b/dsl_runtime/lang/expression.py
similarity index 100%
rename from dsl/expression.py
rename to dsl_runtime/lang/expression.py
diff --git a/dsl/parser.py b/dsl_runtime/lang/parser.py
similarity index 99%
rename from dsl/parser.py
rename to dsl_runtime/lang/parser.py
index ced903d..e165894 100644
--- a/dsl/parser.py
+++ b/dsl_runtime/lang/parser.py
@@ -4,7 +4,7 @@
import yaml
-from dsl.ast_nodes import (
+from dsl_runtime.lang.ast_nodes import (
ActionCall,
ChartSpec,
ControlActionSpec,
diff --git a/dsl/state_machine.py b/dsl_runtime/lang/state_machine.py
similarity index 79%
rename from dsl/state_machine.py
rename to dsl_runtime/lang/state_machine.py
index 86716ce..5bc3f89 100644
--- a/dsl/state_machine.py
+++ b/dsl_runtime/lang/state_machine.py
@@ -3,7 +3,7 @@
from dataclasses import dataclass
from typing import Dict
-from dsl.ast_nodes import State
+from dsl_runtime.lang.ast_nodes import State
@dataclass
diff --git a/infra/__init__.py b/infra/__init__.py
new file mode 100644
index 0000000..b5eeb83
--- /dev/null
+++ b/infra/__init__.py
@@ -0,0 +1 @@
+# Infrastructure root package.
diff --git a/infra/comm/__init__.py b/infra/comm/__init__.py
new file mode 100644
index 0000000..a705860
--- /dev/null
+++ b/infra/comm/__init__.py
@@ -0,0 +1 @@
+# Communication infrastructure package.
diff --git a/core/communication_manager.py b/infra/comm/communication_manager.py
similarity index 96%
rename from core/communication_manager.py
rename to infra/comm/communication_manager.py
index 358b399..ffcc9e8 100644
--- a/core/communication_manager.py
+++ b/infra/comm/communication_manager.py
@@ -4,9 +4,9 @@
from typing import Optional, Union
-from core.event_bus import EventBus
-from core.serial_manager import SerialManager
-from core.tcp_session import TcpSession
+from infra.common.event_bus import EventBus
+from infra.comm.serial_manager import SerialManager
+from infra.comm.tcp_session import TcpSession
SessionType = Union[SerialManager, TcpSession]
diff --git a/core/serial_manager.py b/infra/comm/serial_manager.py
similarity index 99%
rename from core/serial_manager.py
rename to infra/comm/serial_manager.py
index 680120c..40de9b9 100644
--- a/core/serial_manager.py
+++ b/infra/comm/serial_manager.py
@@ -10,7 +10,7 @@
from serial import SerialException
from serial.tools import list_ports
-from core.event_bus import EventBus
+from infra.common.event_bus import EventBus
class SerialManager:
diff --git a/core/tcp_session.py b/infra/comm/tcp_session.py
similarity index 98%
rename from core/tcp_session.py
rename to infra/comm/tcp_session.py
index 8bf9a81..478b100 100644
--- a/core/tcp_session.py
+++ b/infra/comm/tcp_session.py
@@ -7,7 +7,7 @@
import time
from typing import Optional
-from core.event_bus import EventBus
+from infra.common.event_bus import EventBus
class TcpSession:
diff --git a/infra/common/__init__.py b/infra/common/__init__.py
new file mode 100644
index 0000000..88d6fbd
--- /dev/null
+++ b/infra/common/__init__.py
@@ -0,0 +1 @@
+# Common infrastructure package.
diff --git a/core/event_bus.py b/infra/common/event_bus.py
similarity index 100%
rename from core/event_bus.py
rename to infra/common/event_bus.py
diff --git a/utils/__init__.py b/infra/common/utils/__init__.py
similarity index 100%
rename from utils/__init__.py
rename to infra/common/utils/__init__.py
diff --git a/utils/crc16.py b/infra/common/utils/crc16.py
similarity index 100%
rename from utils/crc16.py
rename to infra/common/utils/crc16.py
diff --git a/utils/file_utils.py b/infra/common/utils/file_utils.py
similarity index 100%
rename from utils/file_utils.py
rename to infra/common/utils/file_utils.py
diff --git a/utils/lrc.py b/infra/common/utils/lrc.py
similarity index 100%
rename from utils/lrc.py
rename to infra/common/utils/lrc.py
diff --git a/utils/path_utils.py b/infra/common/utils/path_utils.py
similarity index 100%
rename from utils/path_utils.py
rename to infra/common/utils/path_utils.py
diff --git a/infra/protocol/__init__.py b/infra/protocol/__init__.py
new file mode 100644
index 0000000..151b9e5
--- /dev/null
+++ b/infra/protocol/__init__.py
@@ -0,0 +1,4 @@
+from infra.protocol.registry import ProtocolRegistry # noqa: F401
+from infra.protocol.base import BaseProtocol # noqa: F401
+
+# 具体协议实现会在各自模块内完成注册
diff --git a/protocols/at.py b/infra/protocol/at.py
similarity index 95%
rename from protocols/at.py
rename to infra/protocol/at.py
index 18ef924..9d36d1a 100644
--- a/protocols/at.py
+++ b/infra/protocol/at.py
@@ -3,8 +3,8 @@
import time
from typing import List
-from protocols.base import BaseProtocol
-from protocols.registry import ProtocolRegistry
+from infra.protocol.base import BaseProtocol
+from infra.protocol.registry import ProtocolRegistry
class ATProtocol(BaseProtocol):
diff --git a/protocols/base.py b/infra/protocol/base.py
similarity index 100%
rename from protocols/base.py
rename to infra/protocol/base.py
diff --git a/protocols/modbus_ascii.py b/infra/protocol/modbus_ascii.py
similarity index 94%
rename from protocols/modbus_ascii.py
rename to infra/protocol/modbus_ascii.py
index 91c53d0..bc93d67 100644
--- a/protocols/modbus_ascii.py
+++ b/infra/protocol/modbus_ascii.py
@@ -2,9 +2,9 @@
import time
-from protocols.modbus_base import ModbusBase
-from protocols.registry import ProtocolRegistry
-from utils.lrc import lrc_modbus_ascii
+from infra.protocol.modbus_base import ModbusBase
+from infra.protocol.registry import ProtocolRegistry
+from infra.common.utils.lrc import lrc_modbus_ascii
class ModbusASCII(ModbusBase):
diff --git a/protocols/modbus_base.py b/infra/protocol/modbus_base.py
similarity index 99%
rename from protocols/modbus_base.py
rename to infra/protocol/modbus_base.py
index a9a2d69..1351f6a 100644
--- a/protocols/modbus_base.py
+++ b/infra/protocol/modbus_base.py
@@ -2,7 +2,7 @@
from typing import Iterable, List, Sequence
-from protocols.base import BaseProtocol
+from infra.protocol.base import BaseProtocol
class ModbusBase(BaseProtocol):
diff --git a/protocols/modbus_rtu.py b/infra/protocol/modbus_rtu.py
similarity index 95%
rename from protocols/modbus_rtu.py
rename to infra/protocol/modbus_rtu.py
index 781eb9e..711ddf2 100644
--- a/protocols/modbus_rtu.py
+++ b/infra/protocol/modbus_rtu.py
@@ -3,9 +3,9 @@
import time
from typing import Optional
-from protocols.modbus_base import ModbusBase
-from protocols.registry import ProtocolRegistry
-from utils.crc16 import crc16_modbus
+from infra.protocol.modbus_base import ModbusBase
+from infra.protocol.registry import ProtocolRegistry
+from infra.common.utils.crc16 import crc16_modbus
class ModbusRTU(ModbusBase):
diff --git a/protocols/modbus_tcp.py b/infra/protocol/modbus_tcp.py
similarity index 95%
rename from protocols/modbus_tcp.py
rename to infra/protocol/modbus_tcp.py
index 467023d..ca8037e 100644
--- a/protocols/modbus_tcp.py
+++ b/infra/protocol/modbus_tcp.py
@@ -3,8 +3,8 @@
import itertools
import time
-from protocols.modbus_base import ModbusBase
-from protocols.registry import ProtocolRegistry
+from infra.protocol.modbus_base import ModbusBase
+from infra.protocol.registry import ProtocolRegistry
class ModbusTCP(ModbusBase):
diff --git a/core/protocol_loader.py b/infra/protocol/protocol_loader.py
similarity index 98%
rename from core/protocol_loader.py
rename to infra/protocol/protocol_loader.py
index 2d69b6e..dcac738 100644
--- a/core/protocol_loader.py
+++ b/infra/protocol/protocol_loader.py
@@ -13,8 +13,8 @@
import yaml
-from core.event_bus import EventBus
-from utils.path_utils import resolve_resource_path
+from infra.common.event_bus import EventBus
+from infra.common.utils.path_utils import resolve_resource_path
def crc16_modbus(data: bytes) -> int:
diff --git a/protocols/registry.py b/infra/protocol/registry.py
similarity index 93%
rename from protocols/registry.py
rename to infra/protocol/registry.py
index 4918251..157536d 100644
--- a/protocols/registry.py
+++ b/infra/protocol/registry.py
@@ -2,7 +2,7 @@
from typing import Dict, Type
-from protocols.base import BaseProtocol
+from infra.protocol.base import BaseProtocol
class ProtocolRegistry:
diff --git a/protocols/schema_runtime.py b/infra/protocol/schema_runtime.py
similarity index 98%
rename from protocols/schema_runtime.py
rename to infra/protocol/schema_runtime.py
index 4c42d07..f2d4ae2 100644
--- a/protocols/schema_runtime.py
+++ b/infra/protocol/schema_runtime.py
@@ -7,8 +7,8 @@
import yaml
-from utils.crc16 import crc16_modbus
-from utils.path_utils import resolve_resource_path
+from infra.common.utils.crc16 import crc16_modbus
+from infra.common.utils.path_utils import resolve_resource_path
def _hex_to_bytes(value: str | bytes | None) -> bytes:
diff --git a/protocols/scpi.py b/infra/protocol/scpi.py
similarity index 97%
rename from protocols/scpi.py
rename to infra/protocol/scpi.py
index 7754e2d..ed98d35 100644
--- a/protocols/scpi.py
+++ b/infra/protocol/scpi.py
@@ -3,8 +3,8 @@
import time
from typing import Any, Dict
-from protocols.base import BaseProtocol
-from protocols.registry import ProtocolRegistry
+from infra.protocol.base import BaseProtocol
+from infra.protocol.registry import ProtocolRegistry
class SCPIProtocol(BaseProtocol):
diff --git a/protocols/xmodem.py b/infra/protocol/xmodem.py
similarity index 95%
rename from protocols/xmodem.py
rename to infra/protocol/xmodem.py
index f72867e..ee57c7b 100644
--- a/protocols/xmodem.py
+++ b/infra/protocol/xmodem.py
@@ -3,9 +3,9 @@
import time
from pathlib import Path
-from protocols.base import BaseProtocol
-from protocols.registry import ProtocolRegistry
-from utils.crc16 import crc16_xmodem
+from infra.protocol.base import BaseProtocol
+from infra.protocol.registry import ProtocolRegistry
+from infra.common.utils.crc16 import crc16_xmodem
SOH = 0x01
diff --git a/protocols/ymodem.py b/infra/protocol/ymodem.py
similarity index 95%
rename from protocols/ymodem.py
rename to infra/protocol/ymodem.py
index e9123be..0a3ff44 100644
--- a/protocols/ymodem.py
+++ b/infra/protocol/ymodem.py
@@ -3,9 +3,9 @@
import time
from pathlib import Path
-from protocols.base import BaseProtocol
-from protocols.registry import ProtocolRegistry
-from utils.crc16 import crc16_xmodem
+from infra.protocol.base import BaseProtocol
+from infra.protocol.registry import ProtocolRegistry
+from infra.common.utils.crc16 import crc16_xmodem
SOH = 0x01
diff --git a/main.py b/main.py
index 6fd836c..f653d1c 100644
--- a/main.py
+++ b/main.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from main_web import main
+from app.main_web import main
if __name__ == "__main__":
diff --git a/main_runtime.py b/main_runtime.py
deleted file mode 100644
index 1378ed8..0000000
--- a/main_runtime.py
+++ /dev/null
@@ -1,190 +0,0 @@
-from __future__ import annotations
-
-import argparse
-import logging
-import socket
-import sys
-import time
-from typing import Any, Dict
-
-import yaml
-
-from actions import at_command, modbus_request, scpi_command, xmodem_send, ymodem_send
-from protocols import at, modbus_ascii, modbus_rtu, modbus_tcp, scpi, xmodem, ymodem # noqa: F401 触发注册
-from utils.path_utils import resolve_resource_path
-
-try:
- import serial
-except ImportError: # pragma: no cover
- serial = None
-
-
-class BaseChannel:
- def write(self, data: bytes | str) -> None: # pragma: no cover - 接口
- raise NotImplementedError()
-
- def read(self, size: int = 1, timeout: float = 1.0) -> bytes: # pragma: no cover - 接口
- raise NotImplementedError()
-
- def read_until(self, terminator: bytes, timeout: float = 1.0) -> bytes:
- buf = bytearray()
- deadline = time.time() + timeout
- while time.time() < deadline:
- chunk = self.read(1, timeout=max(0.01, deadline - time.time()))
- if chunk:
- buf.extend(chunk)
- if buf.endswith(terminator):
- break
- else:
- time.sleep(0.01)
- return bytes(buf)
-
- def close(self) -> None: # pragma: no cover - 接口
- pass
-
-
-class SerialChannel(BaseChannel):
- def __init__(self, port: str, baudrate: int = 115200, timeout: float = 1.0) -> None:
- if serial is None:
- raise ImportError("pyserial 未安装,无法创建串口通道")
- self.ser = serial.Serial(port=port, baudrate=baudrate, timeout=0)
- self._timeout = timeout
-
- def write(self, data: bytes | str) -> None:
- payload = data.encode() if isinstance(data, str) else data
- self.ser.write(payload)
-
- def read(self, size: int = 1, timeout: float = 1.0) -> bytes:
- deadline = time.time() + timeout
- buf = bytearray()
- while len(buf) < size and time.time() < deadline:
- chunk = self.ser.read(size - len(buf))
- if chunk:
- buf.extend(chunk)
- else:
- time.sleep(0.01)
- return bytes(buf)
-
- def close(self) -> None:
- try:
- self.ser.close()
- except Exception:
- pass
-
-
-class TcpChannel(BaseChannel):
- def __init__(self, host: str, port: int, timeout: float = 2.0) -> None:
- self.sock = socket.create_connection((host, port), timeout=timeout)
- self.sock.settimeout(0.5)
-
- def write(self, data: bytes | str) -> None:
- payload = data.encode() if isinstance(data, str) else data
- self.sock.sendall(payload)
-
- def read(self, size: int = 1, timeout: float = 1.0) -> bytes:
- deadline = time.time() + timeout
- buf = bytearray()
- while len(buf) < size and time.time() < deadline:
- try:
- chunk = self.sock.recv(size - len(buf))
- if chunk:
- buf.extend(chunk)
- else:
- time.sleep(0.01)
- except socket.timeout:
- continue
- return bytes(buf)
-
- def close(self) -> None:
- try:
- self.sock.close()
- except Exception:
- pass
-
-
-ACTIONS = {
- "at_command": at_command.run,
- "modbus_request": modbus_request.run,
- "scpi_command": scpi_command.run,
- "xmodem_send": xmodem_send.run,
- "ymodem_send": ymodem_send.run,
-}
-
-
-def load_yaml(path: Path) -> Dict[str, Any]:
- with path.open("r", encoding="utf-8") as f:
- return yaml.safe_load(f) or {}
-
-
-def build_channels(config: Dict[str, Any]) -> Dict[str, BaseChannel]:
- channels_cfg = config.get("channels") or config.get("app", {}).get("channels", {})
- channels: Dict[str, BaseChannel] = {}
- for name, cfg in channels_cfg.items():
- ctype = str(cfg.get("type", "serial")).lower()
- if ctype == "serial":
- channels[name] = SerialChannel(
- port=cfg["port"],
- baudrate=int(cfg.get("baudrate", 115200)),
- timeout=float(cfg.get("timeout", 1.0)),
- )
- elif ctype == "tcp":
- channels[name] = TcpChannel(
- host=cfg["host"],
- port=int(cfg["port"]),
- timeout=float(cfg.get("timeout", 2.0)),
- )
- else:
- raise ValueError(f"不支持的通道类型: {ctype}")
- return channels
-
-
-def run_tasks(tasks: list[Dict[str, Any]], channels: Dict[str, BaseChannel], logger: logging.Logger) -> None:
- for idx, task in enumerate(tasks):
- action = task.get("action")
- runner = ACTIONS.get(action)
- if not runner:
- raise ValueError(f"未知 action: {action}")
- logger.info(f"执行任务 {idx + 1}/{len(tasks)}: {action}")
- result = runner(task, channels, logger)
- logger.info(f"任务完成 {action}: {result}")
-
-
-def main(argv: list[str] | None = None) -> int:
- parser = argparse.ArgumentParser(description="ProtoFlow 通信运行时")
- parser.add_argument(
- "-c", "--config", default="config/app.yaml", help="任务 YAML 路径,默认 config/app.yaml"
- )
- args = parser.parse_args(argv)
-
- logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
- logger = logging.getLogger("runtime")
-
- config_path = resolve_resource_path(args.config)
- if not config_path.exists():
- logger.error(f"配置不存在: {config_path}")
- return 2
-
- config = load_yaml(config_path)
- tasks = config.get("tasks") or config.get("app", {}).get("tasks", [])
- if not tasks:
- logger.warning("未在 YAML 中找到 tasks,退出")
- return 0
-
- channels: Dict[str, BaseChannel] = {}
- try:
- channels = build_channels(config)
- run_tasks(tasks, channels, logger)
- return 0
- except Exception as exc:
- logger.exception("运行时异常: %s", exc)
- return 1
- finally:
- for ch in channels.values():
- try:
- ch.close()
- except Exception:
- pass
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/protocols/__init__.py b/protocols/__init__.py
deleted file mode 100644
index 6784aa7..0000000
--- a/protocols/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from protocols.registry import ProtocolRegistry # noqa: F401
-from protocols.base import BaseProtocol # noqa: F401
-
-# 具体协议实现会在各自模块内完成注册
diff --git a/scripts/build_windows.ps1 b/scripts/build_windows.ps1
index 61f8a5e..6f857c4 100644
--- a/scripts/build_windows.ps1
+++ b/scripts/build_windows.ps1
@@ -6,7 +6,7 @@ param(
$ErrorActionPreference = "Stop"
Write-Host "==> Build web UI"
-Push-Location "frontend"
+Push-Location "ui\\frontend"
& $Node ci
& $Node run build
Pop-Location
@@ -17,13 +17,13 @@ Write-Host "==> Install Python deps"
Write-Host "==> Build app (PyInstaller)"
& $Python -m PyInstaller --name ProtoFlow --windowed --onedir --noconfirm --icon "installer\ProtoFlow.ico" `
- --add-data "frontend\dist;frontend\dist" `
+ --add-data "ui\\frontend\\dist;ui\\frontend\\dist" `
--add-data "config;config" `
--add-data "plugins;plugins" `
- --add-data "assets;assets" `
+ --add-data "ui\\assets;ui\\assets" `
main.py
Write-Host "==> Generate installer icon"
if (-not (Test-Path -LiteralPath "installer\\ProtoFlow.ico")) {
- & powershell -File scripts\generate_icon.ps1 -Source assets\\icons\\logo.png -Output installer\\ProtoFlow.ico -Size 256
+ & powershell -File scripts\generate_icon.ps1 -Source ui\\assets\\icons\\logo.png -Output installer\\ProtoFlow.ico -Size 256
}
diff --git a/scripts/generate_icon.ps1 b/scripts/generate_icon.ps1
index 779721e..bcfc514 100644
--- a/scripts/generate_icon.ps1
+++ b/scripts/generate_icon.ps1
@@ -1,5 +1,5 @@
param(
- [string]$Source = "assets/icons/logo.png",
+ [string]$Source = "ui/assets/icons/logo.png",
[string]$Output = "installer/ProtoFlow.ico",
[int]$Size = 256
)
diff --git a/src/com_tool.py b/src/com_tool.py
deleted file mode 100644
index 908aa89..0000000
--- a/src/com_tool.py
+++ /dev/null
@@ -1,92 +0,0 @@
-"""Simple COM helper script for quick experiments.
-
-Usage example:
- python src/com_tool.py --prog-id "Your.ProgID" --method "Ping" --args "hello" "world"
-"""
-
-from __future__ import annotations
-
-import argparse
-import os
-import sys
-from typing import Any, Sequence
-
-import pythoncom
-import win32com.client
-from dotenv import load_dotenv
-
-
-def parse_args(argv: Sequence[str]) -> argparse.Namespace:
- parser = argparse.ArgumentParser(description="Call a method on a COM component.")
- parser.add_argument(
- "--prog-id",
- dest="prog_id",
- default=os.getenv("COM_PROG_ID"),
- help="ProgID or CLSID of the COM component (env: COM_PROG_ID).",
- )
- parser.add_argument(
- "--method",
- required=False,
- help="Method name to invoke on the COM object.",
- )
- parser.add_argument(
- "--args",
- nargs="*",
- default=[],
- help="Positional arguments passed to the COM method.",
- )
- parser.add_argument(
- "--visible",
- action="store_true",
- default=os.getenv("COM_VISIBLE") in {"1", "true", "True"},
- help="If the COM object supports a Visible property, set it to True.",
- )
- return parser.parse_args(argv)
-
-
-def create_com_instance(prog_id: str):
- # CoInitialize/CoUninitialize must be paired; caller handles teardown.
- return win32com.client.Dispatch(prog_id)
-
-
-def invoke_method(target: Any, method: str | None, args: Sequence[str]) -> Any:
- if not method:
- return None
- if not hasattr(target, method):
- raise AttributeError(f"COM object has no method '{method}'")
- callable_attr = getattr(target, method)
- return callable_attr(*args)
-
-
-def main(argv: Sequence[str] | None = None) -> int:
- load_dotenv()
- args = parse_args(argv or sys.argv[1:])
-
- if not args.prog_id:
- print("Error: --prog-id is required (or set COM_PROG_ID in .env).", file=sys.stderr)
- return 2
-
- pythoncom.CoInitialize()
- try:
- com_obj = create_com_instance(args.prog_id)
- if args.visible and hasattr(com_obj, "Visible"):
- try:
- com_obj.Visible = True
- except Exception:
- pass # Some COM servers expose Visible but reject writes.
-
- result = invoke_method(com_obj, args.method, args.args)
- if args.method:
- print(f"[OK] {args.prog_id}.{args.method} -> {result!r}")
- else:
- print(f"[OK] Connected to {args.prog_id}, no method invoked.")
- return 0
- except Exception as exc:
- print(f"[ERROR] {exc}", file=sys.stderr)
- return 1
- finally:
- pythoncom.CoUninitialize()
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/ui/__init__.py b/ui/__init__.py
new file mode 100644
index 0000000..e6a7566
--- /dev/null
+++ b/ui/__init__.py
@@ -0,0 +1 @@
+# UI package.
diff --git a/assets/icons/logo.png b/ui/assets/icons/logo.png
similarity index 100%
rename from assets/icons/logo.png
rename to ui/assets/icons/logo.png
diff --git a/assets/icons/logo.svg b/ui/assets/icons/logo.svg
similarity index 100%
rename from assets/icons/logo.svg
rename to ui/assets/icons/logo.svg
diff --git a/assets/web/index.html b/ui/assets/web/index.html
similarity index 100%
rename from assets/web/index.html
rename to ui/assets/web/index.html
diff --git a/ui/desktop/__init__.py b/ui/desktop/__init__.py
new file mode 100644
index 0000000..992022e
--- /dev/null
+++ b/ui/desktop/__init__.py
@@ -0,0 +1 @@
+# Desktop UI package.
diff --git a/desktop/script_runner_qt.py b/ui/desktop/script_runner_qt.py
similarity index 89%
rename from desktop/script_runner_qt.py
rename to ui/desktop/script_runner_qt.py
index 08d7bd6..cb08b42 100644
--- a/desktop/script_runner_qt.py
+++ b/ui/desktop/script_runner_qt.py
@@ -8,16 +8,16 @@
from PySide6.QtCore import QThread, Signal
-from actions.builtin_actions import register_builtin_actions
-from actions.protocol_actions import register_protocol_actions
-from actions.schema_protocol import register_schema_protocol_actions
-from actions.chart_actions import register_chart_actions
-from actions.record_actions import register_record_actions
-from actions.data_actions import register_data_actions
-from dsl.executor import StateMachineExecutor
-from dsl.parser import parse_script
-from runtime.channels import build_channels
-from runtime.context import RuntimeContext
+from dsl_runtime.actions.dsl_builtin_actions import register_builtin_actions
+from dsl_runtime.actions.dsl_protocol_actions import register_protocol_actions
+from dsl_runtime.actions.dsl_protocol_schema_actions import register_schema_protocol_actions
+from dsl_runtime.actions.dsl_chart_actions import register_chart_actions
+from dsl_runtime.actions.dsl_record_actions import register_record_actions
+from dsl_runtime.actions.dsl_data_actions import register_data_actions
+from dsl_runtime.lang.executor import StateMachineExecutor
+from dsl_runtime.lang.parser import parse_script
+from dsl_runtime.engine.channels import build_channels
+from dsl_runtime.engine.context import RuntimeContext
class _LogHandler(logging.Handler):
diff --git a/desktop/web_bridge.py b/ui/desktop/web_bridge.py
similarity index 99%
rename from desktop/web_bridge.py
rename to ui/desktop/web_bridge.py
index 7ffce9d..f32cbf2 100644
--- a/desktop/web_bridge.py
+++ b/ui/desktop/web_bridge.py
@@ -19,9 +19,9 @@
from PyQt6.QtCore import QObject, Q_ARG, QMetaObject, QTimer, Qt, pyqtSignal as Signal, pyqtSlot as Slot # type: ignore
from PyQt6.QtWidgets import QFileDialog # type: ignore
-from protocols.registry import ProtocolRegistry
-import protocols as protocols_pkg
-from desktop.script_runner_qt import ScriptRunnerQt
+from infra.protocol.registry import ProtocolRegistry
+import infra.protocol as protocols_pkg
+from ui.desktop.script_runner_qt import ScriptRunnerQt
class WebBridge(QObject):
diff --git a/desktop/web_window.py b/ui/desktop/web_window.py
similarity index 99%
rename from desktop/web_window.py
rename to ui/desktop/web_window.py
index 0411157..e31a618 100644
--- a/desktop/web_window.py
+++ b/ui/desktop/web_window.py
@@ -20,8 +20,8 @@
from PyQt6.QtWidgets import QFileDialog, QMainWindow, QMenu # type: ignore
from PyQt6.QtWebEngineWidgets import QWebEngineView # type: ignore
-from desktop.web_bridge import WebBridge
-from desktop.win_snap import apply_snap_styles
+from ui.desktop.web_bridge import WebBridge
+from ui.desktop.win_snap import apply_snap_styles
class LoggingWebPage(QWebEnginePage):
def javaScriptConsoleMessage(self, level, message, line_number, source_id): # type: ignore[override]
diff --git a/desktop/widgets/__init__.py b/ui/desktop/widgets/__init__.py
similarity index 100%
rename from desktop/widgets/__init__.py
rename to ui/desktop/widgets/__init__.py
diff --git a/desktop/win_snap.py b/ui/desktop/win_snap.py
similarity index 100%
rename from desktop/win_snap.py
rename to ui/desktop/win_snap.py
diff --git a/frontend/.gitignore b/ui/frontend/.gitignore
similarity index 100%
rename from frontend/.gitignore
rename to ui/frontend/.gitignore
diff --git a/frontend/.vscode/extensions.json b/ui/frontend/.vscode/extensions.json
similarity index 100%
rename from frontend/.vscode/extensions.json
rename to ui/frontend/.vscode/extensions.json
diff --git a/frontend/README.md b/ui/frontend/README.md
similarity index 100%
rename from frontend/README.md
rename to ui/frontend/README.md
diff --git a/frontend/config/ui_settings.json b/ui/frontend/config/ui_settings.json
similarity index 78%
rename from frontend/config/ui_settings.json
rename to ui/frontend/config/ui_settings.json
index c977230..ed7bddc 100644
--- a/frontend/config/ui_settings.json
+++ b/ui/frontend/config/ui_settings.json
@@ -1,6 +1,6 @@
{
"autoConnectOnStart": false,
- "dslWorkspacePath": "D:\\GitRepository\\ProtoFlow\\frontend\\workflows",
+ "dslWorkspacePath": "D:\\GitRepository\\ProtoFlow\\ui\\frontend\\workflows",
"network": {
"tcpHeartbeatSec": 60,
"tcpRetryCount": 3,
@@ -13,4 +13,4 @@
},
"uiLanguage": "English (US)",
"uiTheme": "绯荤粺榛樿"
-}
\ No newline at end of file
+}
diff --git a/frontend/index.html b/ui/frontend/index.html
similarity index 100%
rename from frontend/index.html
rename to ui/frontend/index.html
diff --git a/frontend/package-lock.json b/ui/frontend/package-lock.json
similarity index 100%
rename from frontend/package-lock.json
rename to ui/frontend/package-lock.json
diff --git a/frontend/package.json b/ui/frontend/package.json
similarity index 100%
rename from frontend/package.json
rename to ui/frontend/package.json
diff --git a/frontend/postcss.config.js b/ui/frontend/postcss.config.js
similarity index 100%
rename from frontend/postcss.config.js
rename to ui/frontend/postcss.config.js
diff --git a/frontend/public/vite.svg b/ui/frontend/public/vite.svg
similarity index 100%
rename from frontend/public/vite.svg
rename to ui/frontend/public/vite.svg
diff --git a/frontend/src/App.vue b/ui/frontend/src/App.vue
similarity index 100%
rename from frontend/src/App.vue
rename to ui/frontend/src/App.vue
diff --git a/frontend/src/assets/fonts/MaterialSymbolsOutlined.ttf b/ui/frontend/src/assets/fonts/MaterialSymbolsOutlined.ttf
similarity index 100%
rename from frontend/src/assets/fonts/MaterialSymbolsOutlined.ttf
rename to ui/frontend/src/assets/fonts/MaterialSymbolsOutlined.ttf
diff --git a/frontend/src/assets/vue.svg b/ui/frontend/src/assets/vue.svg
similarity index 100%
rename from frontend/src/assets/vue.svg
rename to ui/frontend/src/assets/vue.svg
diff --git a/frontend/src/components/DropdownSelect.vue b/ui/frontend/src/components/DropdownSelect.vue
similarity index 100%
rename from frontend/src/components/DropdownSelect.vue
rename to ui/frontend/src/components/DropdownSelect.vue
diff --git a/frontend/src/components/HelloWorld.vue b/ui/frontend/src/components/HelloWorld.vue
similarity index 100%
rename from frontend/src/components/HelloWorld.vue
rename to ui/frontend/src/components/HelloWorld.vue
diff --git a/frontend/src/components/LogStream.vue b/ui/frontend/src/components/LogStream.vue
similarity index 100%
rename from frontend/src/components/LogStream.vue
rename to ui/frontend/src/components/LogStream.vue
diff --git a/frontend/src/components/ManualView.vue b/ui/frontend/src/components/ManualView.vue
similarity index 100%
rename from frontend/src/components/ManualView.vue
rename to ui/frontend/src/components/ManualView.vue
diff --git a/frontend/src/components/ProxyMonitorView.vue b/ui/frontend/src/components/ProxyMonitorView.vue
similarity index 100%
rename from frontend/src/components/ProxyMonitorView.vue
rename to ui/frontend/src/components/ProxyMonitorView.vue
diff --git a/frontend/src/components/ScriptsView.vue b/ui/frontend/src/components/ScriptsView.vue
similarity index 100%
rename from frontend/src/components/ScriptsView.vue
rename to ui/frontend/src/components/ScriptsView.vue
diff --git a/frontend/src/components/YamlUiLab.vue b/ui/frontend/src/components/YamlUiLab.vue
similarity index 100%
rename from frontend/src/components/YamlUiLab.vue
rename to ui/frontend/src/components/YamlUiLab.vue
diff --git a/frontend/src/components/ui-kit/AppShell.vue b/ui/frontend/src/components/ui-kit/AppShell.vue
similarity index 100%
rename from frontend/src/components/ui-kit/AppShell.vue
rename to ui/frontend/src/components/ui-kit/AppShell.vue
diff --git a/frontend/src/components/ui-kit/EventLogPanel.vue b/ui/frontend/src/components/ui-kit/EventLogPanel.vue
similarity index 100%
rename from frontend/src/components/ui-kit/EventLogPanel.vue
rename to ui/frontend/src/components/ui-kit/EventLogPanel.vue
diff --git a/frontend/src/components/ui-kit/InspectorJSON.vue b/ui/frontend/src/components/ui-kit/InspectorJSON.vue
similarity index 100%
rename from frontend/src/components/ui-kit/InspectorJSON.vue
rename to ui/frontend/src/components/ui-kit/InspectorJSON.vue
diff --git a/frontend/src/components/ui-kit/PanelCard.vue b/ui/frontend/src/components/ui-kit/PanelCard.vue
similarity index 100%
rename from frontend/src/components/ui-kit/PanelCard.vue
rename to ui/frontend/src/components/ui-kit/PanelCard.vue
diff --git a/frontend/src/components/ui-kit/SplitContainer.vue b/ui/frontend/src/components/ui-kit/SplitContainer.vue
similarity index 100%
rename from frontend/src/components/ui-kit/SplitContainer.vue
rename to ui/frontend/src/components/ui-kit/SplitContainer.vue
diff --git a/frontend/src/components/ui-kit/TopBar.vue b/ui/frontend/src/components/ui-kit/TopBar.vue
similarity index 100%
rename from frontend/src/components/ui-kit/TopBar.vue
rename to ui/frontend/src/components/ui-kit/TopBar.vue
diff --git a/frontend/src/components/ui-kit/actions/BatchActionGroup.vue b/ui/frontend/src/components/ui-kit/actions/BatchActionGroup.vue
similarity index 100%
rename from frontend/src/components/ui-kit/actions/BatchActionGroup.vue
rename to ui/frontend/src/components/ui-kit/actions/BatchActionGroup.vue
diff --git a/frontend/src/components/ui-kit/actions/ConfirmDangerButton.vue b/ui/frontend/src/components/ui-kit/actions/ConfirmDangerButton.vue
similarity index 100%
rename from frontend/src/components/ui-kit/actions/ConfirmDangerButton.vue
rename to ui/frontend/src/components/ui-kit/actions/ConfirmDangerButton.vue
diff --git a/frontend/src/components/ui-kit/actions/DropdownAction.vue b/ui/frontend/src/components/ui-kit/actions/DropdownAction.vue
similarity index 100%
rename from frontend/src/components/ui-kit/actions/DropdownAction.vue
rename to ui/frontend/src/components/ui-kit/actions/DropdownAction.vue
diff --git a/frontend/src/components/ui-kit/actions/GhostButton.vue b/ui/frontend/src/components/ui-kit/actions/GhostButton.vue
similarity index 100%
rename from frontend/src/components/ui-kit/actions/GhostButton.vue
rename to ui/frontend/src/components/ui-kit/actions/GhostButton.vue
diff --git a/frontend/src/components/ui-kit/actions/PrimaryButton.vue b/ui/frontend/src/components/ui-kit/actions/PrimaryButton.vue
similarity index 100%
rename from frontend/src/components/ui-kit/actions/PrimaryButton.vue
rename to ui/frontend/src/components/ui-kit/actions/PrimaryButton.vue
diff --git a/frontend/src/components/ui-kit/actions/SecondaryButton.vue b/ui/frontend/src/components/ui-kit/actions/SecondaryButton.vue
similarity index 100%
rename from frontend/src/components/ui-kit/actions/SecondaryButton.vue
rename to ui/frontend/src/components/ui-kit/actions/SecondaryButton.vue
diff --git a/frontend/src/components/ui-kit/inputs/CheckboxGroup.vue b/ui/frontend/src/components/ui-kit/inputs/CheckboxGroup.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/CheckboxGroup.vue
rename to ui/frontend/src/components/ui-kit/inputs/CheckboxGroup.vue
diff --git a/frontend/src/components/ui-kit/inputs/ColorInput.vue b/ui/frontend/src/components/ui-kit/inputs/ColorInput.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/ColorInput.vue
rename to ui/frontend/src/components/ui-kit/inputs/ColorInput.vue
diff --git a/frontend/src/components/ui-kit/inputs/DateTimeInput.vue b/ui/frontend/src/components/ui-kit/inputs/DateTimeInput.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/DateTimeInput.vue
rename to ui/frontend/src/components/ui-kit/inputs/DateTimeInput.vue
diff --git a/frontend/src/components/ui-kit/inputs/KeybindRecorder.vue b/ui/frontend/src/components/ui-kit/inputs/KeybindRecorder.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/KeybindRecorder.vue
rename to ui/frontend/src/components/ui-kit/inputs/KeybindRecorder.vue
diff --git a/frontend/src/components/ui-kit/inputs/MultiSelectChips.vue b/ui/frontend/src/components/ui-kit/inputs/MultiSelectChips.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/MultiSelectChips.vue
rename to ui/frontend/src/components/ui-kit/inputs/MultiSelectChips.vue
diff --git a/frontend/src/components/ui-kit/inputs/NumberInput.vue b/ui/frontend/src/components/ui-kit/inputs/NumberInput.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/NumberInput.vue
rename to ui/frontend/src/components/ui-kit/inputs/NumberInput.vue
diff --git a/frontend/src/components/ui-kit/inputs/PathPicker.vue b/ui/frontend/src/components/ui-kit/inputs/PathPicker.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/PathPicker.vue
rename to ui/frontend/src/components/ui-kit/inputs/PathPicker.vue
diff --git a/frontend/src/components/ui-kit/inputs/RadioGroup.vue b/ui/frontend/src/components/ui-kit/inputs/RadioGroup.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/RadioGroup.vue
rename to ui/frontend/src/components/ui-kit/inputs/RadioGroup.vue
diff --git a/frontend/src/components/ui-kit/inputs/SelectInput.vue b/ui/frontend/src/components/ui-kit/inputs/SelectInput.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/SelectInput.vue
rename to ui/frontend/src/components/ui-kit/inputs/SelectInput.vue
diff --git a/frontend/src/components/ui-kit/inputs/SliderInput.vue b/ui/frontend/src/components/ui-kit/inputs/SliderInput.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/SliderInput.vue
rename to ui/frontend/src/components/ui-kit/inputs/SliderInput.vue
diff --git a/frontend/src/components/ui-kit/inputs/SwitchInput.vue b/ui/frontend/src/components/ui-kit/inputs/SwitchInput.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/SwitchInput.vue
rename to ui/frontend/src/components/ui-kit/inputs/SwitchInput.vue
diff --git a/frontend/src/components/ui-kit/inputs/TextInput.vue b/ui/frontend/src/components/ui-kit/inputs/TextInput.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/TextInput.vue
rename to ui/frontend/src/components/ui-kit/inputs/TextInput.vue
diff --git a/frontend/src/components/ui-kit/inputs/TextareaInput.vue b/ui/frontend/src/components/ui-kit/inputs/TextareaInput.vue
similarity index 100%
rename from frontend/src/components/ui-kit/inputs/TextareaInput.vue
rename to ui/frontend/src/components/ui-kit/inputs/TextareaInput.vue
diff --git a/frontend/src/main.js b/ui/frontend/src/main.js
similarity index 100%
rename from frontend/src/main.js
rename to ui/frontend/src/main.js
diff --git a/frontend/src/stores/uiRuntime.ts b/ui/frontend/src/stores/uiRuntime.ts
similarity index 100%
rename from frontend/src/stores/uiRuntime.ts
rename to ui/frontend/src/stores/uiRuntime.ts
diff --git a/frontend/src/style.css b/ui/frontend/src/style.css
similarity index 100%
rename from frontend/src/style.css
rename to ui/frontend/src/style.css
diff --git a/frontend/src/ui/LayoutRenderer.vue b/ui/frontend/src/ui/LayoutRenderer.vue
similarity index 100%
rename from frontend/src/ui/LayoutRenderer.vue
rename to ui/frontend/src/ui/LayoutRenderer.vue
diff --git a/frontend/src/ui/UnknownWidget.vue b/ui/frontend/src/ui/UnknownWidget.vue
similarity index 100%
rename from frontend/src/ui/UnknownWidget.vue
rename to ui/frontend/src/ui/UnknownWidget.vue
diff --git a/frontend/src/ui/registry.ts b/ui/frontend/src/ui/registry.ts
similarity index 100%
rename from frontend/src/ui/registry.ts
rename to ui/frontend/src/ui/registry.ts
diff --git a/frontend/src/ui/schema.ts b/ui/frontend/src/ui/schema.ts
similarity index 100%
rename from frontend/src/ui/schema.ts
rename to ui/frontend/src/ui/schema.ts
diff --git a/frontend/src/ui/yaml.ts b/ui/frontend/src/ui/yaml.ts
similarity index 100%
rename from frontend/src/ui/yaml.ts
rename to ui/frontend/src/ui/yaml.ts
diff --git a/frontend/tailwind.config.js b/ui/frontend/tailwind.config.js
similarity index 100%
rename from frontend/tailwind.config.js
rename to ui/frontend/tailwind.config.js
diff --git a/frontend/vite.config.js b/ui/frontend/vite.config.js
similarity index 100%
rename from frontend/vite.config.js
rename to ui/frontend/vite.config.js