From 01126a5786c97360d5a6bb8140f5410888fb0432 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Sat, 15 Aug 2026 13:54:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(mtc):=20.mt=20=E8=84=9A=E6=9C=AC=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E5=99=A8=E8=90=BD=E5=9C=B0=20+=20pybot=20=E6=89=AC?= =?UTF-8?q?=E5=A3=B0=E5=99=A8=E9=9D=99=E9=9F=B3=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mtc 新模块:命令式动作脚本(Python DSL)→ .mt 矩阵编译器,非阻塞时间轴 IR(重叠截断/rest 静止值)+ 10ms 栅格化,产物可由 pybot/bot4cj 直接执行;34 测试全绿(含 pybot parser e2e)。 pybot speaker.py:frequency<=0 改为静音节拍(仅 sleep 保时序,不再钳到 min_f 低鸣),对齐仓颉版 sleep 占位语义;mtc Beep 通道 rest=0,段前/段间/段后回落静音,两侧语义闭环。 修复 test_actpred_compatibility 过时断言(99c1bc1 首行分解为重复微步 19→20 帧时漏改,14→15)。 文档三处同步:根 README 回填 memdb/pybot/actpred 并新增 mtc(模块表+端口一览+章节),MODULES/ARCHITECTURE 同步 mtc,新建 mtc/README.md。 --- README.md | 29 +++++ docs/ARCHITECTURE.md | 4 +- docs/MODULES.md | 5 +- mtc/README.md | 127 ++++++++++++++++++++++ mtc/mtc/__init__.py | 14 +++ mtc/mtc/__main__.py | 54 ++++++++++ mtc/mtc/dsl.py | 204 +++++++++++++++++++++++++++++++++++ mtc/mtc/emit.py | 80 ++++++++++++++ mtc/mtc/timeline.py | 108 +++++++++++++++++++ mtc/tests/__init__.py | 0 mtc/tests/conftest.py | 25 +++++ mtc/tests/test_dsl.py | 127 ++++++++++++++++++++++ mtc/tests/test_e2e.py | 64 +++++++++++ mtc/tests/test_emit.py | 97 +++++++++++++++++ mtc/tests/test_timeline.py | 91 ++++++++++++++++ pybot/pybot/hd/speaker.py | 16 ++- pybot/tests/test_recorder.py | 5 +- pybot/tests/test_speaker.py | 82 ++++++++++++++ 18 files changed, 1125 insertions(+), 7 deletions(-) create mode 100644 mtc/README.md create mode 100644 mtc/mtc/__init__.py create mode 100644 mtc/mtc/__main__.py create mode 100644 mtc/mtc/dsl.py create mode 100644 mtc/mtc/emit.py create mode 100644 mtc/mtc/timeline.py create mode 100644 mtc/tests/__init__.py create mode 100644 mtc/tests/conftest.py create mode 100644 mtc/tests/test_dsl.py create mode 100644 mtc/tests/test_e2e.py create mode 100644 mtc/tests/test_emit.py create mode 100644 mtc/tests/test_timeline.py create mode 100644 pybot/tests/test_speaker.py diff --git a/README.md b/README.md index 45d9541..e8b57df 100755 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ | [dnfs](dnfs/) | Python(Flask) | 8006 | ~~树状网络文件系统~~(遗留,已由 dnfs2 平替) | | [db2](db2/) | C++20 | 8005 | 通用数据库服务,基于 pymsg 协议(TCP 长度前缀帧 + JSON)的 SQL-over-TCP,参数化查询防注入;内置集中式日志收集(Logs 表 + 自动清理) | | [db](db/) | Python(pymsg) | 8005 | ~~通用数据库服务~~(遗留,已由 db2 平替) | +| [memdb](memdb/) | C++20 | 8011 | 内存缓存数据库(libmemdb 模板库 + libmsg RPC),TTL 惰性驱逐,9 路由(ping/set/get/del/update/exists/size/show/expire),线程安全 + 优雅退出;暂无业务调用方,预留热点数据/会话缓存 | | [ai_review](ai_review/) | Python(Flask) | 8007 | AI 内容审核服务,DeepSeek LLM + ChromaDB RAG,支持人工反馈纠偏 | | [captcha2](captcha2/) | C++20 | 8009 | 验证码服务(pymsg/TCP),生成图形验证码(5 分钟过期、一次性),经 ca 的 `GET /captcha/image` 代理给浏览器 | | [captcha](captcha/) | Python(pymsg) | 8009 | ~~验证码服务~~(遗留,已由 captcha2 平替) | @@ -77,6 +78,9 @@ | [aibox](aibox/) | Python | 8765 | 独立 AI 智能体系统(DeepSeek + WebSocket),自带技能系统/记忆/任务调度,由 core 拉起但与主干微服务无调用关系 | | [BotOps](BotOps/) | Python | — | aibox 智能体 TUI 前端(Textual),WebSocket 连接 aibox,支持 Markdown 增量流式渲染、思考模式展示、任务独立侧栏、自动重连 | | [bot4cj](bot4cj/) | 仓颉(Cangjie) | — | 机器人硬件脚本解析器,解析 .mt 矩阵脚本通过 I2C 驱动 PCA9685 舵机 / MPU6050 陀螺仪,面向树莓派 Dashun 蜘蛛机器人平台(独立硬件项目) | +| [pybot](pybot/) | Python | — | bot4cj 的 Python 迁移版:.mt 动作脚本解析器(零第三方依赖,与仓颉版逐语义对齐);支持执行 / `--dry-run` / `--record` 录制 actpred episode(含 imu.npy)/ `--loop` 实时预测闭环 | +| [mtc](mtc/) | Python | — | 命令式动作脚本(Python DSL)→ .mt 编译器:非阻塞时间轴 + 10ms 栅格展开,输出可由 pybot/bot4cj 直接执行;零第三方依赖 | +| [actpred](actpred/) | Python(PyTorch) | — | 动作预测模型(图像 + 动作历史 → 下一动作),bot4cj/pybot 的「大脑」配套;两阶段训练(.mt 预训练 → episode 微调),ONNX/Hailo HEF 部署(实验) | | [lci](lci/) | C++ | — | 嵌入式帧缓冲图形界面(LCI),基于 `/dev/fb0` 直接操作显存,无需 X11/Wayland,矢量字体、图片渲染、实时数据可视化,面向树莓派等嵌入式环境 | ### core(核心模块 · 进程托管服务) @@ -171,6 +175,12 @@ - 安全机制:表名白名单(字母数字下划线)、`sqlite3_bind_text` 参数绑定、仅反射已有表无法建删表、RpcServer 异常脱敏。 - 配置全部经 CLI 传递(CLI > 环境变量 > 默认值,原 `db.ini` 迁入 `core/services.yaml` 的 CLI args),不依赖 confs;详见 [db2/README.md](db2/README.md)。 +### memdb(内存缓存数据库) + +- 基于 [libmemdb](libmemdb/)(C++20 header-only 模板库:三索引存储——`std::list` 主存 + `unordered_map` 键索引 + `multiset` TTL 索引,TTL 惰性驱逐)+ [libmsg](libmsg/) RPC 服务,监听 127.0.0.1:8011。 +- 9 路由:`ping` / `set` / `get` / `del` / `update` / `exists` / `size` / `show` / `expire`;`std::mutex` 保护线程安全,SIGINT/SIGTERM 优雅退出。 +- 已纳入 `core/services.yaml` 自启动;暂无业务调用方,预留热点数据/会话缓存;详见 [memdb/README.md](memdb/README.md)。 + ### ai_review(AI 内容审核服务) - 基于 DeepSeek LLM + ChromaDB 向量检索的 RAG 审核架构。 @@ -243,6 +253,21 @@ - 附带数学库(矩阵运算、NN/CNN、Adam 优化器)、CUDA GPU 加速 kernel 和 Lua 硬件模块(ADS1115 / ADS1256 / SPI)。 - 与主干微服务完全独立,需部署在树莓派上独立运行。 +### pybot(机器人动作脚本解析器 · Python 迁移版) + +- [bot4cj](#bot4cj机器人硬件脚本解析器) 的 Python 迁移版,零第三方依赖(纯标准库),与仓颉版逐语义对齐(含占位符 `-` 递归继承、微步分解、块序依赖)。 +- 三种运行模式:脚本执行 `run()`、`--record` 录制 actpred episode(images + actions.npy + imu.npy,≤4 个 MPU6050 按块行序拼接)、`--loop` 实时预测闭环(daemon 线程队列非阻塞取帧,overrun 告警)。 +- 扬声器静音语义:`frequency <= 0` 不出声仅保持节拍(对齐仓颉版 sleep 占位语义)。 +- Windows 开发机可跑测试(fcntl mock + 符号链接 fallback);详见 [pybot/README.md](pybot/README.md)。 + +### mtc(.mt 脚本编译器) + +- 命令式动作脚本(Python DSL)→ `.mt` 矩阵脚本编译器,零第三方依赖;产物可直接由 pybot / bot4cj 执行。 +- 管线:script.py `exec` 注入命名空间 → 事件收集 → 时间轴 IR(相对→绝对、重叠截断)→ 10ms 栅格化 → out.mt。 +- 非阻塞时间轴语义:`Action`/`SetTo`/`Beep` 在当前游标处注册运动段即返回,`Delay`/`Wait` 推进游标,并发运动天然表达;`dur` 须为 10 的倍数(与 pybot 微步节拍一致)。 +- 扬声器通道静止值回落 0(段前/段间空隙/段后 = 静音),与 pybot speaker 静音语义对齐;`spk.frequency`/`spk.delay` 列自动成对补齐。 +- 用法:`python -m mtc walk.py -o walk.mt`(须在 `mtc/` 目录或设 `PYTHONPATH=mtc` 运行);详见 [mtc/README.md](mtc/README.md)。 + ### lci(嵌入式帧缓冲图形界面) - LCI(Linux Chinese Interface)是基于 Linux 帧缓冲设备(`/dev/fb0`)的图形界面系统,**无需 X11 或 Wayland**,直接 `mmap` 操作显存,专为树莓派等嵌入式环境设计。 @@ -291,8 +316,12 @@ | 8007 | ai_review | 127.0.0.1 | AI 审核(仅内部调用) | | 8009 | captcha2 | 127.0.0.1 | 验证码服务(pymsg/TCP,经 ca 代理) | | 8008 | smtp | 127.0.0.1 | 邮件服务(仅内部调用,注册验证码邮件后端) | +| 8011 | memdb | 127.0.0.1 | 内存缓存数据库(libmsg RPC,仅内部调用,暂无业务调用方) | | 8765 | aibox | 127.0.0.1 | AI 智能体(WebSocket,独立产品) | | — | bot4cj | — (树莓派 I2C) | 机器人硬件脚本解析器(独立硬件项目,不联网) | +| — | pybot | — (树莓派) | bot4cj 的 Python 迁移版(.mt 解析/录制/--loop 实时闭环,独立硬件项目) | +| — | mtc | — | 命令式动作脚本 → .mt 编译器(开发期工具,无端口) | +| — | actpred | — | 动作预测模型训练/部署(实验,无端口) | | — | lci | — (树莓派 /dev/fb0) | 嵌入式帧缓冲图形界面(独立硬件项目,不联网) | | — | BotOps | — | aibox 智能体 TUI 前端(WebSocket 客户端,连接 aibox:8765) | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 784f3e8..cff189d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -89,6 +89,7 @@ Blackbird 是一套 **进程托管式轻量微服务** 系统(自研 UniServe BotOps (aibox 的 TUI 前端) bot4cj (Cangjie + 树莓派 I2C, 硬件) pybot (Python, bot4cj 的迁移版: .mt 解析/录制/实时闭环) + mtc (Python, 命令式动作脚本 → .mt 编译器, 开发期工具) lci (C++ /dev/fb0, 嵌入式 GUI) actpred / libtry (实验 / 支撑库) @@ -148,6 +149,7 @@ Blackbird 是一套 **进程托管式轻量微服务** 系统(自研 UniServe | **BotOps** | Python | — | — | aibox 的 TUI 前端 | 独立 | | **bot4cj** | Cangjie | — | 树莓派 I2C | 机器人脚本解析器 | 独立硬件 | | **pybot** | Python | — | 树莓派 | bot4cj 的 Python 迁移版(.mt 解析、actpred episode 录制、--loop 实时预测闭环) | 独立硬件 | +| **mtc** | Python | — | — | 命令式动作脚本(Python DSL)→ .mt 编译器(非阻塞时间轴 IR + 10ms 栅格化),pybot/bot4cj 的脚本 authoring 工具 | 开发期工具 | | **lci** | C++ | — | /dev/fb0 | 嵌入式帧缓冲 GUI | 独立硬件 | | **actpred** | Python | — | — | 动作预测(图像+动作历史→下一动作),bot4cj/pybot 的「大脑」配套 | 实验 | | **libtry** | C | — | — | 嵌入式脚本解释器 | 支撑 | @@ -248,7 +250,7 @@ Blackbird 是一套 **进程托管式轻量微服务** 系统(自研 UniServe 5. **fsys README 图示滞后**:✅ 已修正(2026-08-10)。`fsys/README.md` 架构图与配置章节已更新为 `ca:8443 → gateway:8445`、纯 CLI 配置。 6. **工作记忆偏差**:✅ 已同步。`ca:8443 → gateway:8445`、`dnfs2`、confs 内嵌 core 均已写入各文档。 7. **dnfs2 README 图示滞后**:✅ 已修正(2026-08-10)。`dnfs2/README.md` 架构图与验签描述已更新为 ca:8443(CLI 参数 `--gateway-url` 指向 ca)。 -8. **memdb / pybot 未进根 README**:⏳ 待回填(2026-08-15 发现)。`memdb:8011` 已入 services.yaml、`pybot`(bot4cj Python 迁移版)已落库,但根 `README.md` 模块表与端口一览尚未收录;本文件与 MODULES.md 已同步。 +8. **memdb / pybot / mtc 未进根 README**:✅ 已回填(2026-08-15)。`memdb:8011`、`pybot`(bot4cj Python 迁移版)、`mtc`(Python DSL → .mt 编译器,2026-08-15 落地)连同 `actpred` 已入根 `README.md` 模块表与端口一览;本文件与 MODULES.md 已同步。 --- diff --git a/docs/MODULES.md b/docs/MODULES.md index dbb599f..47fd37b 100644 --- a/docs/MODULES.md +++ b/docs/MODULES.md @@ -1,7 +1,7 @@ # 模块清单(blackbird / micro_servers) > 一套 DIY 的轻量级「进程托管式微服务」(UniServe Framework)。 -> 混合语言:C++20(core/gateway/ca/dnfs2/db2/memdb/fsys/captcha2/lib*)、Python(static/dnfs/db/ai_review/smtp/captcha/confs/redirect80/BotOps/aibox/actpred/pybot/pymsg,多为遗留)、Go(waf)、仓颉/Cangjie(bot4cj)。 +> 混合语言:C++20(core/gateway/ca/dnfs2/db2/memdb/fsys/captcha2/lib*)、Python(static/dnfs/db/ai_review/smtp/captcha/confs/redirect80/BotOps/aibox/actpred/pybot/mtc/pymsg,多为遗留)、Go(waf)、仓颉/Cangjie(bot4cj)。 > 由 `core` 读 `services.yaml` 拉起并守护各子进程;`waf:443 → ca:8443 → gateway:8445 → 业务服务` 为对外主链路。 ## 一、核心与编排 @@ -54,6 +54,7 @@ |------|------|------|------| | bot4cj | 仓颉/Cangjie | — (树莓派 I2C) | 机器人硬件脚本解析器,解析 `.mt` 驱动 PCA9685 舵机 / MPU6050 陀螺仪 / 扬声器,面向 Dashun 蜘蛛机器人 | | pybot | Python | — (树莓派) | bot4cj 的 Python 迁移版:`.mt` 动作脚本解析器(零第三方依赖,与仓颉版逐语义对齐);支持执行 / `--dry-run` / `--record` 录制 actpred episode(含 imu.npy)/ `--loop` 实时预测闭环 | +| mtc | Python | — | 命令式动作脚本(Python DSL)→ `.mt` 编译器:`exec` 注入命名空间 → 时间轴 IR(非阻塞语义、重叠截断)→ 10ms 栅格化;产物可由 pybot/bot4cj 直接执行,零第三方依赖 | | lci | C++ | — (/dev/fb0) | 嵌入式帧缓冲图形界面(无需 X11/Wayland),矢量 GB2312 中文字库、图片渲染、实时数据可视化 | | 3d | — (资产) | — | 机器人 / 机械臂 CAD 模型(STEP/STL/SLDPRT),bot4cj 配套硬件资产 | @@ -91,5 +92,5 @@ - **日志链路**:各服务 `log_client.py`(异步批量)→ `db2:8005` Logs 表(fail-safe,db2 不可用时静默丢弃) - **配置链路**:`gateway` → `core` 内嵌 confs:8010(内存缓存 + mtime 重载,仅 gateway.ini);其余服务配置纯 CLI 传递 - **数据/审核/邮件/缓存**:`dnfs2` → `db2:8005`(pymsg 持久化);`dnfs2` → `ai_review:8007`(审核);注册邮件 `ca` → `smtp:8008`;登录验证码 `ca` → `captcha2:8009`;`memdb:8011`(内存缓存,libmsg RPC,暂无业务调用方,预留热点数据/会话缓存) -- **机器人数据链**:`pybot --record` 录制 episode(images + actions.npy + imu.npy)→ `actpred` 两阶段训练 → ONNX/Hailo HEF 部署 → `pybot --loop` 实时预测闭环(bot4cj 语义对齐) +- **机器人数据链**:`mtc` 编译 Python DSL → `.mt`(或手写 `.mt`)→ `pybot` 执行 / `--record` 录制 episode(images + actions.npy + imu.npy)→ `actpred` 两阶段训练 → ONNX/Hailo HEF 部署 → `pybot --loop` 实时预测闭环(bot4cj 语义对齐) - **端口一览**:80 redirect80 · 443 waf · 8443 ca · 8445 gateway · 5200/8010 core · 9200 fsys · 8006 dnfs2 · 8005 db2 · 8007 ai_review · 8009 captcha2 · 8008 smtp · 8011 memdb · 8765 aibox diff --git a/mtc/README.md b/mtc/README.md new file mode 100644 index 0000000..d16b003 --- /dev/null +++ b/mtc/README.md @@ -0,0 +1,127 @@ +# mtc(.mt 脚本编译器) + +命令式动作脚本(Python DSL)→ `.mt` 矩阵脚本编译器。零第三方依赖(纯标准库),产物可直接由 [pybot](../pybot/) / [bot4cj](../bot4cj/) 解析执行。 + +## 定位与管线 + +手写 `.mt` 矩阵脚本需要心算 10ms 栅格上的全通道快照,冗长且易错。mtc 让动作用命令式 Python 描述(`Action("s1", 10, 100)`),编译器负责展开为栅格矩阵: + +``` +script.py ──exec(注入命名空间)──► 事件收集 ──► 时间轴 IR ──► 10ms 栅格化 ──► out.mt + (相对→绝对、重叠截断) +``` + +## 用法 + +```bash +cd mtc # 须在本目录运行(外层目录会被识别为 namespace package) +python -m mtc walk.py # 编译到 stdout(可管道:python -m mtc walk.py | python -m pybot --dry-run /dev/stdin) +python -m mtc walk.py -o walk.mt # 写文件 +``` + +也可设 `PYTHONPATH=mtc` 后在任意目录运行。诊断(编译告警/错误)走 stderr,stdout 保持纯净 .mt 文本。 + +## DSL 参考 + +脚本即 `.py`,`exec` 注入两类函数。 + +### 硬件声明(函数名 = .mt 块名,位置参数 = 表头列,严格一一对应) + +```python +I2C("i2c-1", "/dev/i2c-1") +PCA9685("pca9685-0", "i2c-1", "0x40") +Servo("s1", 180, 0, 0, "pca9685-0", 0) # name,max,min,offset,controller,channel +Speaker("spk", 2, 48000, 0.6, 5000, 20) # name,channels,sample_rate,volumn,max_f,min_f +Camera("cam0", 640, 480, 30, 8) +MPU6050_BIND("imu0", "i2c-1", "0x68") +Alias("A", "angle") +register_block("ADS1115", ["name", "I2C", "addr"]) # 开放扩展(首列恒为 name) +``` + +### 时间轴指令(编译期求值,不产生 .mt 行) + +| 函数 | 语义 | +|------|------| +| `Action(dev, delta, dur)` | 相对运动:当前游标处注册,dur ms 内走 delta | +| `SetTo(dev, angle, dur)` | 绝对运动(糖:delta = angle − 已注册目标值) | +| `Beep(freq, dur, dev="spk")` | 扬声器频率段:dur ms 内恒为 freq(阶跃段) | +| `Delay(ms)` | 游标推进固定量 | +| `Wait()` | 汇合:游标推进到所有在途运动的最晚完成时刻 | +| `Pose(dev, angle)` | 声明编译期初始姿态(不产生运动段,须在 Action 之前) | + +`dur` / `Delay` 必须是正的 10 的倍数(.mt 栅格与 pybot 微步节拍均为 10ms)。 + +## 语义规则 + +- **非阻塞时间轴**:`Action`/`SetTo`/`Beep` 在当前游标处注册运动段并立即返回,`Delay`/`Wait` 才推进游标。不同设备段在同一时间窗重叠即并发运动。 +- **重叠截断**:同通道新段与未完结末尾段重叠时,前段截断至新段起点,新段基准取前段**目标值**(编译期确定,不依赖运行时实际姿态),并产出告警(stderr)。 +- **静止值(rest)**:姿态通道(舵机等)段外保持前段目标值;`spk.frequency` 通道段前/段间空隙/段后统一回落 0 = 静音(对齐 pybot speaker 静音语义:`freq<=0` 不出声仅保节拍)。 +- **节拍列**:`spk.delay` 由编译器管理(恒 10),脚本不能直接写;无 Speaker 块则无节拍列(pybot 执行时行间隔不等待,编译器给出告警)。`spk.frequency` 无段时自动补恒 0 列(pybot 契约:frequency/delay 两列成对出现)。 +- **栅格行语义**:格点 t=10,20,…,T,每行代表该 10ms 结束时刻的绝对姿态;pybot `run()` 对 delay=10 的行分解为 1 个微步且插值系数 1.0,即直接到位——与栅格语义严格等价。 + +## 示例 + +```python +Servo("s1", 180, 0, 0, "pca9685-0", 0) +Servo("s2", 180, 0, 0, "pca9685-0", 1) +Speaker("spk", 2, 48000, 0.6, 5000, 20) +Action("s1", 10, 100) # s1 在 [0,100ms) 走 10° +Delay(50) # 游标推进到 50ms +Action("s2", 20, 50) # s2 在 [50,100ms) 走 20°(第 6 行起步) +``` + +编译产物: + +``` +@Servo +name max min offset controller channel; +s1 180 0 0 pca9685-0 0; +s2 180 0 0 pca9685-0 1; +@ + +@Speaker +name channels sample_rate volumn max_f min_f; +spk 2 48000 0.6 5000 20; +@ + +@Action +s1.angle s2.angle spk.frequency spk.delay; +1 0 0 10; +2 0 0 10; +3 0 0 10; +4 0 0 10; +5 0 0 10; +6 4 0 10; +7 8 0 10; +8 12 0 10; +9 16 0 10; +10 20 0 10; +@ +``` + +## 输出契约(与 pybot parser 对齐) + +- `@块名` / `@` 定界;行 0 表头;行尾 `;`;列间空格分隔;相邻同名块合并为一个块多行。 +- `@Action` 表头为 `设备.参数` 长名(恰含一个 `.`,Q05 校验),末列 `.delay` 恒 10。 +- 数值格式化:整数无小数点,小数至多 3 位。 + +## 测试 + +```bash +cd mtc && python -m pytest tests/ -q +``` + +含 e2e 闭环验证:编译产物经 pybot `MatrixParser` 零修改解析(列数一致、Q05 表头契约、栅格行数值逐一比对、roundtrip 自检)。pybot 与 mtc 的 `tests/` 目录同名,两套测试须分进程运行。 + +## 目录结构 + +``` +mtc/ +├── mtc/ +│ ├── __init__.py # 包入口(compile_script / CompileError) +│ ├── __main__.py # CLI:python -m mtc script.py [-o out.mt] +│ ├── dsl.py # DSL 命名空间 + 事件收集(Collector) +│ ├── timeline.py # 时间轴 IR:Segment/Channel、重叠截断、rest 静止值 +│ └── emit.py # .mt 序列化:硬件块合并 + 10ms 栅格矩阵 +└── tests/ # pytest(含 pybot parser e2e 闭环) +``` diff --git a/mtc/mtc/__init__.py b/mtc/mtc/__init__.py new file mode 100644 index 0000000..e3ca4bb --- /dev/null +++ b/mtc/mtc/__init__.py @@ -0,0 +1,14 @@ +"""mtc —— 命令式动作脚本(Python DSL)→ .mt 矩阵脚本编译器 + +管线:script.py --exec--> 事件收集 --> 时间轴 IR(相对→绝对, +重叠截断)--> 10ms 栅格化 --> out.mt --> pybot 解析/执行。 + +用法: + python -m mtc walk.py # 编译到 stdout(管道风格) + python -m mtc walk.py -o walk.mt # 写文件 + python -m pybot --dry-run walk.mt # 交给 pybot 验证/执行 +""" + +__version__ = "0.1.0" + +from .dsl import CompileError, compile_script # noqa: F401 diff --git a/mtc/mtc/__main__.py b/mtc/mtc/__main__.py new file mode 100644 index 0000000..19d4c00 --- /dev/null +++ b/mtc/mtc/__main__.py @@ -0,0 +1,54 @@ +"""CLI 入口:python -m mtc script.py [-o out.mt] + +诊断(编译告警/错误)走 stderr,stdout 保持纯净 .mt 文本 +(对齐 pybot 的 S02 日志规范,支持 mtc walk.py | pybot --dry-run 管道)。 +""" + +import argparse +import logging +import sys + +from .dsl import CompileError, compile_script + + +def main(argv=None): + p = argparse.ArgumentParser( + prog="mtc", + description="命令式动作脚本(Python DSL)→ .mt 编译器") + p.add_argument("script", help="DSL 脚本路径(.py)") + p.add_argument("-o", "--output", default=None, + help="输出 .mt 路径(缺省写 stdout)") + p.add_argument("--verbose", action="store_true", help="DEBUG 日志") + args = p.parse_args(argv) + logging.basicConfig(stream=sys.stderr, + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(levelname)s %(name)s: %(message)s") + + try: + with open(args.script, encoding="utf-8") as f: + source = f.read() + except OSError as e: + logging.error("无法读取脚本: %s", e) + return 2 + + try: + text, warnings = compile_script(source, filename=args.script) + except CompileError as e: + logging.error("编译失败: %s", e) + return 2 + + for w in warnings: + logging.warning("%s", w) + + if args.output: + with open(args.output, "w", encoding="utf-8") as f: + f.write(text) + logging.info("written: %s (%d lines)", args.output, + text.count("\n")) + else: + sys.stdout.write(text) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/mtc/mtc/dsl.py b/mtc/mtc/dsl.py new file mode 100644 index 0000000..1eb7e94 --- /dev/null +++ b/mtc/mtc/dsl.py @@ -0,0 +1,204 @@ +"""mtc DSL —— 命令式动作脚本(Python)→ 事件收集 + +脚本即 .py,通过 exec(source, make_namespace(collector)) 执行, +命名空间内注入两类函数: + +硬件声明(函数名 = .mt 块名,位置参数 = 表头列,严格一一对应): + I2C("i2c-1", "/dev/i2c-1") + PCA9685("pca9685-0", "i2c-1", "0x40") + Servo("s1", 90, 0, 0, "pca9685-0", 0) # name,max,min,offset,controller,channel + Speaker("spk", 2, 48000, 0.6, 5000, 20) + Camera("cam0", 640, 480, 30, 8) + MPU6050_BIND("imu0", "i2c-1", "0x68") + Alias("A", "angle") + register_block("ADS1115", ["name", "I2C", "addr"]) # 开放扩展 + +时间轴指令(编译期求值,不直接出现在 .mt 行中): + Action(dev, delta, dur) # 相对运动:当前游标处注册,dur ms 内走 delta + SetTo(dev, angle, dur) # 绝对运动(糖:delta = angle - 已注册目标值) + Delay(ms) # 游标推进固定量 + Wait() # 游标推进到所有在途运动的最晚完成时刻(汇合) + Beep(freq, dur) # 扬声器频率段(默认设备 spk) + Pose(dev, angle) # 声明编译期初始姿态(默认 0,不产生运动段) + +dur/Delay 必须是 10 的倍数(.mt 栅格与 pybot 微步节拍均为 10ms)。 +""" + +from .timeline import STEP_MS, Channel, total_ms +from .emit import emit_mt + +# 已知块 → 表头列(位置参数顺序;首列恒为 name) +_BLOCK_SCHEMAS = { + "Alias": ["name", "alias"], + "I2C": ["name", "path"], + "PCA9685": ["name", "I2C", "address"], + "MPU6050_BIND": ["name", "I2C", "addr"], + "Speaker": ["name", "channels", "sample_rate", "volumn", "max_f", "min_f"], + "Servo": ["name", "max", "min", "offset", "controller", "channel"], + "Camera": ["name", "width", "height", "framerate", "capacity"], +} + + +class CompileError(Exception): + """脚本编译错误(携带可读信息,CLI 原样输出)""" + + +class Collector: + """一次编译的全部状态:硬件块 + 通道时间轴 + 游标 + 告警""" + + def __init__(self): + self.blocks = [] # [(block_name, columns, row_values)] + self.channels = {} # {通道名(dev.param): Channel}(注册序) + self.cursor = 0.0 # 当前时间游标 ms(单调推进) + self.warnings = [] + + # ---------------- 硬件 ---------------- + + def add_block(self, block_name, columns, values): + self.blocks.append((block_name, columns, [str(v) for v in values])) + + def device_names(self): + """已声明硬件变量名集合(每块首列恒为 name)""" + return {row[0] for _, _, row in self.blocks} + + def first_speaker(self): + """第一个 Speaker 块声明的设备名(delay 节拍列所属),无则 None""" + for block_name, _, row in self.blocks: + if block_name == "Speaker": + return row[0] + return None + + # ---------------- 时间轴 ---------------- + + def channel(self, name): + ch = self.channels.get(name) + if ch is None: + ch = self.channels[name] = Channel(name) + return ch + + def check_device(self, dev): + if dev not in self.device_names(): + raise CompileError( + f"设备 '{dev}' 未声明(先用 Servo/Speaker/插件块声明)") + + def warn_overlaps(self): + for ch in self.channels.values(): + self.warnings.extend(ch.overlap_warns) + + # ---------------- 编译 ---------------- + + def compile(self): + """归一化 IR 并序列化为 .mt 文本,返回 (mt_text, warnings)""" + self.warn_overlaps() + active = [ch for ch in self.channels.values() if ch.segments] + t_total = total_ms(self.channels, self.cursor) + if not active and t_total <= 0: + raise CompileError("脚本没有任何动作(@Action 将为空)") + if self.first_speaker() is None: + self.warnings.append( + "无 Speaker 块:.mt 无 delay 节拍列,pybot 执行时行间隔" + "不等待(时间轴语义丢失,仅 --loop/解析场景可用)") + text = emit_mt(self.blocks, active, t_total, + speaker=self.first_speaker()) + return text, self.warnings + + +def _check_dur(ms, what): + if float(ms) <= 0 or float(ms) % STEP_MS != 0: + raise CompileError(f"{what} 必须是正的 {STEP_MS} 的倍数,got {ms!r}") + + +def make_namespace(collector): + """构建脚本 exec 命名空间(硬件函数 + 时间轴指令)""" + ns = {} + schemas = dict(_BLOCK_SCHEMAS) + + def _make_block_fn(block_name, columns): + def fn(*args): + if len(args) != len(columns): + raise CompileError( + f"{block_name}() 需要 {len(columns)} 个参数 " + f"({', '.join(columns)}),got {len(args)}") + collector.add_block(block_name, columns, args) + fn.__name__ = block_name + return fn + + for block_name, cols in schemas.items(): + ns[block_name] = _make_block_fn(block_name, cols) + + def register_block(block_name, columns): + """注册自定义块(插件扩展):register_block("ADS1115", [...])""" + if not columns or columns[0] != "name": + raise CompileError("register_block: 首列必须是 name") + ns[block_name] = _make_block_fn(block_name, list(columns)) + + def _register(dev, param, value, dur, absolute): + collector.check_device(dev) + _check_dur(dur, f"{dev}.{param} 的 dur") + if param == "delay": + raise CompileError("spk.delay 列由编译器管理(恒 10)," + "节拍请用 Delay()/Wait()") + ch = collector.channel(f"{dev}.{param}") + v0 = ch.target # 重叠时基准 = 前段目标值(编译期确定) + v1 = float(value) if absolute else v0 + float(value) + ch.add(collector.cursor, float(dur), v0, v1) + + def Action(dev, delta, dur, param="angle"): + """相对运动:dur ms 内走 delta(可正可负)""" + _register(dev, param, delta, dur, absolute=False) + + def SetTo(dev, angle, dur, param="angle"): + """绝对运动:dur ms 内到达 angle""" + _register(dev, param, angle, dur, absolute=True) + + def Delay(ms): + """游标推进固定量""" + _check_dur(ms, "Delay") + collector.cursor += float(ms) + + def Wait(): + """汇合:游标推进到所有在途运动的最晚完成时刻""" + busy = [ch.busy_until for ch in collector.channels.values()] + collector.cursor = max([collector.cursor] + busy) + + def Beep(freq, dur, dev="spk"): + """扬声器频率段:dev.frequency 在 dur ms 内恒为 freq(阶跃段) + + 通道静止值设为 0:段前/段间空隙/段后回落 0 = 静音 + (对齐 pybot speaker 静音语义:freq<=0 不出声仅保节拍)。 + """ + collector.check_device(dev) + _check_dur(dur, f"Beep({freq}) 的 dur") + ch = collector.channel(f"{dev}.frequency") + ch.rest = 0.0 + ch.add(collector.cursor, float(dur), float(freq), float(freq)) + + def Pose(dev, angle, param="angle"): + """声明编译期初始姿态(不产生运动段;须在 Action 之前)""" + collector.check_device(dev) + ch = collector.channel(f"{dev}.{param}") + if ch.segments: + raise CompileError( + f"Pose({dev}) 必须在任何 Action 之前声明") + ch.set_initial(angle) + + ns.update(Action=Action, SetTo=SetTo, Delay=Delay, Wait=Wait, + Beep=Beep, Pose=Pose, register_block=register_block) + return ns + + +def compile_script(source, filename="