Web profile 插件 —— 宿主半 + 浏览器半。 > **English**: [README.md](README.md) | **中文**: [README.zh.md](README.zh.md) --- # DSH 硬件沙盒 **给 DSH 的 3D 硬件沙盒:把装配摆出来、接上线,然后让你自己项目的代码跑在虚拟硬件上。** **安装(从 GitHub 源码装;本包未发布到 npm)** ```sh dsh plugin --profile web add github:Noob-stupid/dsh-breadboard ``` 装完刷新页面即可。沙盒注册进 **`main`** 槽(3D 视图)与 **`sidebar.panellist`** 槽(各面板); `skill/SKILL.md` 随插件一起注册,装了这个插件的 agent 已经知道该怎么用它。 **喜欢的话**:在插件管理里打开这个插件的详情页,**内容最下方靠右**有一行小字,可以到 [GitHub 点个 Star](https://github.com/Noob-stupid/dsh-breadboard)。它就是透明的一行字 —— 没有底色、没有边框、没有阴影,也只在本插件自己的页面上出现。 这一行能生效靠三件事,三件都在本仓库里: | 要求 | 位置 | 说明 | |---|---|---| | 仓库 topic `dsh-plugin` | GitHub 仓库 topics | 社区列表据此发现本仓库 | | `dsh.bundle` 清单 | `package.json` → `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` | **没有它 `dsh plugin add` 装不进任何东西** —— 而且界面上的症状极具误导性:插件列表说「没有声明组合包」,真正被挡住的却是装配本身,于是所有 `/api/*` 返回 404,面板干脆不出现 | | 导出 `apply(ctx)` | `src/index.ts`(宿主半)/ `src/client/index.ts`(浏览器半) | 宿主半注册路由、工具与仿真引擎;浏览器半注册上面那两个槽 | ## 这是什么 / 解决什么问题 一个软硬件项目里,**只有真机能试**的那几段,是接线本身、传感器在寄存器层的语义、以及出错时的处理。 本插件把它们搬进一个真能跑起来的虚拟场景: - **3D 装配** —— 从内置模型库放器件(ESP32、树莓派 4B、BME280、HC-SR501 PIR、LED 模块、 MPL3115A2、面包板),按端口逐个连线,得到一份活的 SSOT(唯一真相)装配状态,带拓扑与电气校验。 - **项目级硬件在环** —— 一个**真实 Python 进程**跑**你自己项目的代码**。Python shim 拦截它的 `smbus`/I2C 访问,由虚拟设备应答,所以被测项目读到的是寄存器级的值 (例如它自己拿虚拟芯片的数据做真正的 BME280 补偿计算)。 - **联网设备模拟**(并列的**第二类**虚拟硬件)—— 一台按真实节拍**主动**向**你的**外部 HTTP 后端 上报的设备:注册 → 拉配置 → 周期上报。已在真实的智能选座系统上全链路验证 (设备 → HTTP → 服务端状态机 → 数据库)。 - **故障注入** —— 拔线、返回 NACK、强制读数、让设备掉线,然后看项目的异常处理到底抓没抓住。 - **项目级便利** —— 扫描一个真实项目文件夹,得出它需要哪些硬件;再一键换掉整套硬件。 依赖 DSH 的 **`web`** profile 与 `webServer`、`tools`、`subprocess` 三个服务。宿主半把它们声明为 **硬 `inject` 依赖(刻意如此)**:缺任何一个,插件**根本不激活**,而不是"激活了但工具没注册 / 界面空白 / 仿真起不来"。 ### 没有真实项目也想看效果 `examples/zhizuo-virtual-esp32/` 是虚拟 ESP32 座位传感器的独立脚本版。 `hw_list_projects` 会列出内置演示项目(**zhizuo-seat-node**)以及它当前是否软硬件连通; `hw_import_project` 一次把它的器件、连线与联网绑定摆好。 ## 界面导览 界面目前**只有中文**,下表用的就是屏幕上那行字。 | 面板 | 作用 | |---|---| | **③ 项目(一键换整套硬件)** | 主工作流。每个项目本体显示:文件数(采样跳过时为「N 个文件(采样时跳过 M 个)」)、**扫描需求**、**删除**;底部还有:**+ 导入项目本体(选文件夹)**、**⤓ 导出建模组(存成一个 .json)**、**+ 导入建模组(选 .json)**、**+ 添加型号定义(选 .json)**、**✎ 让 agent 帮我写** | | **① 添加器件** | 点一下型号键,把它放进场景。**只列有真几何的型号**(默认收起) | | **② 换装模型** | 搜模型 / 导入文件,给某个型号换装;没有几何的型号渲染为占位盒。**外部(数据驱动)型号带「[数据驱动]」标记**(默认收起) | | **④ 真机观察** | 显示**真实硬件**的状态(只读),并能从界面开始/停止观察(默认收起) | | **统计** | 渲染统计(绘制次数 / 三角形数 / 帧率)—— 纯读取,默认关 | | **中央 3D 视图** | 已放置器件(带世界变换)、端口之间的**蓝色连线**、在带 `warn`/`error` 告警的器件上就地面出的告警框、变换手柄 / 右键菜单(钉住、删除)、端口悬浮提示 | | **面板浮层输入框** | 不离开面板就能给当前会话发消息;宿主没提供 `agents` 服务时该输入框直接隐藏(`capabilities.chat` 缺省) | 其中两条要点名: - **「扫描需求」只是界面按钮。** 它没有对应的 agent 工具 —— agent 侧没有这一步。agent 能做的是 它后面的事:摆器件、连线、建项目图纸、跑仿真。 - **删除项目只删掉项目定义**(那一份 JSON)。它**不动场景**,所以删项目永远不会把你眼前的硬件清掉。 ## 快速上手:三步 1. **导入项目本体** —— 点 **+ 导入项目本体(选文件夹)**,选一个**完整的项目文件夹** (前后端 + 硬件代码)。它**不带建模**;宿主读它来判断需要哪些硬件。 2. **扫描需要哪些硬件** —— ③ 面板里的 **扫描需求** 按钮(界面专属,见上)。报告会列出每个候选 型号键,并给出**证据**(在哪几个文件里看到的),让你自己判断对不对;扫描时如果做了采样, 它会说出来,而不会让你以为整个项目都扫过了。 3. **导入建模组** —— 点 **+ 导入建模组(选文件夹)**,布局是 `<型号键>/model.glb`。 没补的型号继续渲染为占位盒。 或者干脆跳过 1–3:把系统用话讲给 agent,让它用 **`hw_save_project`** 写成图纸, 再用 **`hw_import_project`** 一次把器件、连线、联网绑定全部摆好。 ## Agent 工具:16 个 `hw_*` 以下是**实际注册的清单**(未增未删)。「什么时候用」这一列才是表格的意义 —— 这些工具并不通用互换。 ### 项目 | 工具 | 作用 | 什么时候用 | |---|---|---| | `hw_list_projects` | 列内置软硬件项目:器件、接线、联网配置,以及当前是否软硬件连通 | **在手工摆器件之前先用它** —— 一张图纸是一次调用;手工摆是四步,少一步就不通且不报错 | | `hw_import_project` | 导入项目:**先清空当前装配**,再摆器件、连线、挂绑定 | 换掉整套硬件时用;注意它是**替换**,不是往上加 | | `hw_save_project` | 新建或覆盖项目图纸(存进 `$DSH_HOME/dsh-hardware-sandbox/projects/`) | 用户用话描述了一个系统 —— 先写成图纸,再导入 | ### 装配 | 工具 | 作用 | 什么时候用 | |---|---|---| | `hw_get_assembly` | 读 SSOT 摘要:revision、组件、连线、告警(连 `message` 一起给,不只是码)、供电、虚拟设备 | **每次改动前都先读** —— 你需要真实的端口 id;端口 id 是引脚身份(`E4`),不是丝印名(`GPIO23`) | | `hw_edit_assembly` | 一个工具覆盖契约里的 `ClientAction` 联合:放器件 / 移动 / 旋转 / 连线 / 拔线 / 删除 / 钉住 | 任何对场景的修改。它收一个 action 对象,先读工具描述里的样例 | ### 仿真 | 工具 | 作用 | 什么时候用 | |---|---|---| | `hw_run_simulation` | 在虚拟硬件上运行真实的 Python 项目;`duration` 是**虚拟**秒数;返回项目输出与虚拟/真实耗时 | 真正的硬件在环那一跑。`accelerated`(默认)远快于真实时间;`realtime` 按真实节拍限速,便于观察 | | `hw_get_sim_log` | 读最近一次(或进行中)那跑的项目输出,可只取 `tail` 行 | 当 `hw_run_simulation` 的输出被截断时 | | `hw_inject_fault` | 对设备 id 或 `型号:地址` 目标注入 `disconnect` / `nack` / `busy` / `set_reading` / `clear` | 验证项目的异常处理**真的看到**了故障(例如断线后的 `OSError`),而不是假定它会看到 | ### 联网设备 | 工具 | 作用 | 什么时候用 | |---|---|---| | `hw_network_add` | 添加一台联网设备(协议适配器 + endpoint + 设备自报身份)。**它不会启动** | 第一次把设备模拟器接上你的后端 | | `hw_network_devices` | 列设备:协议、外部系统地址、在线状态、上报/失败次数、当前读数、服务端下发的配置 | 确认设备到底连上没有、有没有在真的上报 —— 否则"已添加但停着"看起来也很正常 | | `hw_network_control` | `start` / `stop` / `disconnect`(掉线故障)/ `clear` | **只有 `start` 会让请求真的发出去** —— 这是刻意的,默认不该有外部副作用;`disconnect` 用来验对面能否识别离线 | | `hw_network_set_reading` | 设置**物理读数**(如 `{ ir_front: true, ir_back: true }`,`true` = 被遮挡) | 单独验协议层对不对,与硬件通路无关。电平由协议按配置(如 `ir_active_high`)翻译,**极性搞反的责任留在协议侧**,不在调用方 | | `hw_set_occlusion` | 遮挡 / 移开装配里的某个器件,并**沿实际接线**把影响传导到联网设备的读数上 | 验**硬件**通路:PIR 没接上、或接错了脚,就**什么都不会发生** —— 真机就是这样 | ### 型号定义(数据驱动) | 工具 | 作用 | 什么时候用 | |---|---|---| | `hw_scan_board_pins` | **从本机 PlatformIO 的权威文件推导引脚表草稿**:`boards/.json` → `build.variant` → `variants//pins_arduino.h`(**编译器实际用的那份映射**)。`list:true` 列出本机能找到的板子 | **要加一块新板子时先跑它**。★★ 它给的是**草稿 + 缺口清单**(缺:物理排布 / 电气语义 / 丝印),**不是成品** —— 缺口按数据手册补齐后再用 `hw_save_model_definition` | | `hw_save_model_definition` | 保存一条**型号定义**(引脚表:`portId`/丝印/协议/电压/方向 + 尺寸 + 排布)。校验通过才落盘,**落盘后立即生效** | 需要一块**插件还不认识**的板子时。**引脚表必须有权威来源**(厂商数据手册 / PlatformIO 的 variant 文件 / 项目自己的工程文件);找不到就**问用户**,不要凭记忆编 | ### 真机在环(只读观察) | 工具 | 作用 | 什么时候用 | |---|---|---| | `hw_watch_device` | **观察一台真机**:定时**只读**查询外部系统(智座用 `GET /api/admin/sensor/devices`),把真机状态映射到场景器件上。`action` = `start` / `stop` / `status` | 想知道"**真机现在到底连没连上**"。⚠️ 它与 `hw_network_add` 方向**相反**:那个是我们**冒充设备**发请求,这个是**我们去问真机**(只读、无对外副作用) | ## 原因码与对策 装配操作的失败都带原因码(契约的 `WarningCode`,共 **12 项**,见 `src/contracts/assembly.ts`)。 **照着码改对应的那件事。不要盲试。** | 原因码 | 含义 | 该做什么 | |---|---|---| | `bad_params` | 动作参数不可用(如旋转角里带 `NaN` / `Infinity`) | 改数值 —— 问题在值上,不在组件 id 上 | | `direction_conflict` | 两端电流方向冲突(输出对输出 / 两个供电接在一起) | 按电流方向重排:至少一端要变成输入 | | `protocol_mismatch` | 两端口信号种类不同 | 用匹配的端口对;确认那个脚到底支持哪种信号 | | `port_occupied` | 这个端口被**别的**线占了 | 拔掉那根线(用它的 `cableId` 做 `disconnect`),或换脚 | | `voltage_mismatch` | 端口电压不兼容 | 接电压兼容的端口,或加电平转换 | | `power_exceeded` | 装配的供电需求超出供给预算 | 增加供电或减小功耗 | | `i2c_address_conflict` | 总线上两个器件地址撞了 | 改其中一个的地址(SDO/AD0 脚) | | `i2c_pullup_missing` | I2C 总线缺上拉 | 给 SDA/SCL 加上拉 | | `unconnected_port` | 有端口一根线都没接 | 按需连接 —— 并检查是不是忘了成对的那一根 | | `unknown_hardware_model` | 找不到这个型号键 | 用 `hw_get_assembly` 看现有组件里的真实键 | | `component_pinned` | 组件被钉住了,移动被**明确拒绝** | 先解开钉住(右键菜单,或 `set_pinned: false`) | | `already_connected` | **就是这一对**端口已经连了 | 别去找一根不存在的线;想换线先拔这一根 | 三条值得留下的说明: - **`port_occupied` 与 `already_connected` 是刻意分开的两个码。** 合成一个,用户会去找一根不存在的线。 - **多个*不同*端口接到同一个端口是合法的**(那是 I2C 总线),**不报** `already_connected`。 - **`ok: true` 不等于效果发生了。** 每次写入之后都应该再读一次装配确认。 ## 型号定义:让插件认识一块新硬件 **先说清两件不同的事**(这是最容易混的一处): | | 给的是 | 从哪来 | |---|---|---| | **建模组(`.glb`)** | **外观** —— 场景里**长得像真板子** | 模型库(step.parts / GrabCAD / SnapEDA…,**大部分板子有**)或你自己导入 | | **型号定义(`.json`)** | **引脚数据** —— 有几个脚、每个脚是什么 | **模型库里没有**:连 SnapEDA 的符号库都"**不含总线协议与电压**",而语义正是我们要校验的东西 | ⇒ **外观来自库;引脚数据靠型号定义。** 没有定义,插件不认识这块板子(摆上去也只是占位方块)。 ### 一条定义里写什么 ```jsonc { "key": "stm32f103c8t6", // 型号键(小写字母/数字/连字符);不能与内置型号同名 "label": "STM32F103C8T6(Blue Pill)", "size": { "x": 0.0533, "y": 0.011, "z": 0.0229 }, // 包围盒,单位【米】 "powerDrawW": 0, "powerSupplyW": 0, "ports": [ // portId = 身份(连线用它,必须唯一);name = 丝印(人看的那行字,可重复) { "portId": "P1", "name": "VBAT", "protocol": "power", "voltage": 3.3, "direction": "in" }, { "portId": "P2", "name": "PC13", "protocol": "gpio", "voltage": 3.3, "direction": "io" } ], // 排布:贴哪条边、间距、几排(双排排针用 rows/rowPitch) "portLayout": { "edge": "+z", "pitch": 0.00254, "count": 40, "inset": 0.001, "rows": 2, "rowPitch": 0.02292 } } ``` **三条容易踩的**: 1. **`portId` 是身份、`name` 是丝印** —— 一块板有**多个 GND**,用功能名当身份会**撞号**,而撞号的后果是静默的(两根线分不清)。 2. **`voltage: 0` 是合法的** —— 那是 **GND 的电压**,不是"没填"。 3. 端口**要么每个给 `position`、要么给 `portLayout`** —— 两者都缺会被拒绝(不是给个默认值假装能用)。 ### 三条获取路径(按可靠性排序) | 路径 | 怎么做 | 说明 | |---|---|---| | **① 扫描/推导**(主路径) | 从**工程文件**推:`platformio.ini` 的 `board =` 认出型号;framework 的 `variants//pins_arduino.h`(**编译器实际用的那份映射**)拿到引脚;固件源码 `#define IR_SENSOR_A_PIN 23` 看出**本项目实际用了哪些脚**;STM32 的 `.ioc` 有完整外设→引脚分配 | 覆盖大部分;**推不出**电气语义(3.3V/5V、I2C、上拉)与物理尺寸 —— 那部分要手册 | | **② AI 一键写** | ③ 面板里填型号名 → **✎ 让 agent 帮我写**(或让 agent 直接调 `hw_save_model_definition`) | agent 会先找**权威来源**;**找不到就来问你**,不许凭记忆编 | | **③ 手动/用户提供** | ③ 面板 **+ 添加型号定义(选 .json)** | 你自己有手册/接线表时最快 | ### ★★ 一条纪律(写进工具描述里的) **引脚表必须有权威来源**(厂商数据手册 / PlatformIO variant / CubeMX 数据库 / 项目自己的工程文件)。 **手打错的引脚表比没有更坏** —— 它会产出"**看起来对但接错脚**"的校验,而**没人能发现**。 ⇒ 所以插件宁可拒绝、宁可让你补来源,也不给一个"看起来合理"的默认值。 **内置型号不可被覆盖**:撞名会被拒绝并说明原因 —— 否则别人发一个 `rpi-4b` 定义就能**悄悄改掉所有用户的引脚表**。 ## 真机观察:把真机状态读进场景 让场景里的器件反映**真实硬件**的状态:**真机 → 服务端 → 插件**(只读)。 ``` hw_watch_device({ action:'start', componentId:'c1', deviceId:'AA:BB:CC:11:22:33', endpoint:'http://127.0.0.1:5800', intervalMs:5000, cookie:'…' }) hw_watch_device({ action:'status' }) // 看现在是什么状态 hw_watch_device({ action:'stop', componentId:'c1' }) ``` 界面上对应 **④ 真机观察** 面板(填器件 / 真机标识 / 地址 / 间隔 → **开始观察(只读)**)。 ### ★★ 五种状态**分开**(这是这一层最重要的设计) | 状态 | 含义 | 该去查什么 | |---|---|---| | `ok` | 查到了,数据是新的 | —— | | `stale` | **查到了,但数据很旧** | **真机确实离线** → 查硬件(供电/网络)| | `error` | **我们查不到**(网络/鉴权/解析失败)| **地址与鉴权**,**与硬件无关** | | `not_found` | 服务端答得好好的,**但没有这台设备** | `deviceId` 是否写错;或它还没上过线 | | `idle` | 还没查过 | —— | ⚠️ **把 `error` 说成"设备离线"是本项目最怕的那种错**:结论看起来合理、实际完全错误,还会让人去**拔插硬件**,而真正的问题在我们这边(地址写错 / 没登录)。 ### 两条硬约束 1. **轮询走墙钟**(真机按真实时间走,**不能加速**),**间隔下限 2 秒** —— 这是**保护对方服务器**的硬约束,不是建议值。 2. **只有宿主在轮询**:界面只读宿主的缓存(不会把对方服务器打两遍,也不会出现两份不一致的状态)。 ### ⚠️ 一个真实的陷阱(源码里挖出来的) **不要用 `GET /api/sensor/device_config` 做观察** —— 智座那条路由会把 `last_seen` **刷成当前时间**(它本来是给设备拉配置用的)。 用它做观察 = **我们一查,离线设备就变在线** ⇒ 得出"设备在线"而设备根本没插电。 **⇒ 观察只用 `/api/admin/sensor/devices`**(只读),这条有断言钉死。 ## 建模组格式 交给 **+ 导入建模组** 的文件夹(也是导出与导入往返的同一套布局)是: ``` <型号键>/ model.glb 模型本体 meta.json 可选元信息 ``` - **渲染只加载 GLB / GLTF。** 导入侧的格式判定**认内容不认扩展名**,接受 glb / gltf / stl / obj / ply / 3mf / dae / fbx / wrl;但 **STEP / IGES 会被明确拒绝,而不是静默存下** —— three.js 没有对应 loader,存下来的 STEP 永远渲染不出来。 - 内部每个模型是一个目录 `<型号键>/{model.bin, meta.json}`,`model.bin` 装几何字节 (`meta.json` 记 `format` / `nodes` / `bytes` / `generator`)。用中性文件名存字节是刻意的: 布局里没有扩展名歧义,`meta` 与它的字节绑在一起。导出的建模组用上面的 `<型号键>/model.glb` 形式,所以导出与导入是同一套布局。 - 随包分发(启动时补进用户模型目录,**只补缺的键** —— 你自己导入过的模型绝不会被覆盖): | 型号键 | 来源 | 在包里 | 为什么 | |---|---|---|---| | `bme280` | 自产 | 有 | 寄存器级设备模型 + 演示装配要用 | | `led-5v` | 自产 | 有 | 最简单的输出器件 | | `hc-sr501` | 自产 | 有 | 演示装配要用两个(PIR) | | `esp32-seat-sensor` | 自产 | 有 | 联网设备演示要用 | | `rpi-4b` | 从 step.parts 导入(第三方) | **没有** | 第三方几何 —— **本插件不分发**;需要时从界面上自己导入 | | `mpl3115a2` | 从 step.parts 导入(第三方) | **没有** | 同上 | | `breadboard` | 自产(无端口、无源) | 无几何 | 渲染为按契约尺寸生成的占位盒 | 为什么这么分:四个自产模型是我们的,可以随包发,所以插件**开箱即用** (演示装配就是 `esp32-seat-sensor` + 2 × `hc-sr501`,全在包里)。第三方模型是别人的资产、有它自己的条款, 所以插件**不分发它们** —— 界面给搜索入口,文件你自己导。端口**语义**(I2C 地址、协议、电压、方向) 一律自产,因为没有哪个模型库卖这个 —— 详见 `docs/05`。 ## 墙钟陷阱:联网设备为什么不用虚拟时间 虚拟时钟是**加速**的:`advance(60)` 会把 60 个虚拟秒在几十毫秒内跑完。被动等被读的总线设备挂上去没问题 (确定性、纯同步);**联网设备不行** —— 它是主动周期上报的,一次快进会朝别人的后端打出几十次 **真实 HTTP 请求**,把对方打爆。 所以联网设备**一律按墙钟(真实节拍)走,与虚拟时钟解耦**;它的快照里带 `wallClockDriven: true`, 加速模式对它们没有意义。两个推论: - **加速模式对联网设备不适用。** 它们始终按真实间隔上报。 - **设备被添加后停在 `idle`,必须显式 `start`** —— 默认完全不产生外部副作用。 这是本插件的一条结构性纪律:默认行为不许伸到机器外面去。 ## 构建与开发 ```powershell # 1. 安装(同时链接构建所需的框架 peer) npm install # 2. 构建:src/ -> lib/(需要 bash 与一份 DSH 源码 checkout) $env:DSH_CHECKOUT='D:\dsh\deepseek-harness'; bash scripts/build.sh # 3. 测试与类型检查(2026-10-11 实测) npm test # node --test,308 项,0 失败 npm run typecheck # tsc -p tsconfig.json --noEmit -> exit 0 npm run typecheck:client # tsc -p tsconfig.client.json --noEmit -> exit 0 ``` `scripts/build.sh` 除了编译还做三件事:链接构建依赖(`scripts/link-deps.mjs`,同时挂在 `postinstall` 上, 因为 `npm install` 会清掉它不认识的 junction)、把 `project-shim/` 复制进 `lib/project-shim/`、 写出 `lib/build-stamp.json`。那次复制是**功能性步骤,不是打包细节** —— `SimEngine` 把 Python 进程的 `PYTHONPATH` 指向 shim(`smbus` + `RPi.GPIO` + `sitecustomize`), 而 `package.json` 的 `files` 只发 `lib`,不做这一步就等于发布一个跑不起来的包。 `scripts/prepare.mjs` 让 **git 装法**也能用:`lib/` 被 gitignore,所以 git 装出来会没有入口。 它只在 `lib/index.js` 缺失时才构建,而且**构建失败只警告(exit 0)** —— 构建失败大概率是这台机器缺 bash 或缺 devDependencies,让整个 install 失败只会把锅甩给 npm。 发布产物:`npm pack` → 一个 `.tgz`,内含 `lib`、`cordis.patch.yml`、`skill`、`docs`、`models`、`scripts`。 ### 活体自检:线上跑的是你以为的那次构建吗? ```sh curl http://127.0.0.1:<端口>/@noob-stupid/dsh-breadboard/api/capabilities ``` 所有界面与 API 都挂在路由前缀 `/@noob-stupid/dsh-breadboard/` 下。`GET …/api/capabilities` 返回 `build`(**当前活着**的那份代码的构建时间戳)与 `simTools`(**实际注册成功**的工具名)。 觉得哪里不对就先看它:工具注册失败是**完全静默**的 —— 模型只会说"我没这个能力",界面上看不出任何异常; 而热重载可能报成功、实际还是旧模块在服务。 ## 已知限制 诚实清单;下面没有一条被"看着像默认值"的东西掩盖。 - **本包未发布到 npm。** 从 GitHub 装。有一个预构建的 tgz 作为发布产物,但没有 `npm install` 这一行可给。 - **界面只有中文。** 面板里今天没有语言切换,也没有英文界面文案可以承诺。 - **项目 Python 代码读 GPIO 尚未实现。** `hw_set_occlusion` 会把遮挡沿实际接线传导进联网设备的读数, 但反方向 —— 项目代码经 shim 调 `RPi.GPIO.input()` —— 会抛 `NotImplementedError`, 桥接层的 `gpio_read` / `gpio_write` 在契约里已声明、行为未实现("已定义协议但行为模型尚未实现")。 所以"固件轮询引脚再上报"这条路今天还走不通;"固件直接 HTTP 上报"这条路可以。 - **联网设备还没画进 3D 场景。** 它们已在 SSOT 里(`transport: 'network'`)、工具也完全能驱动, 但前端还没渲染它们。 - **只声称验证过的路径。** 联网设备那条链在真实后端上验证过;**I2C 形态的真实项目**还没有适配过 —— I2C 那一半目前由本仓库自带的测试项目验证。 - **模型尺寸 vs 几何保真是待决的二选一**(见 `docs/06` §6):把导入模型缩放到声明的真板 `size` 会让它轻微变形。今天的做法是保留真机尺寸、端口按变形后的间距对齐。 - **`hw_set_occlusion` 只够得着联网设备。** 若被遮挡的器件接到的对端不是带 `pinMap` 的联网设备, 什么都传导不过去 —— 这时工具会在 `reason` 里说出来,而不是静默成功。 - **部分导入模型的端口数还在收敛**(例如树莓派排针有几个脚落在 PCB 上而非引脚 mesh 上)。 端口可用,但少数锚点有零点几毫米的偏差。 ## 文档 | 文件 | 内容 | |---|---| | [docs/00-宿主API核实.md](docs/00-宿主API核实.md) | 宿主 API 核实(slot 树、路由注册、坑位) | | [docs/01-项目定位.md](docs/01-项目定位.md) | 项目定位、锁定范围 | | [docs/02-步骤①-虚拟时钟内核.md](docs/02-步骤①-虚拟时钟内核.md) | 虚拟时钟内核 | | [docs/03-模型库可行性验证.md](docs/03-模型库可行性验证.md) | 模型库可行性验证(为什么不做采集器) | | [docs/04-失败族-看起来正常实际全错.md](docs/04-失败族-看起来正常实际全错.md) | 失败族「看起来正常、实际全错」(20 例) | | [docs/05-模型来源与导入方案.md](docs/05-模型来源与导入方案.md) | 模型来源与导入方案决策 | | [docs/06-状态与交接.md](docs/06-状态与交接.md) | 状态与交接(一页纸) | | [docs/07-联网设备-第二类虚拟硬件.md](docs/07-联网设备-第二类虚拟硬件.md) | 联网设备,第二类虚拟硬件 | 这些文档是中文的工作文档(翻译它们会多出一份需要同步的副本)。本文件与 `README.md` 是成对维护的。 ## 许可证 BSD-3-Clause。 --- dsh-breadboard