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