From 34482558c62b57f4fa07e18e5d3445593cd4d40b Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Fri, 6 Feb 2026 16:42:52 +0800 Subject: [PATCH 01/10] feat(actions): unify DSL actions with class schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更: - 引入 ActionBase 与参数 schema 校验 - 动作注册改为 ActionBase 实例 - 统一内置/协议/记录/数据/图表动作的类实现 - 文档更新为类化注册方式 - 影响: - DSL 动作参数会进行必填/类型/别名校验 - 兼容性/行为变化: - 不再支持函数式 ActionRegistry 注册 - 依赖/环境: - 无新增依赖 - 验证: - 未运行 [English] - Changes: - Introduce ActionBase with schema validation - Register actions as ActionBase instances - Migrate built-in/protocol/record/data/chart actions to classes - Update docs for class-based registration - Impact: - DSL action args are validated (required/type/alias) - Compatibility/Behavior changes: - Function-based ActionRegistry registration is no longer supported - Dependencies/Environment: - No new dependencies - Verification: - Not run Refs: - --- actions/base.py | 99 +++++++++++++ actions/builtin_actions.py | 279 ++++++++++++++++++++++++------------ actions/chart_actions.py | 115 ++++++++------- actions/data_actions.py | 158 +++++++++++--------- actions/protocol_actions.py | 109 ++++++++++---- actions/record_actions.py | 135 +++++++++-------- actions/registry.py | 16 ++- actions/schema_protocol.py | 109 ++++++++------ docs/USER_GUIDE.md | 26 +++- docs/USER_GUIDE_EN.md | 17 ++- runtime/context.py | 9 +- 11 files changed, 716 insertions(+), 356 deletions(-) create mode 100644 actions/base.py diff --git a/actions/base.py b/actions/base.py new file mode 100644 index 0000000..0cea9d8 --- /dev/null +++ b/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 ActionBase: + 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/builtin_actions.py b/actions/builtin_actions.py index f1312e1..8af4183 100644 --- a/actions/builtin_actions.py +++ b/actions/builtin_actions.py @@ -3,6 +3,7 @@ import time from typing import Any, Dict +from actions.base import ActionBase from actions.registry import ActionRegistry from dsl.expression import eval_expr @@ -13,98 +14,186 @@ def _eval(ctx, val): 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} +class SetAction(ActionBase): + 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(ActionBase): + 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(ActionBase): + 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(ActionBase): + 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(ActionBase): + 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(ActionBase): + 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(ActionBase): + 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/actions/chart_actions.py b/actions/chart_actions.py index 693c289..6a0beb1 100644 --- a/actions/chart_actions.py +++ b/actions/chart_actions.py @@ -3,6 +3,7 @@ import time from typing import Any, Dict +from actions.base import ActionBase from actions.chart_bridge import chart_bridge from actions.registry import ActionRegistry @@ -13,58 +14,76 @@ def _eval(ctx, val: Any) -> Any: 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"): +class ChartAddAction(ActionBase): + 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: - 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} + 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)") +class ChartAdd3dAction(ActionBase): + 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 - 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) + ActionRegistry.register("chart_add", ChartAddAction()) + ActionRegistry.register("chart_add3d", ChartAdd3dAction()) diff --git a/actions/data_actions.py b/actions/data_actions.py index 321f1d6..c97dc00 100644 --- a/actions/data_actions.py +++ b/actions/data_actions.py @@ -2,6 +2,7 @@ from typing import Any, Dict, Iterable, List, Tuple +from actions.base import ActionBase from actions.registry import ActionRegistry from dsl.expression import eval_expr @@ -59,18 +60,28 @@ def _parse_inline_actions(items: Any) -> List[Tuple[str, Dict[str, Any]]]: 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)} +class IfAction(ActionBase): + 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]: @@ -88,62 +99,77 @@ def _item_env(item: Any, index: int) -> Dict[str, Any]: 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) +class ListFilterAction(ActionBase): + 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(ActionBase): + 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") 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 + 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", action_if) - ActionRegistry.register("list_filter", action_list_filter) - ActionRegistry.register("list_map", action_list_map) - + ActionRegistry.register("if", IfAction()) + ActionRegistry.register("list_filter", ListFilterAction()) + ActionRegistry.register("list_map", ListMapAction()) diff --git a/actions/protocol_actions.py b/actions/protocol_actions.py index 6f8b579..f69c3dc 100644 --- a/actions/protocol_actions.py +++ b/actions/protocol_actions.py @@ -2,6 +2,7 @@ from typing import Dict +from actions.base import ActionBase from actions.registry import ActionRegistry from protocols.registry import ProtocolRegistry from protocols import modbus_ascii, modbus_rtu, modbus_tcp # noqa: F401 @@ -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(ActionBase): + 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(ActionBase): + 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(ActionBase): + 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(ActionBase): + 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/actions/record_actions.py b/actions/record_actions.py index 6ff44a2..c0ec4bf 100644 --- a/actions/record_actions.py +++ b/actions/record_actions.py @@ -2,6 +2,7 @@ from typing import Any, Dict +from actions.base import ActionBase from actions.registry import ActionRegistry from runtime.experiment_recorder import ExperimentRecorder @@ -11,71 +12,89 @@ def _eval_str(ctx, value: Any) -> str: 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 +class RecordStartAction(ActionBase): + def __init__(self) -> None: + super().__init__( + name="record_start", + schema={ + "optional": {"dir": None, "name": "run", "script_text": None, "script_path": None}, + "allow_extra": False, + }, + ) - 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") + 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 - 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) + 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_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) + 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) - rec = ExperimentRecorder(base_dir=base_dir, name=name, script_text=script_text, script_path=script_path) - root = rec.start() + 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) - 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() + 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 = None - if hasattr(ctx, "logger"): + 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(ActionBase): + 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: - ctx.logger.info(f"[REC] stop -> {rec.paths.root}") - except Exception: - pass - return str(rec.paths.root) + 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) + ActionRegistry.register("record_start", RecordStartAction()) + ActionRegistry.register("record_stop", RecordStopAction()) diff --git a/actions/registry.py b/actions/registry.py index 1be019e..28020f4 100644 --- a/actions/registry.py +++ b/actions/registry.py @@ -1,17 +1,21 @@ from __future__ import annotations -from typing import Any, Callable, Dict +from typing import Dict + +from actions.base import ActionBase class ActionRegistry: - actions: Dict[str, Callable[..., Any]] = {} + actions: Dict[str, ActionBase] = {} @classmethod - def register(cls, name: str, fn: Callable[..., Any]) -> None: - cls.actions[name] = fn + def register(cls, name: str, action: ActionBase) -> None: + if not isinstance(action, ActionBase): + raise TypeError("action must be an ActionBase") + cls.actions[name] = action @classmethod - def get(cls, name: str) -> Callable[..., Any]: + def get(cls, name: str) -> ActionBase: if name not in cls.actions: - raise KeyError(f"动作未注册: {name}") + raise KeyError(f"δע: {name}") return cls.actions[name] diff --git a/actions/schema_protocol.py b/actions/schema_protocol.py index 06d3ab5..05dd260 100644 --- a/actions/schema_protocol.py +++ b/actions/schema_protocol.py @@ -3,6 +3,7 @@ from functools import lru_cache from typing import Any, Dict +from actions.base import ActionBase from actions.registry import ActionRegistry from protocols.schema_runtime import ProtocolSchema @@ -12,50 +13,70 @@ 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 +class SendFrameAction(ActionBase): + 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(ActionBase): + 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", action_send_frame) - ActionRegistry.register("expect_frame", action_expect_frame) + ActionRegistry.register("send_frame", SendFrameAction()) + ActionRegistry.register("expect_frame", ExpectFrameAction()) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index e378077..d9d60c4 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -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`。 +- 自定义动作:继承 `ActionBase`,实现 `execute(ctx, args)` 并定义参数 `schema`,使用 `ActionRegistry.register("name", MyAction())` 注册。 ## 9. XMODEM 动作 - `send_xmodem_block`:发送指定块号(128B,自动 0x1A 填充),参数 `block: "$block"`。 @@ -267,11 +267,27 @@ state_machine: ## 15. 扩展指南 - 添加新动作: ```python + from actions.base import ActionBase 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) + + class MyAction(ActionBase): + 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)。 - 添加新协议适配:实现协议封包/解析,供动作调用。 diff --git a/docs/USER_GUIDE_EN.md b/docs/USER_GUIDE_EN.md index d3fa77b..94b8c67 100644 --- a/docs/USER_GUIDE_EN.md +++ b/docs/USER_GUIDE_EN.md @@ -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 `ActionBase`, 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.base import ActionBase from 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(ActionBase): + 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`. @@ -335,7 +340,7 @@ state_machine: 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 adapter: implement packet build/parse for actions to call. diff --git a/runtime/context.py b/runtime/context.py index c3ccd3f..95127a4 100644 --- a/runtime/context.py +++ b/runtime/context.py @@ -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: From 699ff31bfc72dc47a8ff2144cce93c747f3ddad5 Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Fri, 6 Feb 2026 16:48:42 +0800 Subject: [PATCH 02/10] refactor(actions): normalize protocol action filenames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更: - 重命名协议相关 action 文件为 protocol_*_actions - 同步更新运行时与脚本入口引用 - 影响: - 文件路径变化,但 action 名称不变 - 兼容性/行为变化: - 仅文件名/导入变更 - 依赖/环境: - 无新增依赖 - 验证: - 未运行 [English] - Changes: - Rename protocol action files to protocol_*_actions - Update runtime/script imports - Impact: - File paths changed; action names unchanged - Compatibility/Behavior changes: - Filename/import only - Dependencies/Environment: - No new dependencies - Verification: - Not run Refs: - --- actions/{at_command.py => protocol_at_actions.py} | 0 ...{modbus_request.py => protocol_modbus_actions.py} | 0 ...schema_protocol.py => protocol_schema_actions.py} | 0 .../{scpi_command.py => protocol_scpi_actions.py} | 0 .../{xmodem_send.py => protocol_xmodem_actions.py} | 0 .../{ymodem_send.py => protocol_ymodem_actions.py} | 0 desktop/script_runner_qt.py | 2 +- main_runtime.py | 12 ++++++------ runtime/runner.py | 2 +- 9 files changed, 8 insertions(+), 8 deletions(-) rename actions/{at_command.py => protocol_at_actions.py} (100%) rename actions/{modbus_request.py => protocol_modbus_actions.py} (100%) rename actions/{schema_protocol.py => protocol_schema_actions.py} (100%) rename actions/{scpi_command.py => protocol_scpi_actions.py} (100%) rename actions/{xmodem_send.py => protocol_xmodem_actions.py} (100%) rename actions/{ymodem_send.py => protocol_ymodem_actions.py} (100%) diff --git a/actions/at_command.py b/actions/protocol_at_actions.py similarity index 100% rename from actions/at_command.py rename to actions/protocol_at_actions.py diff --git a/actions/modbus_request.py b/actions/protocol_modbus_actions.py similarity index 100% rename from actions/modbus_request.py rename to actions/protocol_modbus_actions.py diff --git a/actions/schema_protocol.py b/actions/protocol_schema_actions.py similarity index 100% rename from actions/schema_protocol.py rename to actions/protocol_schema_actions.py diff --git a/actions/scpi_command.py b/actions/protocol_scpi_actions.py similarity index 100% rename from actions/scpi_command.py rename to actions/protocol_scpi_actions.py diff --git a/actions/xmodem_send.py b/actions/protocol_xmodem_actions.py similarity index 100% rename from actions/xmodem_send.py rename to actions/protocol_xmodem_actions.py diff --git a/actions/ymodem_send.py b/actions/protocol_ymodem_actions.py similarity index 100% rename from actions/ymodem_send.py rename to actions/protocol_ymodem_actions.py diff --git a/desktop/script_runner_qt.py b/desktop/script_runner_qt.py index 08d7bd6..210b5ca 100644 --- a/desktop/script_runner_qt.py +++ b/desktop/script_runner_qt.py @@ -10,7 +10,7 @@ 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.protocol_schema_actions 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 diff --git a/main_runtime.py b/main_runtime.py index 1378ed8..64be209 100644 --- a/main_runtime.py +++ b/main_runtime.py @@ -9,7 +9,7 @@ import yaml -from actions import at_command, modbus_request, scpi_command, xmodem_send, ymodem_send +from actions import protocol_at_actions, protocol_modbus_actions, protocol_scpi_actions, protocol_xmodem_actions, protocol_ymodem_actions from protocols import at, modbus_ascii, modbus_rtu, modbus_tcp, scpi, xmodem, ymodem # noqa: F401 触发注册 from utils.path_utils import resolve_resource_path @@ -103,11 +103,11 @@ def close(self) -> None: 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, + "at_command": protocol_at_actions.run, + "modbus_request": protocol_modbus_actions.run, + "scpi_command": protocol_scpi_actions.run, + "xmodem_send": protocol_xmodem_actions.run, + "ymodem_send": protocol_ymodem_actions.run, } diff --git a/runtime/runner.py b/runtime/runner.py index 08322e1..b5e15bf 100644 --- a/runtime/runner.py +++ b/runtime/runner.py @@ -4,7 +4,7 @@ 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.protocol_schema_actions 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 ee00a772961b713fa1c4039e02c1ff4bd88537d4 Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Fri, 6 Feb 2026 16:55:45 +0800 Subject: [PATCH 03/10] refactor(actions): group DSL actions under actions/dsl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更: - 将 DSL 动作文件迁移到 actions/dsl 并统一 dsl_*_actions 命名 - 更新 runner 与脚本入口的导入路径 - 新增 actions/dsl 包初始化文件 - 影响: - DSL 动作模块路径变化 - 兼容性/行为变化: - 仅目录/文件名调整,动作行为不变 - 依赖/环境: - 无新增依赖 - 验证: - 未运行 [English] - Changes: - Move DSL actions under actions/dsl with dsl_*_actions filenames - Update runner and script imports - Add actions/dsl package init - Impact: - DSL action module paths changed - Compatibility/Behavior changes: - Filename/directory change only - Dependencies/Environment: - No new dependencies - Verification: - Not run Refs: - --- actions/dsl/__init__.py | 1 + .../dsl_builtin_actions.py} | 0 .../{chart_actions.py => dsl/dsl_chart_actions.py} | 0 actions/{data_actions.py => dsl/dsl_data_actions.py} | 0 .../dsl_protocol_actions.py} | 0 .../dsl_protocol_schema_actions.py} | 0 .../{record_actions.py => dsl/dsl_record_actions.py} | 0 desktop/script_runner_qt.py | 12 ++++++------ runtime/runner.py | 12 ++++++------ 9 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 actions/dsl/__init__.py rename actions/{builtin_actions.py => dsl/dsl_builtin_actions.py} (100%) rename actions/{chart_actions.py => dsl/dsl_chart_actions.py} (100%) rename actions/{data_actions.py => dsl/dsl_data_actions.py} (100%) rename actions/{protocol_actions.py => dsl/dsl_protocol_actions.py} (100%) rename actions/{protocol_schema_actions.py => dsl/dsl_protocol_schema_actions.py} (100%) rename actions/{record_actions.py => dsl/dsl_record_actions.py} (100%) diff --git a/actions/dsl/__init__.py b/actions/dsl/__init__.py new file mode 100644 index 0000000..a67640c --- /dev/null +++ b/actions/dsl/__init__.py @@ -0,0 +1 @@ +# DSL action modules package. diff --git a/actions/builtin_actions.py b/actions/dsl/dsl_builtin_actions.py similarity index 100% rename from actions/builtin_actions.py rename to actions/dsl/dsl_builtin_actions.py diff --git a/actions/chart_actions.py b/actions/dsl/dsl_chart_actions.py similarity index 100% rename from actions/chart_actions.py rename to actions/dsl/dsl_chart_actions.py diff --git a/actions/data_actions.py b/actions/dsl/dsl_data_actions.py similarity index 100% rename from actions/data_actions.py rename to actions/dsl/dsl_data_actions.py diff --git a/actions/protocol_actions.py b/actions/dsl/dsl_protocol_actions.py similarity index 100% rename from actions/protocol_actions.py rename to actions/dsl/dsl_protocol_actions.py diff --git a/actions/protocol_schema_actions.py b/actions/dsl/dsl_protocol_schema_actions.py similarity index 100% rename from actions/protocol_schema_actions.py rename to actions/dsl/dsl_protocol_schema_actions.py diff --git a/actions/record_actions.py b/actions/dsl/dsl_record_actions.py similarity index 100% rename from actions/record_actions.py rename to actions/dsl/dsl_record_actions.py diff --git a/desktop/script_runner_qt.py b/desktop/script_runner_qt.py index 210b5ca..73f40c6 100644 --- a/desktop/script_runner_qt.py +++ b/desktop/script_runner_qt.py @@ -8,12 +8,12 @@ from PySide6.QtCore import QThread, Signal -from actions.builtin_actions import register_builtin_actions -from actions.protocol_actions import register_protocol_actions -from actions.protocol_schema_actions 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 actions.dsl.dsl_builtin_actions import register_builtin_actions +from actions.dsl.dsl_protocol_actions import register_protocol_actions +from actions.dsl.dsl_protocol_schema_actions import register_schema_protocol_actions +from actions.dsl.dsl_chart_actions import register_chart_actions +from actions.dsl.dsl_record_actions import register_record_actions +from actions.dsl.dsl_data_actions import register_data_actions from dsl.executor import StateMachineExecutor from dsl.parser import parse_script from runtime.channels import build_channels diff --git a/runtime/runner.py b/runtime/runner.py index b5e15bf..34b5ee9 100644 --- a/runtime/runner.py +++ b/runtime/runner.py @@ -2,12 +2,12 @@ import logging -from actions.builtin_actions import register_builtin_actions -from actions.protocol_actions import register_protocol_actions -from actions.protocol_schema_actions 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 actions.dsl.dsl_builtin_actions import register_builtin_actions +from actions.dsl.dsl_protocol_actions import register_protocol_actions +from actions.dsl.dsl_protocol_schema_actions import register_schema_protocol_actions +from actions.dsl.dsl_chart_actions import register_chart_actions +from actions.dsl.dsl_record_actions import register_record_actions +from actions.dsl.dsl_data_actions import register_data_actions from dsl.executor import StateMachineExecutor from dsl.parser import parse_script from runtime.channels import build_channels From 596b27f476618af5146492ad5226c850841f7bfd Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Fri, 6 Feb 2026 16:58:57 +0800 Subject: [PATCH 04/10] refactor(actions): group runtime task actions under actions/runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更: - 新增 actions/runtime 目录 - 运行时任务动作迁移为 runtime_protocol_*_actions - 更新 main_runtime 导入与映射 - 影响: - 运行时任务动作模块路径变化 - 兼容性/行为变化: - 仅目录/文件名调整 - 依赖/环境: - 无新增依赖 - 验证: - 未运行 [English] - Changes: - Add actions/runtime package - Move runtime task actions to runtime_protocol_*_actions - Update main_runtime imports and mapping - Impact: - Runtime task module paths changed - Compatibility/Behavior changes: - Filename/directory change only - Dependencies/Environment: - No new dependencies - Verification: - Not run Refs: - --- actions/runtime/__init__.py | 1 + .../runtime_protocol_at_actions.py} | 0 .../runtime_protocol_modbus_actions.py} | 0 .../runtime_protocol_scpi_actions.py} | 0 .../runtime_protocol_xmodem_actions.py} | 0 .../runtime_protocol_ymodem_actions.py} | 0 main_runtime.py | 12 ++++++------ 7 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 actions/runtime/__init__.py rename actions/{protocol_at_actions.py => runtime/runtime_protocol_at_actions.py} (100%) rename actions/{protocol_modbus_actions.py => runtime/runtime_protocol_modbus_actions.py} (100%) rename actions/{protocol_scpi_actions.py => runtime/runtime_protocol_scpi_actions.py} (100%) rename actions/{protocol_xmodem_actions.py => runtime/runtime_protocol_xmodem_actions.py} (100%) rename actions/{protocol_ymodem_actions.py => runtime/runtime_protocol_ymodem_actions.py} (100%) diff --git a/actions/runtime/__init__.py b/actions/runtime/__init__.py new file mode 100644 index 0000000..02c369c --- /dev/null +++ b/actions/runtime/__init__.py @@ -0,0 +1 @@ +# Runtime task action modules package. diff --git a/actions/protocol_at_actions.py b/actions/runtime/runtime_protocol_at_actions.py similarity index 100% rename from actions/protocol_at_actions.py rename to actions/runtime/runtime_protocol_at_actions.py diff --git a/actions/protocol_modbus_actions.py b/actions/runtime/runtime_protocol_modbus_actions.py similarity index 100% rename from actions/protocol_modbus_actions.py rename to actions/runtime/runtime_protocol_modbus_actions.py diff --git a/actions/protocol_scpi_actions.py b/actions/runtime/runtime_protocol_scpi_actions.py similarity index 100% rename from actions/protocol_scpi_actions.py rename to actions/runtime/runtime_protocol_scpi_actions.py diff --git a/actions/protocol_xmodem_actions.py b/actions/runtime/runtime_protocol_xmodem_actions.py similarity index 100% rename from actions/protocol_xmodem_actions.py rename to actions/runtime/runtime_protocol_xmodem_actions.py diff --git a/actions/protocol_ymodem_actions.py b/actions/runtime/runtime_protocol_ymodem_actions.py similarity index 100% rename from actions/protocol_ymodem_actions.py rename to actions/runtime/runtime_protocol_ymodem_actions.py diff --git a/main_runtime.py b/main_runtime.py index 64be209..0cb1bc2 100644 --- a/main_runtime.py +++ b/main_runtime.py @@ -9,7 +9,7 @@ import yaml -from actions import protocol_at_actions, protocol_modbus_actions, protocol_scpi_actions, protocol_xmodem_actions, protocol_ymodem_actions +from actions.runtime import runtime_protocol_at_actions, runtime_protocol_modbus_actions, runtime_protocol_scpi_actions, runtime_protocol_xmodem_actions, runtime_protocol_ymodem_actions from protocols import at, modbus_ascii, modbus_rtu, modbus_tcp, scpi, xmodem, ymodem # noqa: F401 触发注册 from utils.path_utils import resolve_resource_path @@ -103,11 +103,11 @@ def close(self) -> None: ACTIONS = { - "at_command": protocol_at_actions.run, - "modbus_request": protocol_modbus_actions.run, - "scpi_command": protocol_scpi_actions.run, - "xmodem_send": protocol_xmodem_actions.run, - "ymodem_send": protocol_ymodem_actions.run, + "at_command": runtime_protocol_at_actions.run, + "modbus_request": runtime_protocol_modbus_actions.run, + "scpi_command": runtime_protocol_scpi_actions.run, + "xmodem_send": runtime_protocol_xmodem_actions.run, + "ymodem_send": runtime_protocol_ymodem_actions.run, } From 7d0687673e97739dfa8d35b816c69cf5be9caac5 Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Fri, 6 Feb 2026 17:07:14 +0800 Subject: [PATCH 05/10] refactor(actions): split DSL and runtime action bases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更: - 将 DSL 基类移动为 actions/dsl/base.py 并更名为 DslActionBase - 新增运行时任务基类 RuntimeTaskBase - DSL 动作与注册器改用 DslActionBase - 运行时任务动作改为 RuntimeTaskBase 封装 - 文档示例同步更新 - 影响: - DSL/运行时动作基类路径变化 - 兼容性/行为变化: - 仅基类与导入变更,行为不变 - 依赖/环境: - 无新增依赖 - 验证: - 未运行 [English] - Changes: - Move DSL base to actions/dsl/base.py and rename to DslActionBase - Add RuntimeTaskBase for runtime tasks - Update DSL actions and registry to DslActionBase - Wrap runtime tasks with RuntimeTaskBase - Sync docs examples - Impact: - DSL/runtime base class paths changed - Compatibility/Behavior changes: - Base/import changes only - Dependencies/Environment: - No new dependencies - Verification: - Not run Refs: - --- actions/{ => dsl}/base.py | 2 +- actions/dsl/dsl_builtin_actions.py | 16 +-- actions/dsl/dsl_chart_actions.py | 6 +- actions/dsl/dsl_data_actions.py | 8 +- actions/dsl/dsl_protocol_actions.py | 10 +- actions/dsl/dsl_protocol_schema_actions.py | 6 +- actions/dsl/dsl_record_actions.py | 6 +- actions/registry.py | 12 +-- actions/runtime/base.py | 99 +++++++++++++++++++ .../runtime/runtime_protocol_at_actions.py | 52 ++++++---- .../runtime_protocol_modbus_actions.py | 97 ++++++++++++------ .../runtime/runtime_protocol_scpi_actions.py | 62 +++++++----- .../runtime_protocol_xmodem_actions.py | 47 ++++++--- .../runtime_protocol_ymodem_actions.py | 47 ++++++--- docs/USER_GUIDE.md | 6 +- docs/USER_GUIDE_EN.md | 6 +- 16 files changed, 346 insertions(+), 136 deletions(-) rename actions/{ => dsl}/base.py (99%) create mode 100644 actions/runtime/base.py diff --git a/actions/base.py b/actions/dsl/base.py similarity index 99% rename from actions/base.py rename to actions/dsl/base.py index 0cea9d8..2c9fc8a 100644 --- a/actions/base.py +++ b/actions/dsl/base.py @@ -18,7 +18,7 @@ class ActionValidationError(ValueError): @dataclass -class ActionBase: +class DslActionBase: name: str schema: Dict[str, Any] | None = None diff --git a/actions/dsl/dsl_builtin_actions.py b/actions/dsl/dsl_builtin_actions.py index 8af4183..6abc025 100644 --- a/actions/dsl/dsl_builtin_actions.py +++ b/actions/dsl/dsl_builtin_actions.py @@ -3,7 +3,7 @@ import time from typing import Any, Dict -from actions.base import ActionBase +from actions.dsl.base import DslActionBase from actions.registry import ActionRegistry from dsl.expression import eval_expr @@ -14,7 +14,7 @@ def _eval(ctx, val): return val -class SetAction(ActionBase): +class SetAction(DslActionBase): def __init__(self) -> None: super().__init__( name="set", @@ -29,7 +29,7 @@ def execute(self, ctx, args: Dict[str, Any]): return ctx.vars_snapshot() -class LogAction(ActionBase): +class LogAction(DslActionBase): def __init__(self) -> None: super().__init__( name="log", @@ -48,7 +48,7 @@ def execute(self, ctx, args: Dict[str, Any]): ctx.logger.info(str(msg)) -class WaitAction(ActionBase): +class WaitAction(DslActionBase): def __init__(self) -> None: super().__init__( name="wait", @@ -64,7 +64,7 @@ def execute(self, ctx, args: Dict[str, Any]): time.sleep(ms / 1000.0) -class WaitForEventAction(ActionBase): +class WaitForEventAction(DslActionBase): def __init__(self) -> None: super().__init__( name="wait_for_event", @@ -89,7 +89,7 @@ def execute(self, ctx, args: Dict[str, Any]): return None -class SendTextAction(ActionBase): +class SendTextAction(DslActionBase): def __init__(self) -> None: super().__init__( name="send_text", @@ -117,7 +117,7 @@ def execute(self, ctx, args: Dict[str, Any]): return {"text": text, "append_cr": append_cr, "append_lf": append_lf} -class ReadLineAction(ActionBase): +class ReadLineAction(DslActionBase): def __init__(self) -> None: super().__init__( name="read_line", @@ -141,7 +141,7 @@ def execute(self, ctx, args: Dict[str, Any]): return {"text": text, "hex": raw.hex().upper()} -class ReadStreamAction(ActionBase): +class ReadStreamAction(DslActionBase): def __init__(self) -> None: super().__init__( name="read_stream", diff --git a/actions/dsl/dsl_chart_actions.py b/actions/dsl/dsl_chart_actions.py index 6a0beb1..6e2d6f7 100644 --- a/actions/dsl/dsl_chart_actions.py +++ b/actions/dsl/dsl_chart_actions.py @@ -3,7 +3,7 @@ import time from typing import Any, Dict -from actions.base import ActionBase +from actions.dsl.base import DslActionBase from actions.chart_bridge import chart_bridge from actions.registry import ActionRegistry @@ -14,7 +14,7 @@ def _eval(ctx, val: Any) -> Any: return val -class ChartAddAction(ActionBase): +class ChartAddAction(DslActionBase): def __init__(self) -> None: super().__init__( name="chart_add", @@ -49,7 +49,7 @@ def execute(self, ctx, args: Dict[str, Any]): return {"ts": ts, "bind": str(bind), "value": val} -class ChartAdd3dAction(ActionBase): +class ChartAdd3dAction(DslActionBase): def __init__(self) -> None: super().__init__( name="chart_add3d", diff --git a/actions/dsl/dsl_data_actions.py b/actions/dsl/dsl_data_actions.py index c97dc00..37f8bca 100644 --- a/actions/dsl/dsl_data_actions.py +++ b/actions/dsl/dsl_data_actions.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Iterable, List, Tuple -from actions.base import ActionBase +from actions.dsl.base import DslActionBase from actions.registry import ActionRegistry from dsl.expression import eval_expr @@ -60,7 +60,7 @@ def _parse_inline_actions(items: Any) -> List[Tuple[str, Dict[str, Any]]]: return parsed -class IfAction(ActionBase): +class IfAction(DslActionBase): def __init__(self) -> None: super().__init__( name="if", @@ -99,7 +99,7 @@ def _item_env(item: Any, index: int) -> Dict[str, Any]: return env -class ListFilterAction(ActionBase): +class ListFilterAction(DslActionBase): def __init__(self) -> None: super().__init__( name="list_filter", @@ -133,7 +133,7 @@ def execute(self, ctx, args: Dict[str, Any]) -> List[Any]: return out -class ListMapAction(ActionBase): +class ListMapAction(DslActionBase): def __init__(self) -> None: super().__init__( name="list_map", diff --git a/actions/dsl/dsl_protocol_actions.py b/actions/dsl/dsl_protocol_actions.py index f69c3dc..401c2b8 100644 --- a/actions/dsl/dsl_protocol_actions.py +++ b/actions/dsl/dsl_protocol_actions.py @@ -2,7 +2,7 @@ from typing import Dict -from actions.base import ActionBase +from actions.dsl.base import DslActionBase from actions.registry import ActionRegistry from protocols.registry import ProtocolRegistry from protocols import modbus_ascii, modbus_rtu, modbus_tcp # noqa: F401 @@ -26,7 +26,7 @@ def build_eot() -> bytes: return bytes([0x04]) -class SendXmodemBlockAction(ActionBase): +class SendXmodemBlockAction(DslActionBase): def __init__(self) -> None: super().__init__( name="send_xmodem_block", @@ -51,7 +51,7 @@ def execute(self, ctx, args: Dict[str, object]): ctx.set_var("last_sent_block", block) -class SendEotAction(ActionBase): +class SendEotAction(DslActionBase): def __init__(self) -> None: super().__init__( name="send_eot", @@ -150,7 +150,7 @@ def _run_modbus(ctx, args: Dict[str, object]): return result -class ModbusReadAction(ActionBase): +class ModbusReadAction(DslActionBase): def __init__(self) -> None: super().__init__( name="modbus_read", @@ -175,7 +175,7 @@ def execute(self, ctx, args: Dict[str, object]): return _run_modbus(ctx, args) -class ModbusWriteAction(ActionBase): +class ModbusWriteAction(DslActionBase): def __init__(self) -> None: super().__init__( name="modbus_write", diff --git a/actions/dsl/dsl_protocol_schema_actions.py b/actions/dsl/dsl_protocol_schema_actions.py index 05dd260..db721da 100644 --- a/actions/dsl/dsl_protocol_schema_actions.py +++ b/actions/dsl/dsl_protocol_schema_actions.py @@ -3,7 +3,7 @@ from functools import lru_cache from typing import Any, Dict -from actions.base import ActionBase +from actions.dsl.base import DslActionBase from actions.registry import ActionRegistry from protocols.schema_runtime import ProtocolSchema @@ -13,7 +13,7 @@ def _load_schema(path: str) -> ProtocolSchema: return ProtocolSchema.load(path) -class SendFrameAction(ActionBase): +class SendFrameAction(DslActionBase): def __init__(self) -> None: super().__init__( name="send_frame", @@ -36,7 +36,7 @@ def execute(self, ctx, args: Dict[str, Any]): return packet -class ExpectFrameAction(ActionBase): +class ExpectFrameAction(DslActionBase): def __init__(self) -> None: super().__init__( name="expect_frame", diff --git a/actions/dsl/dsl_record_actions.py b/actions/dsl/dsl_record_actions.py index c0ec4bf..bb88a3b 100644 --- a/actions/dsl/dsl_record_actions.py +++ b/actions/dsl/dsl_record_actions.py @@ -2,7 +2,7 @@ from typing import Any, Dict -from actions.base import ActionBase +from actions.dsl.base import DslActionBase from actions.registry import ActionRegistry from runtime.experiment_recorder import ExperimentRecorder @@ -12,7 +12,7 @@ def _eval_str(ctx, value: Any) -> str: return str(v) if v is not None else "" -class RecordStartAction(ActionBase): +class RecordStartAction(DslActionBase): def __init__(self) -> None: super().__init__( name="record_start", @@ -61,7 +61,7 @@ def execute(self, ctx, args: Dict[str, Any]): return str(root) -class RecordStopAction(ActionBase): +class RecordStopAction(DslActionBase): def __init__(self) -> None: super().__init__( name="record_stop", diff --git a/actions/registry.py b/actions/registry.py index 28020f4..b66f9e8 100644 --- a/actions/registry.py +++ b/actions/registry.py @@ -2,20 +2,20 @@ from typing import Dict -from actions.base import ActionBase +from actions.dsl.base import DslActionBase class ActionRegistry: - actions: Dict[str, ActionBase] = {} + actions: Dict[str, DslActionBase] = {} @classmethod - def register(cls, name: str, action: ActionBase) -> None: - if not isinstance(action, ActionBase): - raise TypeError("action must be an ActionBase") + 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) -> ActionBase: + def get(cls, name: str) -> DslActionBase: if name not in cls.actions: raise KeyError(f"δע: {name}") return cls.actions[name] diff --git a/actions/runtime/base.py b/actions/runtime/base.py new file mode 100644 index 0000000..60df7da --- /dev/null +++ b/actions/runtime/base.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Tuple, Type + + +class RuntimeTaskValidationError(ValueError): + pass + + +_TYPE_ALIASES: Dict[str, Tuple[Type[Any], ...]] = { + "number": (int, float), + "string": (str,), + "bool": (bool,), + "mapping": (dict,), + "list": (list, tuple), +} + + +@dataclass +class RuntimeTaskBase: + name: str + schema: Dict[str, Any] | None = None + + def run(self, task: Dict[str, Any] | None, channels: Dict[str, Any], logger) -> Any: + payload = self.validate_task(task) + return self.execute(payload, channels, logger) + + def execute(self, task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any: + raise NotImplementedError() + + def validate_task(self, task: Dict[str, Any] | None) -> Dict[str, Any]: + if task is None: + task = {} + if not isinstance(task, dict): + raise RuntimeTaskValidationError("task 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(task) + 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 RuntimeTaskValidationError(f"missing required field: {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 RuntimeTaskValidationError(f"unknown fields: {', '.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 RuntimeTaskValidationError( + f"field '{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/runtime/runtime_protocol_at_actions.py b/actions/runtime/runtime_protocol_at_actions.py index c255131..859db32 100644 --- a/actions/runtime/runtime_protocol_at_actions.py +++ b/actions/runtime/runtime_protocol_at_actions.py @@ -2,24 +2,42 @@ from typing import Any, Dict +from actions.runtime.base import RuntimeTaskBase from protocols.registry import ProtocolRegistry +class AtCommandTask(RuntimeTaskBase): + def __init__(self) -> None: + super().__init__( + name="at_command", + schema={ + "aliases": {"command": "cmd"}, + "required": ["channel", "cmd"], + "optional": {"timeout": 2.0, "terminator": "\r\n", "ok": "OK", "error": "ERROR", "echo": True}, + "types": {"timeout": "number", "echo": bool}, + "allow_extra": False, + }, + ) + + def execute(self, 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") + 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) + + +_TASK = AtCommandTask() + + 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) + return _TASK.run(task, channels, logger) diff --git a/actions/runtime/runtime_protocol_modbus_actions.py b/actions/runtime/runtime_protocol_modbus_actions.py index 5b6e943..ba94f69 100644 --- a/actions/runtime/runtime_protocol_modbus_actions.py +++ b/actions/runtime/runtime_protocol_modbus_actions.py @@ -2,6 +2,7 @@ from typing import Any, Dict +from actions.runtime.base import RuntimeTaskBase from protocols.registry import ProtocolRegistry _MAP = { @@ -11,36 +12,68 @@ } +class ModbusRequestTask(RuntimeTaskBase): + def __init__(self) -> None: + super().__init__( + name="modbus_request", + schema={ + "required": ["protocol", "channel", "function", "address"], + "optional": { + "quantity": 1, + "values": None, + "unit_id": 1, + "timeout": None, + "retries": 3, + }, + "types": {"quantity": "number", "unit_id": "number", "timeout": "number", "retries": "number"}, + "allow_extra": False, + }, + ) + + def execute(self, 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, + ) + + +_TASK = ModbusRequestTask() + + 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, - ) + return _TASK.run(task, channels, logger) diff --git a/actions/runtime/runtime_protocol_scpi_actions.py b/actions/runtime/runtime_protocol_scpi_actions.py index 7e9d530..15399c9 100644 --- a/actions/runtime/runtime_protocol_scpi_actions.py +++ b/actions/runtime/runtime_protocol_scpi_actions.py @@ -2,29 +2,47 @@ from typing import Any, Dict +from actions.runtime.base import RuntimeTaskBase from protocols.registry import ProtocolRegistry +class ScpiCommandTask(RuntimeTaskBase): + def __init__(self) -> None: + super().__init__( + name="scpi_command", + schema={ + "aliases": {"command": "cmd"}, + "required": ["channel", "cmd"], + "optional": {"timeout": 2.0, "terminator": "\n", "expect_response": None, "strip": True}, + "types": {"timeout": "number", "strip": bool}, + "allow_extra": False, + }, + ) + + def execute(self, 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") + 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, + ) + + +_TASK = ScpiCommandTask() + + 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, - ) + return _TASK.run(task, channels, logger) diff --git a/actions/runtime/runtime_protocol_xmodem_actions.py b/actions/runtime/runtime_protocol_xmodem_actions.py index a634bea..c267af6 100644 --- a/actions/runtime/runtime_protocol_xmodem_actions.py +++ b/actions/runtime/runtime_protocol_xmodem_actions.py @@ -2,20 +2,41 @@ from typing import Any, Dict +from actions.runtime.base import RuntimeTaskBase from protocols.registry import ProtocolRegistry +class XmodemSendTask(RuntimeTaskBase): + def __init__(self) -> None: + super().__init__( + name="xmodem_send", + schema={ + "aliases": {"path": "file"}, + "required": ["channel", "file"], + "optional": {"retries": 10, "start_timeout": 10.0}, + "types": {"retries": "number", "start_timeout": "number"}, + "allow_extra": False, + }, + ) + + def execute(self, 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") + 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) + + +_TASK = XmodemSendTask() + + 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) + return _TASK.run(task, channels, logger) diff --git a/actions/runtime/runtime_protocol_ymodem_actions.py b/actions/runtime/runtime_protocol_ymodem_actions.py index 4c5846d..371ef8e 100644 --- a/actions/runtime/runtime_protocol_ymodem_actions.py +++ b/actions/runtime/runtime_protocol_ymodem_actions.py @@ -2,20 +2,41 @@ from typing import Any, Dict +from actions.runtime.base import RuntimeTaskBase from protocols.registry import ProtocolRegistry +class YmodemSendTask(RuntimeTaskBase): + def __init__(self) -> None: + super().__init__( + name="ymodem_send", + schema={ + "aliases": {"path": "file"}, + "required": ["channel", "file"], + "optional": {"retries": 10, "start_timeout": 10.0}, + "types": {"retries": "number", "start_timeout": "number"}, + "allow_extra": False, + }, + ) + + def execute(self, 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") + 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) + + +_TASK = YmodemSendTask() + + 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) + return _TASK.run(task, channels, logger) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index d9d60c4..9ae0a1d 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -104,7 +104,7 @@ stateDiagram-v2 - `list_filter`: 列表过滤(`src`/`where`,可选 `dst`)。 - `list_map`: 列表映射(`src`/`expr`,可选 `dst`,可选 `where`)。 - 协议动作:XMODEM/Modbus 等(下文详述)。 -- 自定义动作:继承 `ActionBase`,实现 `execute(ctx, args)` 并定义参数 `schema`,使用 `ActionRegistry.register("name", MyAction())` 注册。 +- 自定义动作:继承 `DslActionBase`,实现 `execute(ctx, args)` 并定义参数 `schema`,使用 `ActionRegistry.register("name", MyAction())` 注册。 ## 9. XMODEM 动作 - `send_xmodem_block`:发送指定块号(128B,自动 0x1A 填充),参数 `block: "$block"`。 @@ -267,10 +267,10 @@ state_machine: ## 15. 扩展指南 - 添加新动作: ```python - from actions.base import ActionBase + from actions.dsl.base import DslActionBase from actions.registry import ActionRegistry - class MyAction(ActionBase): + class MyAction(DslActionBase): def __init__(self) -> None: super().__init__( name="my_action", diff --git a/docs/USER_GUIDE_EN.md b/docs/USER_GUIDE_EN.md index 94b8c67..16573c2 100644 --- a/docs/USER_GUIDE_EN.md +++ b/docs/USER_GUIDE_EN.md @@ -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: subclass `ActionBase`, implement `execute(ctx, args)` with a parameter `schema`, then register the instance via `ActionRegistry.register("name", MyAction())`. +- 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,10 +160,10 @@ 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.base import ActionBase + from actions.dsl.base import DslActionBase from actions.registry import ActionRegistry - class MyAction(ActionBase): + class MyAction(DslActionBase): def __init__(self) -> None: super().__init__(name="my_action", schema={"allow_extra": False}) From 46b8160aa4133f7ea6497c6916b042ae97dd8cff Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Fri, 6 Feb 2026 17:27:44 +0800 Subject: [PATCH 06/10] refactor(runtime): remove task-mode runtime entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更: - 删除 main_runtime.py 与 actions/runtime 任务入口 - 文档移除 task 模式说明,改为 DSL 动作说明 - 影响: - 移除非 DSL 的 tasks 执行路径 - 兼容性/行为变化: - 不再支持 main_runtime tasks 模式 - 依赖/环境: - 无新增依赖 - 验证: - 未运行 [English] - Changes: - Remove main_runtime.py and actions/runtime task entry - Update docs to describe DSL actions instead of task mode - Impact: - Non-DSL tasks execution path removed - Compatibility/Behavior changes: - main_runtime tasks mode no longer available - Dependencies/Environment: - No new dependencies - Verification: - Not run Refs: - --- actions/runtime/__init__.py | 1 - actions/runtime/base.py | 99 --------- .../runtime/runtime_protocol_at_actions.py | 43 ---- .../runtime_protocol_modbus_actions.py | 79 -------- .../runtime/runtime_protocol_scpi_actions.py | 48 ----- .../runtime_protocol_xmodem_actions.py | 42 ---- .../runtime_protocol_ymodem_actions.py | 42 ---- docs/USER_GUIDE.md | 2 +- docs/USER_GUIDE_EN.md | 2 +- main_runtime.py | 190 ------------------ 10 files changed, 2 insertions(+), 546 deletions(-) delete mode 100644 actions/runtime/__init__.py delete mode 100644 actions/runtime/base.py delete mode 100644 actions/runtime/runtime_protocol_at_actions.py delete mode 100644 actions/runtime/runtime_protocol_modbus_actions.py delete mode 100644 actions/runtime/runtime_protocol_scpi_actions.py delete mode 100644 actions/runtime/runtime_protocol_xmodem_actions.py delete mode 100644 actions/runtime/runtime_protocol_ymodem_actions.py delete mode 100644 main_runtime.py diff --git a/actions/runtime/__init__.py b/actions/runtime/__init__.py deleted file mode 100644 index 02c369c..0000000 --- a/actions/runtime/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Runtime task action modules package. diff --git a/actions/runtime/base.py b/actions/runtime/base.py deleted file mode 100644 index 60df7da..0000000 --- a/actions/runtime/base.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Dict, Tuple, Type - - -class RuntimeTaskValidationError(ValueError): - pass - - -_TYPE_ALIASES: Dict[str, Tuple[Type[Any], ...]] = { - "number": (int, float), - "string": (str,), - "bool": (bool,), - "mapping": (dict,), - "list": (list, tuple), -} - - -@dataclass -class RuntimeTaskBase: - name: str - schema: Dict[str, Any] | None = None - - def run(self, task: Dict[str, Any] | None, channels: Dict[str, Any], logger) -> Any: - payload = self.validate_task(task) - return self.execute(payload, channels, logger) - - def execute(self, task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any: - raise NotImplementedError() - - def validate_task(self, task: Dict[str, Any] | None) -> Dict[str, Any]: - if task is None: - task = {} - if not isinstance(task, dict): - raise RuntimeTaskValidationError("task 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(task) - 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 RuntimeTaskValidationError(f"missing required field: {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 RuntimeTaskValidationError(f"unknown fields: {', '.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 RuntimeTaskValidationError( - f"field '{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/runtime/runtime_protocol_at_actions.py b/actions/runtime/runtime_protocol_at_actions.py deleted file mode 100644 index 859db32..0000000 --- a/actions/runtime/runtime_protocol_at_actions.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -from typing import Any, Dict - -from actions.runtime.base import RuntimeTaskBase -from protocols.registry import ProtocolRegistry - - -class AtCommandTask(RuntimeTaskBase): - def __init__(self) -> None: - super().__init__( - name="at_command", - schema={ - "aliases": {"command": "cmd"}, - "required": ["channel", "cmd"], - "optional": {"timeout": 2.0, "terminator": "\r\n", "ok": "OK", "error": "ERROR", "echo": True}, - "types": {"timeout": "number", "echo": bool}, - "allow_extra": False, - }, - ) - - def execute(self, 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") - 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) - - -_TASK = AtCommandTask() - - -def run(task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any: - return _TASK.run(task, channels, logger) diff --git a/actions/runtime/runtime_protocol_modbus_actions.py b/actions/runtime/runtime_protocol_modbus_actions.py deleted file mode 100644 index ba94f69..0000000 --- a/actions/runtime/runtime_protocol_modbus_actions.py +++ /dev/null @@ -1,79 +0,0 @@ -from __future__ import annotations - -from typing import Any, Dict - -from actions.runtime.base import RuntimeTaskBase -from protocols.registry import ProtocolRegistry - -_MAP = { - "rtu": "modbus_rtu", - "ascii": "modbus_ascii", - "tcp": "modbus_tcp", -} - - -class ModbusRequestTask(RuntimeTaskBase): - def __init__(self) -> None: - super().__init__( - name="modbus_request", - schema={ - "required": ["protocol", "channel", "function", "address"], - "optional": { - "quantity": 1, - "values": None, - "unit_id": 1, - "timeout": None, - "retries": 3, - }, - "types": {"quantity": "number", "unit_id": "number", "timeout": "number", "retries": "number"}, - "allow_extra": False, - }, - ) - - def execute(self, 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, - ) - - -_TASK = ModbusRequestTask() - - -def run(task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any: - return _TASK.run(task, channels, logger) diff --git a/actions/runtime/runtime_protocol_scpi_actions.py b/actions/runtime/runtime_protocol_scpi_actions.py deleted file mode 100644 index 15399c9..0000000 --- a/actions/runtime/runtime_protocol_scpi_actions.py +++ /dev/null @@ -1,48 +0,0 @@ -from __future__ import annotations - -from typing import Any, Dict - -from actions.runtime.base import RuntimeTaskBase -from protocols.registry import ProtocolRegistry - - -class ScpiCommandTask(RuntimeTaskBase): - def __init__(self) -> None: - super().__init__( - name="scpi_command", - schema={ - "aliases": {"command": "cmd"}, - "required": ["channel", "cmd"], - "optional": {"timeout": 2.0, "terminator": "\n", "expect_response": None, "strip": True}, - "types": {"timeout": "number", "strip": bool}, - "allow_extra": False, - }, - ) - - def execute(self, 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") - 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, - ) - - -_TASK = ScpiCommandTask() - - -def run(task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any: - return _TASK.run(task, channels, logger) diff --git a/actions/runtime/runtime_protocol_xmodem_actions.py b/actions/runtime/runtime_protocol_xmodem_actions.py deleted file mode 100644 index c267af6..0000000 --- a/actions/runtime/runtime_protocol_xmodem_actions.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -from typing import Any, Dict - -from actions.runtime.base import RuntimeTaskBase -from protocols.registry import ProtocolRegistry - - -class XmodemSendTask(RuntimeTaskBase): - def __init__(self) -> None: - super().__init__( - name="xmodem_send", - schema={ - "aliases": {"path": "file"}, - "required": ["channel", "file"], - "optional": {"retries": 10, "start_timeout": 10.0}, - "types": {"retries": "number", "start_timeout": "number"}, - "allow_extra": False, - }, - ) - - def execute(self, 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") - 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) - - -_TASK = XmodemSendTask() - - -def run(task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any: - return _TASK.run(task, channels, logger) diff --git a/actions/runtime/runtime_protocol_ymodem_actions.py b/actions/runtime/runtime_protocol_ymodem_actions.py deleted file mode 100644 index 371ef8e..0000000 --- a/actions/runtime/runtime_protocol_ymodem_actions.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -from typing import Any, Dict - -from actions.runtime.base import RuntimeTaskBase -from protocols.registry import ProtocolRegistry - - -class YmodemSendTask(RuntimeTaskBase): - def __init__(self) -> None: - super().__init__( - name="ymodem_send", - schema={ - "aliases": {"path": "file"}, - "required": ["channel", "file"], - "optional": {"retries": 10, "start_timeout": 10.0}, - "types": {"retries": "number", "start_timeout": "number"}, - "allow_extra": False, - }, - ) - - def execute(self, 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") - 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) - - -_TASK = YmodemSendTask() - - -def run(task: Dict[str, Any], channels: Dict[str, Any], logger) -> Any: - return _TASK.run(task, channels, logger) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 9ae0a1d..60be31e 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -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 协议驱动(`protocols/modbus_*.py`),可在 DSL 动作中调用(已注册 `modbus_read/modbus_write`)。 ## 12. 事件系统(Events) - 来源:通道 `read_event`(UART/TCP 读取到的字节,默认字符;无法解码则 HEX 字符串)。 diff --git a/docs/USER_GUIDE_EN.md b/docs/USER_GUIDE_EN.md index 16573c2..cdc14d2 100644 --- a/docs/USER_GUIDE_EN.md +++ b/docs/USER_GUIDE_EN.md @@ -187,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 `protocols/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). diff --git a/main_runtime.py b/main_runtime.py deleted file mode 100644 index 0cb1bc2..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.runtime import runtime_protocol_at_actions, runtime_protocol_modbus_actions, runtime_protocol_scpi_actions, runtime_protocol_xmodem_actions, runtime_protocol_ymodem_actions -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": runtime_protocol_at_actions.run, - "modbus_request": runtime_protocol_modbus_actions.run, - "scpi_command": runtime_protocol_scpi_actions.run, - "xmodem_send": runtime_protocol_xmodem_actions.run, - "ymodem_send": runtime_protocol_ymodem_actions.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()) From 6015fd7b18b176342ead0a2d033d2a423b0dd7e5 Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Fri, 6 Feb 2026 17:57:43 +0800 Subject: [PATCH 07/10] refactor(structure): separate infra and dsl runtime layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更: - 新增 infra 通用/通信/协议基础设施目录并迁移相关模块 - 将 DSL 运行时整合到 dsl_runtime(engine/lang/actions) - 更新全仓库导入路径与文档引用 - 清理旧 actions/runtime/dsl 目录残留 - 影响: - 模块路径全面调整 - 兼容性/行为变化: - 仅路径/导入调整,行为不变 - 依赖/环境: - 无新增依赖 - 验证: - 未运行 [English] - Changes: - Add infra common/comm/protocol directories and move modules - Consolidate DSL runtime under dsl_runtime (engine/lang/actions) - Update imports and docs references - Clean up legacy directories - Impact: - Module paths updated across the repo - Compatibility/Behavior changes: - Path/import changes only - Dependencies/Environment: - No new dependencies - Verification: - Not run Refs: - --- actions/__init__.py | 1 - core/packet_engine.py | 4 ++-- core/plugin_manager.py | 2 +- desktop/script_runner_qt.py | 20 +++++++++---------- desktop/web_bridge.py | 4 ++-- docs/USER_GUIDE.md | 10 +++++----- docs/USER_GUIDE_EN.md | 12 +++++------ dsl_main.py | 2 +- dsl_runtime/__init__.py | 1 + .../dsl => dsl_runtime/actions}/__init__.py | 0 {actions/dsl => dsl_runtime/actions}/base.py | 0 .../actions}/chart_bridge.py | 0 .../actions}/dsl_builtin_actions.py | 6 +++--- .../actions}/dsl_chart_actions.py | 6 +++--- .../actions}/dsl_data_actions.py | 6 +++--- .../actions}/dsl_protocol_actions.py | 6 +++--- .../actions}/dsl_protocol_schema_actions.py | 6 +++--- .../actions}/dsl_record_actions.py | 6 +++--- {actions => dsl_runtime/actions}/registry.py | 2 +- dsl_runtime/engine/__init__.py | 1 + {runtime => dsl_runtime/engine}/channels.py | 0 .../engine}/chart_runtime.py | 0 {runtime => dsl_runtime/engine}/context.py | 6 +++--- .../engine}/experiment_recorder.py | 0 {runtime => dsl_runtime/engine}/runner.py | 20 +++++++++---------- dsl_runtime/lang/__init__.py | 1 + {dsl => dsl_runtime/lang}/ast_nodes.py | 0 {dsl => dsl_runtime/lang}/executor.py | 6 +++--- {dsl => dsl_runtime/lang}/expression.py | 0 {dsl => dsl_runtime/lang}/parser.py | 2 +- {dsl => dsl_runtime/lang}/state_machine.py | 2 +- infra/__init__.py | 1 + infra/comm/__init__.py | 1 + {core => infra/comm}/communication_manager.py | 6 +++--- {core => infra/comm}/serial_manager.py | 2 +- {core => infra/comm}/tcp_session.py | 2 +- infra/common/__init__.py | 1 + {core => infra/common}/event_bus.py | 0 infra/protocol/__init__.py | 4 ++++ {protocols => infra/protocol}/at.py | 4 ++-- {protocols => infra/protocol}/base.py | 0 {protocols => infra/protocol}/modbus_ascii.py | 4 ++-- {protocols => infra/protocol}/modbus_base.py | 2 +- {protocols => infra/protocol}/modbus_rtu.py | 4 ++-- {protocols => infra/protocol}/modbus_tcp.py | 4 ++-- {core => infra/protocol}/protocol_loader.py | 2 +- {protocols => infra/protocol}/registry.py | 2 +- .../protocol}/schema_runtime.py | 0 {protocols => infra/protocol}/scpi.py | 4 ++-- {protocols => infra/protocol}/xmodem.py | 4 ++-- {protocols => infra/protocol}/ymodem.py | 4 ++-- main_web.py | 6 +++--- protocols/__init__.py | 4 ---- 53 files changed, 99 insertions(+), 94 deletions(-) delete mode 100644 actions/__init__.py create mode 100644 dsl_runtime/__init__.py rename {actions/dsl => dsl_runtime/actions}/__init__.py (100%) rename {actions/dsl => dsl_runtime/actions}/base.py (100%) rename {actions => dsl_runtime/actions}/chart_bridge.py (100%) rename {actions/dsl => dsl_runtime/actions}/dsl_builtin_actions.py (97%) rename {actions/dsl => dsl_runtime/actions}/dsl_chart_actions.py (94%) rename {actions/dsl => dsl_runtime/actions}/dsl_data_actions.py (97%) rename {actions/dsl => dsl_runtime/actions}/dsl_protocol_actions.py (97%) rename {actions/dsl => dsl_runtime/actions}/dsl_protocol_schema_actions.py (94%) rename {actions/dsl => dsl_runtime/actions}/dsl_record_actions.py (94%) rename {actions => dsl_runtime/actions}/registry.py (91%) create mode 100644 dsl_runtime/engine/__init__.py rename {runtime => dsl_runtime/engine}/channels.py (100%) rename {runtime => dsl_runtime/engine}/chart_runtime.py (100%) rename {runtime => dsl_runtime/engine}/context.py (97%) rename {runtime => dsl_runtime/engine}/experiment_recorder.py (100%) rename {runtime => dsl_runtime/engine}/runner.py (59%) create mode 100644 dsl_runtime/lang/__init__.py rename {dsl => dsl_runtime/lang}/ast_nodes.py (100%) rename {dsl => dsl_runtime/lang}/executor.py (94%) rename {dsl => dsl_runtime/lang}/expression.py (100%) rename {dsl => dsl_runtime/lang}/parser.py (99%) rename {dsl => dsl_runtime/lang}/state_machine.py (79%) create mode 100644 infra/__init__.py create mode 100644 infra/comm/__init__.py rename {core => infra/comm}/communication_manager.py (96%) rename {core => infra/comm}/serial_manager.py (99%) rename {core => infra/comm}/tcp_session.py (98%) create mode 100644 infra/common/__init__.py rename {core => infra/common}/event_bus.py (100%) create mode 100644 infra/protocol/__init__.py rename {protocols => infra/protocol}/at.py (95%) rename {protocols => infra/protocol}/base.py (100%) rename {protocols => infra/protocol}/modbus_ascii.py (96%) rename {protocols => infra/protocol}/modbus_base.py (99%) rename {protocols => infra/protocol}/modbus_rtu.py (96%) rename {protocols => infra/protocol}/modbus_tcp.py (95%) rename {core => infra/protocol}/protocol_loader.py (99%) rename {protocols => infra/protocol}/registry.py (93%) rename {protocols => infra/protocol}/schema_runtime.py (100%) rename {protocols => infra/protocol}/scpi.py (97%) rename {protocols => infra/protocol}/xmodem.py (96%) rename {protocols => infra/protocol}/ymodem.py (97%) delete mode 100644 protocols/__init__.py 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/core/packet_engine.py b/core/packet_engine.py index 59da002..ecf694c 100644 --- a/core/packet_engine.py +++ b/core/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/core/plugin_manager.py index 0e990fd..e7e66be 100644 --- a/core/plugin_manager.py +++ b/core/plugin_manager.py @@ -10,7 +10,7 @@ from types import ModuleType from typing import Dict, List, Optional -from core.event_bus import EventBus +from infra.common.event_bus import EventBus from utils.path_utils import resolve_resource_path diff --git a/desktop/script_runner_qt.py b/desktop/script_runner_qt.py index 73f40c6..cb08b42 100644 --- a/desktop/script_runner_qt.py +++ b/desktop/script_runner_qt.py @@ -8,16 +8,16 @@ from PySide6.QtCore import QThread, Signal -from actions.dsl.dsl_builtin_actions import register_builtin_actions -from actions.dsl.dsl_protocol_actions import register_protocol_actions -from actions.dsl.dsl_protocol_schema_actions import register_schema_protocol_actions -from actions.dsl.dsl_chart_actions import register_chart_actions -from actions.dsl.dsl_record_actions import register_record_actions -from actions.dsl.dsl_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/desktop/web_bridge.py index 7ffce9d..6de3bd4 100644 --- a/desktop/web_bridge.py +++ b/desktop/web_bridge.py @@ -19,8 +19,8 @@ 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 infra.protocol.registry import ProtocolRegistry +import infra.protocol as protocols_pkg from desktop.script_runner_qt import ScriptRunnerQt diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 60be31e..f4b6e64 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -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`),可在 DSL 动作中调用(已注册 `modbus_read/modbus_write`)。 +说明:仓库中已实现 Modbus 协议驱动(`infra/protocol/modbus_*.py`),可在 DSL 动作中调用(已注册 `modbus_read/modbus_write`)。 ## 12. 事件系统(Events) - 来源:通道 `read_event`(UART/TCP 读取到的字节,默认字符;无法解码则 HEX 字符串)。 @@ -267,8 +267,8 @@ state_machine: ## 15. 扩展指南 - 添加新动作: ```python - from actions.dsl.base import DslActionBase - from actions.registry import ActionRegistry + from dsl_runtime.actions.base import DslActionBase + from dsl_runtime.actions.registry import ActionRegistry class MyAction(DslActionBase): def __init__(self) -> None: @@ -289,9 +289,9 @@ state_machine: 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 cdc14d2..ac6ce21 100644 --- a/docs/USER_GUIDE_EN.md +++ b/docs/USER_GUIDE_EN.md @@ -160,8 +160,8 @@ 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.dsl.base import DslActionBase - from actions.registry import ActionRegistry + from dsl_runtime.actions.base import DslActionBase + from dsl_runtime.actions.registry import ActionRegistry class MyAction(DslActionBase): def __init__(self) -> None: @@ -187,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 available via DSL actions (`modbus_read/modbus_write`). +- 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). @@ -336,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", 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_main.py b/dsl_main.py index 43d6dca..399ab86 100644 --- a/dsl_main.py +++ b/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/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/actions/dsl/__init__.py b/dsl_runtime/actions/__init__.py similarity index 100% rename from actions/dsl/__init__.py rename to dsl_runtime/actions/__init__.py diff --git a/actions/dsl/base.py b/dsl_runtime/actions/base.py similarity index 100% rename from actions/dsl/base.py rename to dsl_runtime/actions/base.py 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/actions/dsl/dsl_builtin_actions.py b/dsl_runtime/actions/dsl_builtin_actions.py similarity index 97% rename from actions/dsl/dsl_builtin_actions.py rename to dsl_runtime/actions/dsl_builtin_actions.py index 6abc025..e10bbad 100644 --- a/actions/dsl/dsl_builtin_actions.py +++ b/dsl_runtime/actions/dsl_builtin_actions.py @@ -3,9 +3,9 @@ import time from typing import Any, Dict -from actions.dsl.base import DslActionBase -from actions.registry import ActionRegistry -from dsl.expression import eval_expr +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): diff --git a/actions/dsl/dsl_chart_actions.py b/dsl_runtime/actions/dsl_chart_actions.py similarity index 94% rename from actions/dsl/dsl_chart_actions.py rename to dsl_runtime/actions/dsl_chart_actions.py index 6e2d6f7..f030f8e 100644 --- a/actions/dsl/dsl_chart_actions.py +++ b/dsl_runtime/actions/dsl_chart_actions.py @@ -3,9 +3,9 @@ import time from typing import Any, Dict -from actions.dsl.base import DslActionBase -from actions.chart_bridge import chart_bridge -from actions.registry import ActionRegistry +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: diff --git a/actions/dsl/dsl_data_actions.py b/dsl_runtime/actions/dsl_data_actions.py similarity index 97% rename from actions/dsl/dsl_data_actions.py rename to dsl_runtime/actions/dsl_data_actions.py index 37f8bca..b133382 100644 --- a/actions/dsl/dsl_data_actions.py +++ b/dsl_runtime/actions/dsl_data_actions.py @@ -2,9 +2,9 @@ from typing import Any, Dict, Iterable, List, Tuple -from actions.dsl.base import DslActionBase -from actions.registry import ActionRegistry -from dsl.expression import eval_expr +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]: diff --git a/actions/dsl/dsl_protocol_actions.py b/dsl_runtime/actions/dsl_protocol_actions.py similarity index 97% rename from actions/dsl/dsl_protocol_actions.py rename to dsl_runtime/actions/dsl_protocol_actions.py index 401c2b8..84b1ebe 100644 --- a/actions/dsl/dsl_protocol_actions.py +++ b/dsl_runtime/actions/dsl_protocol_actions.py @@ -2,9 +2,9 @@ from typing import Dict -from actions.dsl.base import DslActionBase -from actions.registry import ActionRegistry -from protocols.registry import ProtocolRegistry +from dsl_runtime.actions.base import DslActionBase +from dsl_runtime.actions.registry import ActionRegistry +from infra.protocol.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 diff --git a/actions/dsl/dsl_protocol_schema_actions.py b/dsl_runtime/actions/dsl_protocol_schema_actions.py similarity index 94% rename from actions/dsl/dsl_protocol_schema_actions.py rename to dsl_runtime/actions/dsl_protocol_schema_actions.py index db721da..3d5b10f 100644 --- a/actions/dsl/dsl_protocol_schema_actions.py +++ b/dsl_runtime/actions/dsl_protocol_schema_actions.py @@ -3,9 +3,9 @@ from functools import lru_cache from typing import Any, Dict -from actions.dsl.base import DslActionBase -from actions.registry import ActionRegistry -from protocols.schema_runtime import ProtocolSchema +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) diff --git a/actions/dsl/dsl_record_actions.py b/dsl_runtime/actions/dsl_record_actions.py similarity index 94% rename from actions/dsl/dsl_record_actions.py rename to dsl_runtime/actions/dsl_record_actions.py index bb88a3b..42cf11d 100644 --- a/actions/dsl/dsl_record_actions.py +++ b/dsl_runtime/actions/dsl_record_actions.py @@ -2,9 +2,9 @@ from typing import Any, Dict -from actions.dsl.base import DslActionBase -from actions.registry import ActionRegistry -from runtime.experiment_recorder import ExperimentRecorder +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: diff --git a/actions/registry.py b/dsl_runtime/actions/registry.py similarity index 91% rename from actions/registry.py rename to dsl_runtime/actions/registry.py index b66f9e8..e0aa402 100644 --- a/actions/registry.py +++ b/dsl_runtime/actions/registry.py @@ -2,7 +2,7 @@ from typing import Dict -from actions.dsl.base import DslActionBase +from dsl_runtime.actions.base import DslActionBase class ActionRegistry: 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 97% rename from runtime/context.py rename to dsl_runtime/engine/context.py index 95127a4..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: 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 34b5ee9..53cb945 100644 --- a/runtime/runner.py +++ b/dsl_runtime/engine/runner.py @@ -2,16 +2,16 @@ import logging -from actions.dsl.dsl_builtin_actions import register_builtin_actions -from actions.dsl.dsl_protocol_actions import register_protocol_actions -from actions.dsl.dsl_protocol_schema_actions import register_schema_protocol_actions -from actions.dsl.dsl_chart_actions import register_chart_actions -from actions.dsl.dsl_record_actions import register_record_actions -from actions.dsl.dsl_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/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 96% rename from protocols/modbus_ascii.py rename to infra/protocol/modbus_ascii.py index 91c53d0..5f1e4bc 100644 --- a/protocols/modbus_ascii.py +++ b/infra/protocol/modbus_ascii.py @@ -2,8 +2,8 @@ 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 from utils.lrc import lrc_modbus_ascii 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 96% rename from protocols/modbus_rtu.py rename to infra/protocol/modbus_rtu.py index 781eb9e..9582a4e 100644 --- a/protocols/modbus_rtu.py +++ b/infra/protocol/modbus_rtu.py @@ -3,8 +3,8 @@ import time from typing import Optional -from protocols.modbus_base import ModbusBase -from protocols.registry import ProtocolRegistry +from infra.protocol.modbus_base import ModbusBase +from infra.protocol.registry import ProtocolRegistry from utils.crc16 import crc16_modbus 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 99% rename from core/protocol_loader.py rename to infra/protocol/protocol_loader.py index 2d69b6e..92fba64 100644 --- a/core/protocol_loader.py +++ b/infra/protocol/protocol_loader.py @@ -13,7 +13,7 @@ import yaml -from core.event_bus import EventBus +from infra.common.event_bus import EventBus from utils.path_utils import resolve_resource_path 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 100% rename from protocols/schema_runtime.py rename to infra/protocol/schema_runtime.py 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 96% rename from protocols/xmodem.py rename to infra/protocol/xmodem.py index f72867e..eeca579 100644 --- a/protocols/xmodem.py +++ b/infra/protocol/xmodem.py @@ -3,8 +3,8 @@ import time from pathlib import Path -from protocols.base import BaseProtocol -from protocols.registry import ProtocolRegistry +from infra.protocol.base import BaseProtocol +from infra.protocol.registry import ProtocolRegistry from utils.crc16 import crc16_xmodem diff --git a/protocols/ymodem.py b/infra/protocol/ymodem.py similarity index 97% rename from protocols/ymodem.py rename to infra/protocol/ymodem.py index e9123be..f31e661 100644 --- a/protocols/ymodem.py +++ b/infra/protocol/ymodem.py @@ -3,8 +3,8 @@ import time from pathlib import Path -from protocols.base import BaseProtocol -from protocols.registry import ProtocolRegistry +from infra.protocol.base import BaseProtocol +from infra.protocol.registry import ProtocolRegistry from utils.crc16 import crc16_xmodem diff --git a/main_web.py b/main_web.py index 626c6b1..488c3b7 100644 --- a/main_web.py +++ b/main_web.py @@ -17,11 +17,11 @@ 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 infra.comm.communication_manager import CommunicationManager +from infra.common.event_bus import EventBus from core.packet_engine import PacketAnalysisEngine from core.plugin_manager import PluginManager -from core.protocol_loader import ProtocolLoader +from infra.protocol.protocol_loader import ProtocolLoader from desktop.web_window import WebWindow 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 - -# 具体协议实现会在各自模块内完成注册 From 4e8d231f8605ec193261af76de68b602812c10aa Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Fri, 6 Feb 2026 18:09:08 +0800 Subject: [PATCH 08/10] refactor(structure): introduce app/ui layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更: - 新增 app/ 与 ui/ 目录并迁移入口与界面资源 - main_web/dsl_main 移动到 app/,desktop/frontend/assets 归入 ui/ - 更新导入路径与构建脚本配置 - 更新 README 与用户文档中的路径说明 - 影响: - 入口与 UI 相关路径调整 - 兼容性/行为变化: - 仅路径/导入调整,行为不变 - 依赖/环境: - 无新增依赖 - 验证: - 未运行 [English] - Changes: - Add app/ and ui/ directories and move entry/UI resources - Move main_web/dsl_main into app/, desktop/frontend/assets into ui/ - Update imports and build scripts - Update README/docs paths - Impact: - Entry and UI paths updated - Compatibility/Behavior changes: - Path/import changes only - Dependencies/Environment: - No new dependencies - Verification: - Not run Refs: - --- README.md | 2 +- README_EN.md | 2 +- app/__init__.py | 1 + dsl_main.py => app/dsl_main.py | 0 main_web.py => app/main_web.py | 12 ++++++------ {core => app}/packet_engine.py | 0 {core => app}/plugin_manager.py | 0 docs/USER_GUIDE.md | 2 +- docs/USER_GUIDE_EN.md | 2 +- main.py | 2 +- scripts/build_windows.ps1 | 8 ++++---- scripts/generate_icon.ps1 | 2 +- ui/__init__.py | 1 + {assets => ui/assets}/icons/logo.png | Bin {assets => ui/assets}/icons/logo.svg | 0 {assets => ui/assets}/web/index.html | 0 ui/desktop/__init__.py | 1 + {desktop => ui/desktop}/script_runner_qt.py | 0 {desktop => ui/desktop}/web_bridge.py | 2 +- {desktop => ui/desktop}/web_window.py | 4 ++-- {desktop => ui/desktop}/widgets/__init__.py | 0 {desktop => ui/desktop}/win_snap.py | 0 {frontend => ui/frontend}/.gitignore | 0 {frontend => ui/frontend}/.vscode/extensions.json | 0 {frontend => ui/frontend}/README.md | 0 {frontend => ui/frontend}/config/ui_settings.json | 4 ++-- {frontend => ui/frontend}/index.html | 0 {frontend => ui/frontend}/package-lock.json | 0 {frontend => ui/frontend}/package.json | 0 {frontend => ui/frontend}/postcss.config.js | 0 {frontend => ui/frontend}/public/vite.svg | 0 {frontend => ui/frontend}/src/App.vue | 0 .../src/assets/fonts/MaterialSymbolsOutlined.ttf | Bin {frontend => ui/frontend}/src/assets/vue.svg | 0 .../frontend}/src/components/DropdownSelect.vue | 0 .../frontend}/src/components/HelloWorld.vue | 0 .../frontend}/src/components/LogStream.vue | 0 .../frontend}/src/components/ManualView.vue | 0 .../frontend}/src/components/ProxyMonitorView.vue | 0 .../frontend}/src/components/ScriptsView.vue | 0 .../frontend}/src/components/YamlUiLab.vue | 0 .../frontend}/src/components/ui-kit/AppShell.vue | 0 .../src/components/ui-kit/EventLogPanel.vue | 0 .../src/components/ui-kit/InspectorJSON.vue | 0 .../frontend}/src/components/ui-kit/PanelCard.vue | 0 .../src/components/ui-kit/SplitContainer.vue | 0 .../frontend}/src/components/ui-kit/TopBar.vue | 0 .../components/ui-kit/actions/BatchActionGroup.vue | 0 .../ui-kit/actions/ConfirmDangerButton.vue | 0 .../components/ui-kit/actions/DropdownAction.vue | 0 .../src/components/ui-kit/actions/GhostButton.vue | 0 .../src/components/ui-kit/actions/PrimaryButton.vue | 0 .../components/ui-kit/actions/SecondaryButton.vue | 0 .../src/components/ui-kit/inputs/CheckboxGroup.vue | 0 .../src/components/ui-kit/inputs/ColorInput.vue | 0 .../src/components/ui-kit/inputs/DateTimeInput.vue | 0 .../components/ui-kit/inputs/KeybindRecorder.vue | 0 .../components/ui-kit/inputs/MultiSelectChips.vue | 0 .../src/components/ui-kit/inputs/NumberInput.vue | 0 .../src/components/ui-kit/inputs/PathPicker.vue | 0 .../src/components/ui-kit/inputs/RadioGroup.vue | 0 .../src/components/ui-kit/inputs/SelectInput.vue | 0 .../src/components/ui-kit/inputs/SliderInput.vue | 0 .../src/components/ui-kit/inputs/SwitchInput.vue | 0 .../src/components/ui-kit/inputs/TextInput.vue | 0 .../src/components/ui-kit/inputs/TextareaInput.vue | 0 {frontend => ui/frontend}/src/main.js | 0 {frontend => ui/frontend}/src/stores/uiRuntime.ts | 0 {frontend => ui/frontend}/src/style.css | 0 {frontend => ui/frontend}/src/ui/LayoutRenderer.vue | 0 {frontend => ui/frontend}/src/ui/UnknownWidget.vue | 0 {frontend => ui/frontend}/src/ui/registry.ts | 0 {frontend => ui/frontend}/src/ui/schema.ts | 0 {frontend => ui/frontend}/src/ui/yaml.ts | 0 {frontend => ui/frontend}/tailwind.config.js | 0 {frontend => ui/frontend}/vite.config.js | 0 76 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 app/__init__.py rename dsl_main.py => app/dsl_main.py (100%) rename main_web.py => app/main_web.py (94%) rename {core => app}/packet_engine.py (100%) rename {core => app}/plugin_manager.py (100%) create mode 100644 ui/__init__.py rename {assets => ui/assets}/icons/logo.png (100%) rename {assets => ui/assets}/icons/logo.svg (100%) rename {assets => ui/assets}/web/index.html (100%) create mode 100644 ui/desktop/__init__.py rename {desktop => ui/desktop}/script_runner_qt.py (100%) rename {desktop => ui/desktop}/web_bridge.py (99%) rename {desktop => ui/desktop}/web_window.py (99%) rename {desktop => ui/desktop}/widgets/__init__.py (100%) rename {desktop => ui/desktop}/win_snap.py (100%) rename {frontend => ui/frontend}/.gitignore (100%) rename {frontend => ui/frontend}/.vscode/extensions.json (100%) rename {frontend => ui/frontend}/README.md (100%) rename {frontend => ui/frontend}/config/ui_settings.json (78%) rename {frontend => ui/frontend}/index.html (100%) rename {frontend => ui/frontend}/package-lock.json (100%) rename {frontend => ui/frontend}/package.json (100%) rename {frontend => ui/frontend}/postcss.config.js (100%) rename {frontend => ui/frontend}/public/vite.svg (100%) rename {frontend => ui/frontend}/src/App.vue (100%) rename {frontend => ui/frontend}/src/assets/fonts/MaterialSymbolsOutlined.ttf (100%) rename {frontend => ui/frontend}/src/assets/vue.svg (100%) rename {frontend => ui/frontend}/src/components/DropdownSelect.vue (100%) rename {frontend => ui/frontend}/src/components/HelloWorld.vue (100%) rename {frontend => ui/frontend}/src/components/LogStream.vue (100%) rename {frontend => ui/frontend}/src/components/ManualView.vue (100%) rename {frontend => ui/frontend}/src/components/ProxyMonitorView.vue (100%) rename {frontend => ui/frontend}/src/components/ScriptsView.vue (100%) rename {frontend => ui/frontend}/src/components/YamlUiLab.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/AppShell.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/EventLogPanel.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/InspectorJSON.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/PanelCard.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/SplitContainer.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/TopBar.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/actions/BatchActionGroup.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/actions/ConfirmDangerButton.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/actions/DropdownAction.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/actions/GhostButton.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/actions/PrimaryButton.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/actions/SecondaryButton.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/CheckboxGroup.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/ColorInput.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/DateTimeInput.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/KeybindRecorder.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/MultiSelectChips.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/NumberInput.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/PathPicker.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/RadioGroup.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/SelectInput.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/SliderInput.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/SwitchInput.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/TextInput.vue (100%) rename {frontend => ui/frontend}/src/components/ui-kit/inputs/TextareaInput.vue (100%) rename {frontend => ui/frontend}/src/main.js (100%) rename {frontend => ui/frontend}/src/stores/uiRuntime.ts (100%) rename {frontend => ui/frontend}/src/style.css (100%) rename {frontend => ui/frontend}/src/ui/LayoutRenderer.vue (100%) rename {frontend => ui/frontend}/src/ui/UnknownWidget.vue (100%) rename {frontend => ui/frontend}/src/ui/registry.ts (100%) rename {frontend => ui/frontend}/src/ui/schema.ts (100%) rename {frontend => ui/frontend}/src/ui/yaml.ts (100%) rename {frontend => ui/frontend}/tailwind.config.js (100%) rename {frontend => ui/frontend}/vite.config.js (100%) diff --git a/README.md b/README.md index 4e22e03..23ef677 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ 

- ProtoFlow logo + ProtoFlow logo

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 @@ 

- ProtoFlow logo + ProtoFlow logo

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 100% rename from dsl_main.py rename to app/dsl_main.py diff --git a/main_web.py b/app/main_web.py similarity index 94% rename from main_web.py rename to app/main_web.py index 488c3b7..e2e8ee2 100644 --- a/main_web.py +++ b/app/main_web.py @@ -19,10 +19,10 @@ from infra.comm.communication_manager import CommunicationManager from infra.common.event_bus import EventBus -from core.packet_engine import PacketAnalysisEngine -from core.plugin_manager import PluginManager +from app.packet_engine import PacketAnalysisEngine +from app.plugin_manager import PluginManager from infra.protocol.protocol_loader import ProtocolLoader -from desktop.web_window import WebWindow +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 100% rename from core/packet_engine.py rename to app/packet_engine.py diff --git a/core/plugin_manager.py b/app/plugin_manager.py similarity index 100% rename from core/plugin_manager.py rename to app/plugin_manager.py diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index f4b6e64..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),状态机执行的事件流;动作可产生下行数据,通道可回传事件。 diff --git a/docs/USER_GUIDE_EN.md b/docs/USER_GUIDE_EN.md index ac6ce21..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. 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/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/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 100% rename from desktop/script_runner_qt.py rename to ui/desktop/script_runner_qt.py 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 6de3bd4..f32cbf2 100644 --- a/desktop/web_bridge.py +++ b/ui/desktop/web_bridge.py @@ -21,7 +21,7 @@ from infra.protocol.registry import ProtocolRegistry import infra.protocol as protocols_pkg -from desktop.script_runner_qt import ScriptRunnerQt +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 From 50052726070283d56675a4e5dfd511d1ad78db0e Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Fri, 6 Feb 2026 18:15:59 +0800 Subject: [PATCH 09/10] refactor(infra): move utils into infra/common and remove legacy src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更: - 将 utils 迁移到 infra/common/utils - 更新相关模块导入路径 - 删除旧 src/com_tool.py - 清理缓存目录与虚拟环境 - 影响: - 工具模块路径调整 - 兼容性/行为变化: - 仅路径调整,行为不变 - 依赖/环境: - 无新增依赖 - 验证: - 未运行 [English] - Changes: - Move utils into infra/common/utils - Update imports - Remove legacy src/com_tool.py - Clean cache/venv directories - Impact: - Utils module paths updated - Compatibility/Behavior changes: - Path/import changes only - Dependencies/Environment: - No new dependencies - Verification: - Not run Refs: - --- app/plugin_manager.py | 2 +- dsl_runtime/actions/dsl_protocol_actions.py | 4 +- {utils => infra/common/utils}/__init__.py | 0 {utils => infra/common/utils}/crc16.py | 0 {utils => infra/common/utils}/file_utils.py | 0 {utils => infra/common/utils}/lrc.py | 0 {utils => infra/common/utils}/path_utils.py | 0 infra/protocol/modbus_ascii.py | 2 +- infra/protocol/modbus_rtu.py | 2 +- infra/protocol/protocol_loader.py | 2 +- infra/protocol/schema_runtime.py | 4 +- infra/protocol/xmodem.py | 2 +- infra/protocol/ymodem.py | 2 +- src/com_tool.py | 92 --------------------- 14 files changed, 10 insertions(+), 102 deletions(-) rename {utils => infra/common/utils}/__init__.py (100%) rename {utils => infra/common/utils}/crc16.py (100%) rename {utils => infra/common/utils}/file_utils.py (100%) rename {utils => infra/common/utils}/lrc.py (100%) rename {utils => infra/common/utils}/path_utils.py (100%) delete mode 100644 src/com_tool.py diff --git a/app/plugin_manager.py b/app/plugin_manager.py index e7e66be..b77e20e 100644 --- a/app/plugin_manager.py +++ b/app/plugin_manager.py @@ -11,7 +11,7 @@ from typing import Dict, List, Optional from infra.common.event_bus import EventBus -from utils.path_utils import resolve_resource_path +from infra.common.utils.path_utils import resolve_resource_path class PluginManager: diff --git a/dsl_runtime/actions/dsl_protocol_actions.py b/dsl_runtime/actions/dsl_protocol_actions.py index 84b1ebe..da96af7 100644 --- a/dsl_runtime/actions/dsl_protocol_actions.py +++ b/dsl_runtime/actions/dsl_protocol_actions.py @@ -6,8 +6,8 @@ from dsl_runtime.actions.registry import ActionRegistry from infra.protocol.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 infra.common.utils.crc16 import crc16_xmodem +from infra.common.utils.file_utils import get_file_meta, read_block class XMODEMPacketBuilder: 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/modbus_ascii.py b/infra/protocol/modbus_ascii.py index 5f1e4bc..bc93d67 100644 --- a/infra/protocol/modbus_ascii.py +++ b/infra/protocol/modbus_ascii.py @@ -4,7 +4,7 @@ from infra.protocol.modbus_base import ModbusBase from infra.protocol.registry import ProtocolRegistry -from utils.lrc import lrc_modbus_ascii +from infra.common.utils.lrc import lrc_modbus_ascii class ModbusASCII(ModbusBase): diff --git a/infra/protocol/modbus_rtu.py b/infra/protocol/modbus_rtu.py index 9582a4e..711ddf2 100644 --- a/infra/protocol/modbus_rtu.py +++ b/infra/protocol/modbus_rtu.py @@ -5,7 +5,7 @@ from infra.protocol.modbus_base import ModbusBase from infra.protocol.registry import ProtocolRegistry -from utils.crc16 import crc16_modbus +from infra.common.utils.crc16 import crc16_modbus class ModbusRTU(ModbusBase): diff --git a/infra/protocol/protocol_loader.py b/infra/protocol/protocol_loader.py index 92fba64..dcac738 100644 --- a/infra/protocol/protocol_loader.py +++ b/infra/protocol/protocol_loader.py @@ -14,7 +14,7 @@ import yaml from infra.common.event_bus import EventBus -from utils.path_utils import resolve_resource_path +from infra.common.utils.path_utils import resolve_resource_path def crc16_modbus(data: bytes) -> int: diff --git a/infra/protocol/schema_runtime.py b/infra/protocol/schema_runtime.py index 4c42d07..f2d4ae2 100644 --- a/infra/protocol/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/infra/protocol/xmodem.py b/infra/protocol/xmodem.py index eeca579..ee57c7b 100644 --- a/infra/protocol/xmodem.py +++ b/infra/protocol/xmodem.py @@ -5,7 +5,7 @@ from infra.protocol.base import BaseProtocol from infra.protocol.registry import ProtocolRegistry -from utils.crc16 import crc16_xmodem +from infra.common.utils.crc16 import crc16_xmodem SOH = 0x01 diff --git a/infra/protocol/ymodem.py b/infra/protocol/ymodem.py index f31e661..0a3ff44 100644 --- a/infra/protocol/ymodem.py +++ b/infra/protocol/ymodem.py @@ -5,7 +5,7 @@ from infra.protocol.base import BaseProtocol from infra.protocol.registry import ProtocolRegistry -from utils.crc16 import crc16_xmodem +from infra.common.utils.crc16 import crc16_xmodem SOH = 0x01 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()) From 28eb132868ccd54cbd3fbc13ee8999831d542fda Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Fri, 6 Feb 2026 18:21:37 +0800 Subject: [PATCH 10/10] fix(dsl): repair registry encoding and protocol import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更: - 修复 dsl_runtime/actions/registry.py 编码导致的语法错误 - 修正 dsl_protocol_actions 对 infra.protocol 的导入 - 影响: - 解除 DSL 动作注册与协议加载的运行错误 - 兼容性/行为变化: - 无 - 依赖/环境: - 无新增依赖 - 验证: - python -c "import dsl_runtime.engine.runner as r; import infra.protocol; import infra.comm.communication_manager; print('ok')" [English] - Changes: - Fix encoding corruption in dsl_runtime/actions/registry.py - Update dsl_protocol_actions to import infra.protocol - Impact: - Remove runtime errors when loading DSL actions/protocols - Compatibility/Behavior changes: - None - Dependencies/Environment: - No new dependencies - Verification: - python -c "import dsl_runtime.engine.runner as r; import infra.protocol; import infra.comm.communication_manager; print('ok')" Refs: - --- dsl_runtime/actions/dsl_protocol_actions.py | 2 +- dsl_runtime/actions/registry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsl_runtime/actions/dsl_protocol_actions.py b/dsl_runtime/actions/dsl_protocol_actions.py index da96af7..0e8947c 100644 --- a/dsl_runtime/actions/dsl_protocol_actions.py +++ b/dsl_runtime/actions/dsl_protocol_actions.py @@ -5,7 +5,7 @@ from dsl_runtime.actions.base import DslActionBase from dsl_runtime.actions.registry import ActionRegistry from infra.protocol.registry import ProtocolRegistry -from protocols import modbus_ascii, modbus_rtu, modbus_tcp # noqa: F401 +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 diff --git a/dsl_runtime/actions/registry.py b/dsl_runtime/actions/registry.py index e0aa402..0e58ee2 100644 --- a/dsl_runtime/actions/registry.py +++ b/dsl_runtime/actions/registry.py @@ -17,5 +17,5 @@ def register(cls, name: str, action: DslActionBase) -> None: @classmethod def get(cls, name: str) -> DslActionBase: if name not in cls.actions: - raise KeyError(f"δע: {name}") + raise KeyError(f"action not registered: {name}") return cls.actions[name]