Bilingual documentation: English default + Chinese .zh-cn.md (0.5.1)

- Every doc (README, AGENT, docs/*) now ships as English under the default
  filename and Chinese as *.zh-cn.md; both versions cross-link in their
  first line, in-document links stay within the same language
- AGENT.md §7 records the keep-both-languages-in-sync rule (same commit,
  no translating later); CHANGELOG kept in both languages
- Docs-only change; version 0.5.1 synced in the usual three places
This commit is contained in:
darceye committed 2026-10-30 21:33:38 +08:00
1 parent 8b1855b229
commit 9284a9c480
20 files changed
+1545 -527

No files matched your search

+130 -85
View File
@@ -1,105 +1,150 @@
# AGENT.md — 后续 Agent 必读(坑与约定)
**English** | [简体中文](AGENT.zh-cn.md)
本文件沉淀开发至今的全部环境坑、架构约定与验证方法。**动手前先读完本文。**
# AGENT.md — required reading for follow-up agents (gotchas & conventions)
## 0. 快速上手
This file captures every environment pitfall, architecture rule and
verification method learned so far. **Read it fully before touching anything.**
(Chinese version: AGENT.zh-cn.md — keep both in sync, see §7.)
## 0. Quick start
```bash
npm install # typescript 走 npmmirror(见 §1)
npm run build # tsc:src/ → build/(改 src 后必须;build/ 不入 git)
npm test # 单元测试 + 集成冒烟
npm install # typescript via npmmirror (see §1)
npm run build # tsc: src/ → build/ (mandatory after src changes; build/ not in git)
npm test # unit tests + integration smoke
npm run serve # http://127.0.0.1:8080/esp-emu-peris.html
npm run sync # 发布进 _tools/esp-emu-browser/www(先 build;--clean 清残留)
npm run sync # publish into _tools/esp-emu-browser/www (build first; --clean removes stale output)
```
架构与数据流见 `docs/ARCHITECTURE.md`;外设开发见 `docs/DEVICE-API.md`;
esp-emulator wasm 能力边界见 `docs/EMU-API.md` 与 `docs/FEASIBILITY.md`(M0 实测)。
Architecture & data flow: `docs/ARCHITECTURE.md`; peripheral development:
`docs/DEVICE-API.md`; esp-emulator wasm capability limits: `docs/EMU-API.md`
and `docs/FEASIBILITY.md` (M0 measurements).
## 1. 环境坑(本机)
## 1. Environment pitfalls (this machine)
- **CDN 不可达**:jsdelivr 直连被重置。npm 用镜像:
`npm --registry=https://registry.npmmirror.com install ...`;静态资源从
`https://registry.npmmirror.com/<pkg>/-/<name>-<ver>.tgz` 下 tarball 解包
(xterm 就是这样 vendor 进 `peris/vendor/` 的;包内是**非压缩**版,无 .min)。
- **Playwright 浏览器二进制**未装(网络);要装:
`PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright`。
- **ESP-IDF**:master 检出在 `C:\esp\master\esp-idf`,环境启用用
`C:\Users\A\Desktop\quick\idfm.bat`(PowerShell profile)。脚本化调用**必须清掉
`MSYSTEM` 环境变量**(Git Bash 继承它,idf.py 会拒绝),参考 `tests/firmware/build.ps1`。
- 固件加载进浏览器**必须 `idf.py merge-bin`** 的 merged image;裸 app bin 无声不启动
(症状:UART 永远无输出)。产物:`tests/firmware/peris_probe/build/merged-binary.bin`。
- `node --test <目录>` 在 Windows/Git Bash 下解析异常,**显式传 .test.mjs 文件列表**。
- **CDN unreachable**: direct jsdelivr connections are reset. Use a mirror for
npm: `npm --registry=https://registry.npmmirror.com install ...`; fetch
static assets as tarballs from
`https://registry.npmmirror.com/<pkg>/-/<name>-<ver>.tgz` and unpack
(that is how xterm was vendored into `peris/vendor/`; the package ships the
**unminified** build, no .min).
- **Playwright browser binaries** not installed (network); to install:
`PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright`.
- **ESP-IDF**: master checkout at `C:\esp\master\esp-idf`; enable the
environment with `C:\Users\A\Desktop\quick\idfm.bat` (PowerShell profile).
Scripted invocation **must clear the `MSYSTEM` env var** (Git Bash inherits
it and idf.py refuses); see `tests/firmware/build.ps1`.
- Firmware must be loaded into the browser as the **`idf.py merge-bin` merged
image**; a bare app bin silently never starts (symptom: UART stays quiet
forever). Artifact: `tests/firmware/peris_probe/build/merged-binary.bin`.
- `node --test <directory>` resolves incorrectly on Windows/Git Bash —
**pass the .test.mjs file list explicitly** (package.json does).
## 2. esp-emulator wasm 行为(实测,勿凭直觉)
## 2. esp-emulator wasm behaviour (measured — do not guess)
- **没有回调**,一切靠 `run_batch()` 批间轮询;UART 输出是 `run_batch` 的返回字符串。
- **LEDC/RMT 波形不出现在 `gpio_state()`**(它们走 GPIO matrix);只有 CPU 直写 GPIO 可见。
PWM→读 LEDC `DUTY_R`(值=duty×16),WS2812→读 RMT 通道 RAM(发送后留存)。
地址表:`src/core/mmio/`,来源与验证:FEASIBILITY.md。
- **`vTaskDelay` 期间时间快进**:睡眠窗口内引脚级波形不可捕获,与采样率无关。
CPU 活跃的位绑定波形可采样,但**同一批次间隙内连续翻转输入会丢边沿**
(press→release 必须隔批次,见 m1_smoke S3)。
- `gpio_set_input` 默认电平高(上拉);能触发固件 GPIO 中断(可靠)。
- 新 `WasmEmulator` 实例后输入电平复位 → 设备必须在 `onEmuReady()` 重驱动。
- **No callbacks**: everything is `run_batch()` polling between batches; UART
output is the return string of `run_batch`.
- **LEDC/RMT waveforms never appear in `gpio_state()`** (they go through the
GPIO matrix); only CPU-direct GPIO writes are visible.
PWM → read the LEDC `DUTY_R` (value = duty×16), WS2812 → read the RMT
channel RAM (left over after transmission).
Address tables: `src/core/mmio/`; sources & verification: FEASIBILITY.md.
- **Time fast-forwards during `vTaskDelay`**: pin-level waveforms inside a
sleep window cannot be captured, regardless of sample rate.
Waveforms of CPU-active bit-banged signals can be sampled, but **consecutive
input flips within one batch gap lose edges** (press→release must straddle
batches; see m1_smoke S3).
- `gpio_set_input` defaults to a high level (pull-up); it reliably raises the
firmware's GPIO interrupts.
- After a fresh `WasmEmulator` instance the input levels reset → devices must
re-drive in `onEmuReady()`.
## 3. 架构铁律(docs/ARCHITECTURE.md 有完整图)
## 3. Architecture iron rules (full picture in docs/ARCHITECTURE.md)
1. **模拟器只在 worker**:页面永不 import wasm;一切经 `src/ui/worker-client.ts`。
2. **worker 里唯一允许复刻上游逻辑的文件是 `src/core/worker.ts`**,改动必须带
`peris:` 标记,便于上游升级 diff(docs/UPGRADE.md 流程)。
3. **设备插件是原生 JS**(丢文件即用,不参与构建);框架是 TS。别给 devices/ 加
import 依赖到 src/ 运行时模块(可用 `build/core/*.d.ts` 类型做 JSDoc 提示)。
4. 插件元数据必须含 `static pins(cfg)`(冲突检测)、`typeId`、`title`、
`defaultConfig`,可选 `version`;registry 加载时校验,**缺了当场报错**。
5. 配置以页面 `PerisPanel.config` 为唯一事实源,变更全量下发+localStorage;
不要在 worker 侧缓存/修改配置。
6. 状态推送有 33ms 节流 + **尾随定时器**(保证最后一次 dirty 必达)——别删掉尾随逻辑,
那是修过的真实缺陷(状态静默丢失)。
7. sigbus 按批做 gpio_state 差分,**联合掩码**(out|oe)每引脚每批至多通知一次。
8. **同语义代码必须复用,禁止复制一份再改**:同一逻辑只允许一份实现,所有
入口(行内 chip / 详情编辑器 / 行图标 / 新插件)调用同一函数——
GPIO 可选项= `config-editor.ts fillPinOptions()`;V·G 徽章推导=`rowChips()`;
插件多视觉元素(如 LED 灯泡+行图标)共用同一 setter/render,不许给图标
单写一份样式逻辑;面板对 View 固定 **先 `rowIcon` 后 `setConfig`**,
保证配置式样能覆盖到图标。改行为前先找有没有既有函数,有就复用;
review 发现两处雷同代码时,先抽公共函数再改。
1. **The emulator lives only in the worker**: the page never imports the wasm;
everything goes through `src/ui/worker-client.ts`.
2. **The only file allowed to replicate upstream logic is `src/core/worker.ts`**
— every change there must carry a `peris:` marker for upstream-upgrade
diffs (procedure in docs/UPGRADE.md).
3. **Device plugins are plain JS** (drop a file and it works, no build); the
framework is TypeScript. Do not add imports from devices/ into src/ runtime
modules (compiled `build/core/*.d.ts` types may be used for JSDoc hints).
4. Plugin metadata must include `static pins(cfg)` (conflict detection),
`typeId`, `title`, `defaultConfig`, optional `version`; the registry
validates at load time and **errors out immediately when missing**.
5. The page's `PerisPanel.config` is the single source of truth for
configuration: every change is sent in full + localStorage; never cache or
mutate config on the worker side.
6. State pushes have a 33ms throttle + a **trailing timer** (guarantees the
last dirty flag is always delivered) — do not remove the trailing flush;
it fixed a real defect (silently dropped state).
7. sigbus diffs `gpio_state` per batch with the **union mask** (out|oe),
notifying each pin at most once per batch.
8. **Same-semantic code must be reused — never copy and modify**: one logic,
one implementation, and every entry point (row chip / details editor / row
icon / new plugin) calls the same function — GPIO options =
`fillPinOptions()` in `config-editor.ts`; V·G badge derivation =
`rowChips()`; a plugin's multiple visuals (LED bulb + row icon) share one
setter/render — never write a second styling path for the icon; the panel
calls View hooks in a fixed order, **`rowIcon` before `setConfig`**, so
config-driven styling covers the icon too. Before changing behaviour, look
for an existing function and reuse it; when review finds duplicated code,
extract the shared function first, then change it.
## 4. 曾踩过的 bug(防止回潮)
## 4. Past bugs (prevent regressions)
| bug | 教训 |
| bug | lesson |
|---|---|
| registry 把 manifest 模块路径解析到 `peris/` 根 | manifest 路径相对 devices/ 目录,编译位置变化要同步改 `DEVICES_DIR` |
| 设备构造函数里 `ctx.subscribe()` 时实例未注册 | sigbus 存**取值函数**惰性解析,不要存实例 |
| 错误条 `style.display=''` 回退 CSS `none` 永不可见 | 显式 `display='block'`,且无错时清空 textContent(避免读到陈旧报错) |
| `peris_config` 早于 registry init 到达 → 全部 unknown type | `applyConfig` 先 await init |
| 设备漏写 `static pins()` → 冲突检测静默失效 | registry 强制校验 + 单测覆盖 |
| 按键 activeLow 电平写反 | idle=高、pressed=低(active-low 上拉接法);单测固定语义 |
| Worker URL 相对**页面**解析,不是相对 import 的模块 | worker-client 里写页面相对路径 `build/core/worker.js` |
| 节流窗口丢最后一次状态 | 尾随 flush(见 §3.7) |
| LED 行图标颜色与详情灯泡不一致(0.3.0) | 面板曾先 setConfig 后 rowIcon,且插件两处各自设色;统一 setColour + 固定调用顺序(§3.8) |
| 换芯片加载固件后,详情编辑器 GPIO 选项仍是旧芯片(0.3.0) | 编辑器把 validPins 快照在构建时;芯片变化必须重建编辑器;选项构建统一走 fillPinOptions(§3.8) |
| registry resolved manifest module paths to the `peris/` root | manifest paths are relative to the devices/ dir; update `DEVICES_DIR` when compile locations move |
| `ctx.subscribe()` in a device constructor ran before the instance was registered | sigbus stores a **getter function** for lazy resolution — never the instance |
| error bar `style.display=''` fell back to CSS `none`, never visible | set `display='block'` explicitly, and clear textContent when there is no error (stale messages) |
| `peris_config` arrived before registry init → everything "unknown type" | `applyConfig` awaits init first |
| a device forgot `static pins()` → conflict detection silently off | registry enforces + unit-tested |
| button activeLow levels inverted | idle = high, pressed = low (active-low pull-up wiring); semantics pinned by unit tests |
| worker URL resolved relative to the **page**, not the importing module | worker-client hard-codes the page-relative path `build/core/worker.js` |
| throttle window dropped the last state | trailing flush (see §3.6) |
| LED row-icon colour disagreed with the details bulb (0.3.0) | the panel called setConfig before rowIcon and the plugin styled the two in separate places; unified `setColour` + fixed call order (§3.8) |
| after loading firmware for another chip, the details editor kept the old chip's GPIO options (0.3.0) | the editor snapshot `validPins` at build time; chip changes must rebuild editors, and option building goes through `fillPinOptions` (§3.8) |
## 5. 测试与验证约定
## 5. Testing & verification conventions
- **单元测试**(node:test,零依赖):纯逻辑——sigbus 位运算、电平语义、配置校验、
MMIO 地址表。改 core/devices 必跑;新增设备请仿照 `tests/unit/devices.test.mjs`
补电平/状态语义用例。
- **集成冒烟** `tests/m1_smoke.mjs`:真实 wasm+固件+runtime+设备。**时序经验**:
- boot 约占 500+ 批(50k 指令/批),别盲选批次号,**按 UART 标记驱动**
(固件用 `###P1 start/end` 等标记,见 `tests/firmware/peris_probe/main/main.c`);
- 固件在每轮 P1 start 把按键计数**清零**,计数可能只出现在 `###P1 end btn=N` 行,
断言正则别只匹配 `i=` 行;
- flushStates 有节流,读状态断言前 `await` ~60ms。
- **M0 探针** `tests/m0_probe.mjs`:上游 wasm 升级后**第一个跑**的东西,六项
结论(Q1a..Q3b)若有变化先改 FEASIBILITY.md/EMU-API.md 再动其它代码。
- **GUI 冒测**(手动,清单在 ARCHITECTURE.md §5):本会话所用 IAB 自动化易劣化
(点击 actionability 超时、截图 surface 失败、xterm DOM 只渲染视口行且 rAF
停滞时整块冻结)——**DOM 只读断言最可靠**;固件注入可用页面自身 drop 处理器
(同源 fetch → File → DataTransfer → drop 事件)作为环境准备。
- **Unit tests** (node:test, zero deps): pure logic — sigbus bit math, level
semantics, config validation, MMIO address tables. Mandatory after changing
core/devices; for a new device, follow `tests/unit/devices.test.mjs` and add
level/state semantics cases.
- **Integration smoke** `tests/m1_smoke.mjs`: real wasm + firmware + runtime +
devices. **Timing lessons**:
- boot takes ~500+ batches (50k instr/batch) — do not hard-code batch
numbers, **drive by UART markers** (the firmware prints `###P1 start/end`
etc., see `tests/firmware/peris_probe/main/main.c`);
- the firmware resets the button counter at each P1 start, so the count may
only appear on the `###P1 end btn=N` line — do not match only `i=` lines;
- flushStates is throttled; `await` ~60ms before asserting on state.
- **M0 probe** `tests/m0_probe.mjs`: after an upstream wasm upgrade this is
**the first thing to run**; if any of the six conclusions (Q1a..Q3b)
changed, update FEASIBILITY.md/EMU-API.md before touching other code.
- **GUI smoke** (manual; checklist in ARCHITECTURE.md §5): the IAB automation
used in past sessions degrades easily (click actionability timeouts,
screenshot surface failures, xterm DOM rendering only viewport rows and
freezing whole blocks when rAF stalls) — **read-only DOM assertions are the
most reliable**; firmware injection can use the page's own drop handler
(same-origin fetch → File → DataTransfer → drop event) as environment setup.
## 6. 版本与发布
## 6. Versioning & releases
- 项目版本:`src/core/version.ts` + `package.json` + `docs/CHANGELOG.md` 三处同步。
- 设备版本:各插件 `static version`,卡片头展示;CHANGELOG 有设备版本表。
- 发进 www 用 `npm run sync`(--clean 清旧产物);www 原版文件(index.html/app.js/
worker.js/pkg/serve.py)**永远不动**。
- Project version: synced in three places — `src/core/version.ts` +
`package.json` + `docs/CHANGELOG.md`.
- Device versions: each plugin's `static version`, shown in the row header;
CHANGELOG keeps a device version table.
- Publish into www with `npm run sync` (--clean removes stale output); the
stock www files (index.html/app.js/worker.js/pkg/serve.py) are **never
touched**.
## 7. Documentation conventions (bilingual)
- Every document ships in two languages: **English under the default
filename**, **Chinese as `*.zh-cn.md`** (README/AGENT/docs/*); the two
versions cross-link in their first line, and in-document links stay within
the same language.
- Any documentation change must be applied to **both languages** in the same
commit — no translating later.
+116
View File
@@ -0,0 +1,116 @@
[English](AGENT.md) | **简体中文**
# AGENT.md — 后续 Agent 必读(坑与约定)
本文件沉淀开发至今的全部环境坑、架构约定与验证方法。**动手前先读完本文。**
(英文版:AGENT.md——两种语言需同步修改,见 §7。)
## 0. 快速上手
```bash
npm install # typescript 走 npmmirror(见 §1)
npm run build # tsc:src/ → build/(改 src 后必须;build/ 不入 git)
npm test # 单元测试 + 集成冒烟
npm run serve # http://127.0.0.1:8080/esp-emu-peris.html
npm run sync # 发布进 _tools/esp-emu-browser/www(先 build;--clean 清残留)
```
架构与数据流见 `docs/ARCHITECTURE.zh-cn.md`;外设开发见 `docs/DEVICE-API.zh-cn.md`;
esp-emulator wasm 能力边界见 `docs/EMU-API.zh-cn.md` 与 `docs/FEASIBILITY.zh-cn.md`(M0 实测)。
## 1. 环境坑(本机)
- **CDN 不可达**:jsdelivr 直连被重置。npm 用镜像:
`npm --registry=https://registry.npmmirror.com install ...`;静态资源从
`https://registry.npmmirror.com/<pkg>/-/<name>-<ver>.tgz` 下 tarball 解包
(xterm 就是这样 vendor 进 `peris/vendor/` 的;包内是**非压缩**版,无 .min)。
- **Playwright 浏览器二进制**未装(网络);要装:
`PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright`。
- **ESP-IDF**:master 检出在 `C:\esp\master\esp-idf`,环境启用用
`C:\Users\A\Desktop\quick\idfm.bat`(PowerShell profile)。脚本化调用**必须清掉
`MSYSTEM` 环境变量**(Git Bash 继承它,idf.py 会拒绝),参考 `tests/firmware/build.ps1`。
- 固件加载进浏览器**必须 `idf.py merge-bin`** 的 merged image;裸 app bin 无声不启动
(症状:UART 永远无输出)。产物:`tests/firmware/peris_probe/build/merged-binary.bin`。
- `node --test <目录>` 在 Windows/Git Bash 下解析异常,**显式传 .test.mjs 文件列表**
(package.json 已如此)。
## 2. esp-emulator wasm 行为(实测,勿凭直觉)
- **没有回调**,一切靠 `run_batch()` 批间轮询;UART 输出是 `run_batch` 的返回字符串。
- **LEDC/RMT 波形不出现在 `gpio_state()`**(它们走 GPIO matrix);只有 CPU 直写 GPIO 可见。
PWM→读 LEDC `DUTY_R`(值=duty×16),WS2812→读 RMT 通道 RAM(发送后留存)。
地址表:`src/core/mmio/`,来源与验证:FEASIBILITY.zh-cn.md。
- **`vTaskDelay` 期间时间快进**:睡眠窗口内引脚级波形不可捕获,与采样率无关。
CPU 活跃的位绑定波形可采样,但**同一批次间隙内连续翻转输入会丢边沿**
(press→release 必须隔批次,见 m1_smoke S3)。
- `gpio_set_input` 默认电平高(上拉);能触发固件 GPIO 中断(可靠)。
- 新 `WasmEmulator` 实例后输入电平复位 → 设备必须在 `onEmuReady()` 重驱动。
## 3. 架构铁律(docs/ARCHITECTURE.zh-cn.md 有完整图)
1. **模拟器只在 worker**:页面永不 import wasm;一切经 `src/ui/worker-client.ts`。
2. **worker 里唯一允许复刻上游逻辑的文件是 `src/core/worker.ts`**,改动必须带
`peris:` 标记,便于上游升级 diff(docs/UPGRADE.zh-cn.md 流程)。
3. **设备插件是原生 JS**(丢文件即用,不参与构建);框架是 TS。别给 devices/ 加
import 依赖到 src/ 运行时模块(可用 `build/core/*.d.ts` 类型做 JSDoc 提示)。
4. 插件元数据必须含 `static pins(cfg)`(冲突检测)、`typeId`、`title`、
`defaultConfig`,可选 `version`;registry 加载时校验,**缺了当场报错**。
5. 配置以页面 `PerisPanel.config` 为唯一事实源,变更全量下发+localStorage;
不要在 worker 侧缓存/修改配置。
6. 状态推送有 33ms 节流 + **尾随定时器**(保证最后一次 dirty 必达)——别删掉尾随逻辑,
那是修过的真实缺陷(状态静默丢失)。
7. sigbus 按批做 gpio_state 差分,**联合掩码**(out|oe)每引脚每批至多通知一次。
8. **同语义代码必须复用,禁止复制一份再改**:同一逻辑只允许一份实现,所有
入口(行内 chip / 详情编辑器 / 行图标 / 新插件)调用同一函数——
GPIO 可选项= `config-editor.ts fillPinOptions()`;V·G 徽章推导=`rowChips()`;
插件多视觉元素(如 LED 灯泡+行图标)共用同一 setter/render,不许给图标
单写一份样式逻辑;面板对 View 固定 **先 `rowIcon` 后 `setConfig`**,
保证配置式样能覆盖到图标。改行为前先找有没有既有函数,有就复用;
review 发现两处雷同代码时,先抽公共函数再改。
## 4. 曾踩过的 bug(防止回潮)
| bug | 教训 |
|---|---|
| registry 把 manifest 模块路径解析到 `peris/` 根 | manifest 路径相对 devices/ 目录,编译位置变化要同步改 `DEVICES_DIR` |
| 设备构造函数里 `ctx.subscribe()` 时实例未注册 | sigbus 存**取值函数**惰性解析,不要存实例 |
| 错误条 `style.display=''` 回退 CSS `none` 永不可见 | 显式 `display='block'`,且无错时清空 textContent(避免读到陈旧报错) |
| `peris_config` 早于 registry init 到达 → 全部 unknown type | `applyConfig` 先 await init |
| 设备漏写 `static pins()` → 冲突检测静默失效 | registry 强制校验 + 单测覆盖 |
| 按键 activeLow 电平写反 | idle=高、pressed=低(active-low 上拉接法);单测固定语义 |
| Worker URL 相对**页面**解析,不是相对 import 的模块 | worker-client 里写页面相对路径 `build/core/worker.js` |
| 节流窗口丢最后一次状态 | 尾随 flush(见 §3.6) |
| LED 行图标颜色与详情灯泡不一致(0.3.0) | 面板曾先 setConfig 后 rowIcon,且插件两处各自设色;统一 setColour + 固定调用顺序(§3.8) |
| 换芯片加载固件后,详情编辑器 GPIO 选项仍是旧芯片(0.3.0) | 编辑器把 validPins 快照在构建时;芯片变化必须重建编辑器;选项构建统一走 fillPinOptions(§3.8) |
## 5. 测试与验证约定
- **单元测试**(node:test,零依赖):纯逻辑——sigbus 位运算、电平语义、配置校验、
MMIO 地址表。改 core/devices 必跑;新增设备请仿照 `tests/unit/devices.test.mjs`
补电平/状态语义用例。
- **集成冒烟** `tests/m1_smoke.mjs`:真实 wasm+固件+runtime+设备。**时序经验**:
- boot 约占 500+ 批(50k 指令/批),别盲选批次号,**按 UART 标记驱动**
(固件用 `###P1 start/end` 等标记,见 `tests/firmware/peris_probe/main/main.c`);
- 固件在每轮 P1 start 把按键计数**清零**,计数可能只出现在 `###P1 end btn=N` 行,
断言正则别只匹配 `i=` 行;
- flushStates 有节流,读状态断言前 `await` ~60ms。
- **M0 探针** `tests/m0_probe.mjs`:上游 wasm 升级后**第一个跑**的东西,六项
结论(Q1a..Q3b)若有变化先改 FEASIBILITY/EMU-API 文档再动其它代码。
- **GUI 冒测**(手动,清单在 ARCHITECTURE §5):本会话所用 IAB 自动化易劣化
(点击 actionability 超时、截图 surface 失败、xterm DOM 只渲染视口行且 rAF
停滞时整块冻结)——**DOM 只读断言最可靠**;固件注入可用页面自身 drop 处理器
(同源 fetch → File → DataTransfer → drop 事件)作为环境准备。
## 6. 版本与发布
- 项目版本:`src/core/version.ts` + `package.json` + `docs/CHANGELOG.zh-cn.md`(英文版
CHANGELOG.md 同步)三处同步。
- 设备版本:各插件 `static version`,行头展示;CHANGELOG 有设备版本表。
- 发进 www 用 `npm run sync`(--clean 清旧产物);www 原版文件(index.html/app.js/
worker.js/pkg/serve.py)**永远不动**。
## 7. 文档约定(双语)
- 所有文档两种语言:**英文版用默认文件名**,**中文版 `*.zh-cn.md`**
(README/AGENT/docs/*);两版第一行互链,文内链接保持同语言跳转。
- 任何文档改动必须在**同一次提交**里落到两种语言——不许"以后再翻译"。
+64 -51
View File
@@ -1,74 +1,86 @@
**English** | [简体中文](README.zh-cn.md)
# esp-emu-peris
为乐鑫 [esp-emulator](https://github.com/espressif/esp-emulator) 浏览器形态(wasm)做的**模拟外设面板**:LED、按键、触摸按键、拨动开关……以插件方式不断添加,不需要改动官方 `www/` 里的任何基础文件。
**Simulated peripherals** for the browser (wasm) build of Espressif's
[esp-emulator](https://github.com/espressif/esp-emulator): LEDs, push buttons,
touch buttons, toggle switches… — added as plugins, without touching any stock
file in the official `www/`.
```
esp-emu-peris.html 入口页面(UART0 控制台 | 外设行式面板 | Debug,三栏同屏)
src/ TypeScript 源码(npm run build → build/)
core/ worker 侧框架:version/types/protocol/chips/mmio/sigbus/registry/runtime/worker
ui/ 页面侧模块:app/worker-client/terminal/chart/debug-panels/peris-panel/config-editor
build/ 编译产物(tsc,未入 git;core/ + ui/)
esp-emu-peris.html entry page (UART0 console | peripherals rows | Debug — three panes at once)
src/ TypeScript sources (npm run build → build/)
core/ worker-side framework: version/types/protocol/chips/mmio/sigbus/registry/runtime/worker
ui/ page-side modules: app/worker-client/terminal/chart/debug-panels/peris-panel/config-editor
build/ compiler output (tsc, not in git; core/ + ui/)
peris/
devices/ 外设插件(原生 JS,丢文件即用,含各自版本号):led/button/touch-button/toggle-switch
vendor/ peris.css xterm 本地化 + 面板样式
examples/ 接线配置示例(JSON,可直接 Import)
tests/unit/ 单元测试(node:test)
tests/m1_smoke.mjs 集成冒烟(真实 wasm+固件全链路)
tests/m0_probe.mjs 模拟器能力探针(上游升级时先跑)
tests/firmware/ ESP-IDF 测试固件
devices/ peripheral plugins (plain JS, drop-in, each with its own version): led/button/touch-button/toggle-switch
vendor/ peris.css vendored xterm + panel styles
examples/ wiring-config examples (JSON, directly importable)
tests/unit/ unit tests (node:test)
tests/m1_smoke.mjs integration smoke test (real wasm + firmware, full chain)
tests/m0_probe.mjs emulator capability probe (run first on upstream upgrades)
tests/firmware/ ESP-IDF test firmware
docs/ ARCHITECTURE / EMU-API / FEASIBILITY / DEVICE-API / CHANGELOG / UPGRADE / ROADMAP
AGENT.md 环境/架构/坑(贡献前必读)
AGENT.md environment/architecture/gotchas (read before contributing)
```
架构、数据流、状态归属与扩展点见 **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)**;
版本规则见 **[docs/CHANGELOG.md](docs/CHANGELOG.md)**(项目版本三处同步,设备有独立版本)。
Architecture, data flow, state ownership and extension points:
**[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)**; versioning rules:
**[docs/CHANGELOG.md](docs/CHANGELOG.md)** (project version synced in three
places; devices carry their own versions).
## 快速开始
## Quick start
```bash
# 一次性:npm 依赖(自动用 npmmirror 源装 typescript)
# one-time: npm deps (typescript installs from the npmmirror registry)
npm install
# 开发循环:改 src/ → 构建 → 测试 → 起 merged 开发服务器
# dev loop: edit src/ → build → test → merged dev server
npm run build
npm test
npm run serve # http://127.0.0.1:8080/esp-emu-peris.html
# 浏览器打开上面的地址,选择芯片、拖入 merged flash image(.bin)
# 注意:必须是 idf.py merge-bin 生成的完整镜像,裸 app bin 无法启动
# open the URL above in a browser, pick a chip, drop a merged flash image (.bin)
# NOTE: it must be a full image produced by idf.py merge-bin — a bare app bin
# will not boot
# 外设栏(控制台右侧):Add 添加 LED/按键等;每外设一行——行内图标可交互
# (LED 点击换色/按键按压),名称、GPIO 方框、V/G 电平徽章点击即改,
# ▸ 展开详情与高级配置,⠿ 拖动排序;面板头部:项目名输入框 + 保存/打开图标
# (导出/导入接线配置 JSON,自动存 localStorage 并记忆芯片,下次打开自动
# 选中)
# peripherals pane (right of the console): the + icon adds LEDs/buttons/etc.
# Each device is one row — the row icon is interactive (LED click changes
# colour / button press), name, GPIO box and V/G pull badge edit on click,
# ▸ expands details & advanced config, ⠿ drags to reorder. Panel head:
# project-name input + save/open icons (export/import the wiring config JSON,
# autosaved to localStorage and remembering the chip, auto-selected next time)
```
把外设面板**发布**进官方 www 目录(只增不改;先 build):
**Publish** the peripherals panel into the official www directory (add-only,
never modifies; build first):
```bash
npm run sync # 复制入口 html + peris/(含编译产物)+ examples/
npm run sync # copies the entry html + peris/ (incl. build output) + examples/
```
## 能力与限制(实测结论,详见 docs/FEASIBILITY.md)
## Capabilities & limits (measured; details in docs/FEASIBILITY.md)
| 外设 | 状态 | 途径 |
| Peripheral | Status | Approach |
|---|---|---|
| 各色 LED | ✅ M1 | 轮询 GPIO 输出寄存器 |
| 物理按键 / 触摸按键(GPIO 语义)/ 拨动开关 | ✅ M1 | 驱动引脚输入,固件可读到并触发中断 |
| PWM 调光 LED / RGB | 🔜 M2 | 读 LEDC duty 寄存器(引脚级 PWM 不可见) |
| WS2812 幻彩灯 | 🔜 M3 | 读 RMT 通道 RAM 解码(引脚级波形不可见) |
| 位绑定 I2C/SPI 设备、IR | 🔜 M4 | 高速引脚采样(需固件位绑定) |
| 硬件 I2C/SPI 设备、显示屏、ADC、真触摸、I2S、SD | ⏸ M5 | 等 esp-emulator 上游暴露 API |
| LEDs, any colour | ✅ M1 | polls the GPIO output registers |
| Push button / touch button (GPIO semantics) / toggle switch | ✅ M1 | drives pin input; firmware reads it and gets interrupts |
| PWM dimmed LED / RGB | 🔜 M2 | reads LEDC duty registers (pin-level PWM is invisible) |
| WS2812 addressable LEDs | 🔜 M3 | decodes from RMT channel RAM (pin-level waveform invisible) |
| Bit-banged I2C/SPI devices, IR | 🔜 M4 | high-rate pin sampling (requires bit-banged firmware) |
| Hardware I2C/SPI devices, displays, ADC, real touch, I2S, SD | ⏸ M5 | waiting for esp-emulator upstream APIs |
注:触摸**外设**未被模拟——触摸按键以 GPIO 输入呈现,固件按 GPIO 读取即可
(`tests/firmware/peris_probe` 有示例:GPIO3 输入 + 下降沿 ISR)。
Note: the touch **peripheral** itself is not emulated — the touch button
appears as a GPIO input that firmware reads as plain GPIO
(`tests/firmware/peris_probe` has an example: GPIO3 input + falling-edge ISR).
## 添加一个新外设
## Adding a new peripheral
1. 在 `peris/devices/` 放一个模块,导出 `Device` 与 `View`(接口见
`peris/device-api.js` 顶部注释或 docs/DEVICE-API.md);
2. 在 `peris/devices/manifest.json` 加一行。完成——页面和 worker 会自动加载。
1. Drop a module into `peris/devices/` exporting `Device` and `View`
(interface: docs/DEVICE-API.md);
2. Add one line to `peris/devices/manifest.json`. Done — page and worker pick
it up automatically.
```js
// peris/devices/my-dev.js
@@ -88,22 +100,23 @@ export class View {
}
```
## 测试固件与测试
## Test firmware & tests
```powershell
# 构建固件(用 C:\Users\A\Desktop\quick\idfm.bat 的 IDF 环境)
# build the firmware (IDF env from C:\Users\A\Desktop\quick\idfm.bat)
powershell -ExecutionPolicy Bypass -NoProfile -File tests\firmware\build.ps1
# 生成 merged image(加载用):
# merged image for loading:
# tests/firmware/peris_probe/build/merged-binary.bin
```
```bash
npm test # 单元测试 + 集成冒烟(见 docs/ARCHITECTURE.md 测试分层)
npm run test:m0 # 模拟器能力探针(上游 wasm 升级时先跑)
npm test # unit + integration smoke (test layering in docs/ARCHITECTURE.md)
npm run test:m0 # emulator capability probe (run first on upstream upgrades)
```
固件三阶段循环:GPIO8 闪烁 / GPIO9 LEDC 呼吸 / GPIO4 RMT-WS2812 发帧,
并配合 GPIO3 输入中断计数,用于端到端验证。
The firmware runs a three-phase loop: GPIO8 blink / GPIO9 LEDC breathing /
GPIO4 RMT-WS2812 frames, plus GPIO3 input-interrupt counting, for end-to-end
verification.
注:页面自身不依赖任何 CDN——xterm/xterm-addon-fit 已本地化到
`peris/vendor/`(取自 npm 包的非压缩构建),离线可用。
Note: the page itself has no CDN dependency — xterm/xterm-addon-fit are
vendored into `peris/vendor/` (unminified npm builds), so it works offline.
+111
View File
@@ -0,0 +1,111 @@
[English](README.md) | **简体中文**
# esp-emu-peris
为乐鑫 [esp-emulator](https://github.com/espressif/esp-emulator) 浏览器形态(wasm)做的**模拟外设面板**:LED、按键、触摸按键、拨动开关……以插件方式不断添加,不需要改动官方 `www/` 里的任何基础文件。
```
esp-emu-peris.html 入口页面(UART0 控制台 | 外设行式面板 | Debug,三栏同屏)
src/ TypeScript 源码(npm run build → build/)
core/ worker 侧框架:version/types/protocol/chips/mmio/sigbus/registry/runtime/worker
ui/ 页面侧模块:app/worker-client/terminal/chart/debug-panels/peris-panel/config-editor
build/ 编译产物(tsc,未入 git;core/ + ui/)
peris/
devices/ 外设插件(原生 JS,丢文件即用,含各自版本号):led/button/touch-button/toggle-switch
vendor/ peris.css xterm 本地化 + 面板样式
examples/ 接线配置示例(JSON,可直接 Import)
tests/unit/ 单元测试(node:test)
tests/m1_smoke.mjs 集成冒烟(真实 wasm+固件全链路)
tests/m0_probe.mjs 模拟器能力探针(上游升级时先跑)
tests/firmware/ ESP-IDF 测试固件
docs/ ARCHITECTURE / EMU-API / FEASIBILITY / DEVICE-API / CHANGELOG / UPGRADE / ROADMAP
AGENT.md 环境/架构/坑(贡献前必读)
```
架构、数据流、状态归属与扩展点见 **[docs/ARCHITECTURE.zh-cn.md](docs/ARCHITECTURE.zh-cn.md)**;
版本规则见 **[docs/CHANGELOG.zh-cn.md](docs/CHANGELOG.zh-cn.md)**(项目版本三处同步,设备有独立版本)。
## 快速开始
```bash
# 一次性:npm 依赖(自动用 npmmirror 源装 typescript)
npm install
# 开发循环:改 src/ → 构建 → 测试 → 起 merged 开发服务器
npm run build
npm test
npm run serve # http://127.0.0.1:8080/esp-emu-peris.html
# 浏览器打开上面的地址,选择芯片、拖入 merged flash image(.bin)
# 注意:必须是 idf.py merge-bin 生成的完整镜像,裸 app bin 无法启动
# 外设栏(控制台右侧):+ 图标添加 LED/按键等;每外设一行——行内图标可交互
# (LED 点击换色/按键按压),名称、GPIO 方框、V/G 电平徽章点击即改,
# ▸ 展开详情与高级配置,⠿ 拖动排序;面板头部:项目名输入框 + 保存/打开图标
# (导出/导入接线配置 JSON,自动存 localStorage 并记忆芯片,下次打开自动
# 选中)
```
把外设面板**发布**进官方 www 目录(只增不改;先 build):
```bash
npm run sync # 复制入口 html + peris/(含编译产物)+ examples/
```
## 能力与限制(实测结论,详见 docs/FEASIBILITY.zh-cn.md)
| 外设 | 状态 | 途径 |
|---|---|---|
| 各色 LED | ✅ M1 | 轮询 GPIO 输出寄存器 |
| 物理按键 / 触摸按键(GPIO 语义)/ 拨动开关 | ✅ M1 | 驱动引脚输入,固件可读到并触发中断 |
| PWM 调光 LED / RGB | 🔜 M2 | 读 LEDC duty 寄存器(引脚级 PWM 不可见) |
| WS2812 幻彩灯 | 🔜 M3 | 读 RMT 通道 RAM 解码(引脚级波形不可见) |
| 位绑定 I2C/SPI 设备、IR | 🔜 M4 | 高速引脚采样(需固件位绑定) |
| 硬件 I2C/SPI 设备、显示屏、ADC、真触摸、I2S、SD | ⏸ M5 | 等 esp-emulator 上游暴露 API |
注:触摸**外设**未被模拟——触摸按键以 GPIO 输入呈现,固件按 GPIO 读取即可
(`tests/firmware/peris_probe` 有示例:GPIO3 输入 + 下降沿 ISR)。
## 添加一个新外设
1. 在 `peris/devices/` 放一个模块,导出 `Device` 与 `View`(接口见
docs/DEVICE-API.zh-cn.md);
2. 在 `peris/devices/manifest.json` 加一行。完成——页面和 worker 会自动加载。
```js
// peris/devices/my-dev.js
export class Device {
static typeId = 'my-dev'; static title = 'My Device'; static category = 'misc';
static defaultConfig = { name: 'My', pin: 6 };
static configSchema = [{ key: 'pin', label: 'GPIO pin', type: 'pin' }];
static pins(cfg) { return [cfg.pin]; }
constructor(ctx, cfg) { this.ctx = ctx; ctx.subscribe([cfg.pin]); }
onPinState(pin, s) { this.on = s.oe && s.out; this.ctx.markDirty(); }
getState() { return { on: this.on }; }
destroy() { this.ctx.unsubscribe(); }
}
export class View {
constructor(el, sendCmd) { el.textContent = 'hello'; }
render(state) { }
}
```
## 测试固件与测试
```powershell
# 构建固件(用 C:\Users\A\Desktop\quick\idfm.bat 的 IDF 环境)
powershell -ExecutionPolicy Bypass -NoProfile -File tests\firmware\build.ps1
# 生成 merged image(加载用):
# tests/firmware/peris_probe/build/merged-binary.bin
```
```bash
npm test # 单元测试 + 集成冒烟(见 docs/ARCHITECTURE.zh-cn.md 测试分层)
npm run test:m0 # 模拟器能力探针(上游升级时先跑)
```
固件三阶段循环:GPIO8 闪烁 / GPIO9 LEDC 呼吸 / GPIO4 RMT-WS2812 发帧,
并配合 GPIO3 输入中断计数,用于端到端验证。
注:页面自身不依赖任何 CDN——xterm/xterm-addon-fit 已本地化到
`peris/vendor/`(取自 npm 包的非压缩构建),离线可用。
+107 -77
View File
@@ -1,104 +1,134 @@
# esp-emu-peris 系统架构
**English** | [简体中文](ARCHITECTURE.zh-cn.md)
## 1. 分层总览
# esp-emu-peris system architecture
## 1. Layered overview
```
┌───────────────────────────────── 页面(page)─────────────────────────────────┐
│ esp-emu-peris.html 上游样式原块(便于升级 diff)+ <link peris/peris.css> │
│ 三栏同屏:UART0 控制台 │ 外设面板 │ Debug(0.3.0 起不再标签切换) │
│ build/ui/* (TypeScript 编译产物) │
│ app.ts 编排器:机器状态、Run/Pause/固件/网络控件、消息路由 │
│ worker-client Worker 唯一入口(其它模块一律经由它收发消息) │
│ terminal / chart / debug-panels UART 控制台 / 性能图 / Debug 只读面板 │
│ peris-panel + config-editor 行式外设列表(图标/名称/GPIO/V·G 徽章 │
│ + 展开/删除/拖动)与详情配置编辑器 │
┌───────────────────────────────── page ──────────────────────────────────────┐
│ esp-emu-peris.html stock style block kept verbatim (easy upgrade diff) │
│ + <link peris/peris.css> │
│ three panes at once: UART0 console | peripherals | debug (no tabs since 0.3)│
│ build/ui/* (TypeScript compiler output) │
│ app.ts orchestrator: machine state, Run/Pause/firmware/network │
│ controls, message routing │
│ worker-client the only Worker entry (all other modules go through it) │
│ terminal / chart / debug-panels UART console / perf chart / read-only │
│ debug panels │
│ peris-panel + config-editor row-based peripherals list │
│ (icon/name/GPIO/V·G badge │
│ + expand/remove/drag-sort) and the │
│ details config editor │
└───────────────▲───────────────────────────────────────────────▲────────────┘
postMessage(协议见 core/protocol.ts) │ DOM
postMessage (protocol in core/protocol.ts) │ DOM
┌───────────────┴───────────────────────────────────────────────▼────────────┐
│ 页面侧设备 View(peris/devices/*.js 的 View 类,由 peris-panel 实例化) │
│ page-side device Views (View classes of peris/devices/*.js, instantiated │
│ by peris-panel) │
├─────────────────────────────────────────────────────────────────────────────┤
│ ← peris_state / peris_command → │
├─────────────────────────────────────────────────────────────────────────────┤
│ build/core/worker.ts 上游 worker 复刻 + 6 处 peris: 钩子(唯一可碰上游逻辑) │
│ build/core/runtime.ts 设备宿主:配置校验/实例化/命令/状态节流推送 │
│ build/core/sigbus.ts 引脚信号总线:每批 gpio_state() 差分分发 │
│ build/core/registry.ts 插件注册表(manifest 驱动 + 元数据校验) │
│ build/core/{version,types,protocol,chips,mmio}.ts 版本/契约/消息/芯片表/寄存器表 │
│ worker 侧设备 Device(peris/devices/*.js 的 Device 类) │
└───────────────────────────┬─────────────────────────────────────────────────┘
│ WasmEmulator(闭源 wasm,只此一处触碰)
esp_emu_bg.wasm v0.51.0
│ build/core/worker.ts upstream worker replica + 6 peris: hooks (the only │
│ file allowed to touch upstream logic) │
│ build/core/runtime.ts device host: config validation/instantiation/commands │
│ /throttled state pushes │
│ build/core/sigbus.ts pin signal bus: per-batch gpio_state() diff dispatch │
│ build/core/registry.ts plugin registry (manifest-driven + meta validation) │
│ build/core/{version,types,protocol,chips,mmio}.ts version/contracts/ │
│ messages/chip tables/register tables │
│ worker-side devices (Device classes of peris/devices/*.js) │
└───────────────────────────────────────────────────┬─────────────────────────┘
│ WasmEmulator (closed-source
│ wasm, touched only here)
esp_emu_bg.wasm v0.51.0
```
## 2. 关键决策与理由
## 2. Key decisions and why
| 决策 | 理由 |
| decision | rationale |
|---|---|
| 模拟器只存在于 worker | 页面永不触碰 wasm;所有交互走 postMessage,线程模型不可能被破坏 |
| 框架层 TypeScript、设备插件原生 JS | 类型检查覆盖 bug 高发区(本次重构即修掉状态丢弃缺陷);插件保持"丢文件即用",不引入构建依赖 |
| tsc 逐文件编译、不打包 | 产物仍是标准 ESM 目录,部署模型不变(www 只增不改);上游同构,升级 diff 容易 |
| 样式拆 peris.css,上游样式原块留 HTML | 升级时直接 diff HTML 里的上游段 |
| 数据在 TS 常量(chips/mmio)而非 JSON | tsc 原生携带、类型检查、无运行时拷贝问题 |
| registry 运行时校验插件元数据 | 插件作者没有编译期检查,缺 `pins()`/`typeId` 在加载时即报错(真实 bug 类) |
| emulator lives only in the worker | the page never touches the wasm; all interaction goes through postMessage, so the threading model cannot be broken |
| framework in TypeScript, device plugins in plain JS | type-checking covers the bug-prone areas (the big refactor fixed a dropped-state defect this way); plugins stay drop-in with no build dependency |
| tsc per-file compilation, no bundling | output stays a standard ESM directory, deployment model unchanged (www add-only); same shape as upstream for easy upgrade diffs |
| peris styles split into peris.css, stock styles stay inline in the HTML | upstream sections of the HTML can be diffed directly on upgrade |
| data lives in TS constants (chips/mmio), not JSON | carried natively by tsc, type-checked, no runtime copy issues |
| registry validates plugin metadata at load time | plugin authors get no compile-time checks; a missing `pins()`/`typeId` fails immediately at load (a real bug class) |
## 3. 数据流与状态归属
## 3. Data flow and state ownership
- **固件 → 设备**:worker 每批 `run_batch()` 后 `runtime.onBatchEnd()` → `sigbus.sample()`
读 `gpio_state()`(4 个 32 位字),按**联合变化掩码**(out|oe)对每个变化引脚分发一次
`onPinState`。无变化时成本 = 1 次 wasm 调用 + 4 次 XOR。
- **设备 → 页面**:设备 `markDirty()` → `flushStates` 节流 ~30fps;**节流窗口内挂尾随
定时器,保证最后一次 dirty 必达**(否则静止在窗口内的最终状态会被丢弃)。
- **页面 → 设备**:View `sendCmd()` → `peris_command` → `runtime.command()` → `onCommand`。
- **输入引脚电平**:唯一写者是 `ctx.setPinInput`(设备)与 Debug 网格的 `gpio_input`。
页面侧 `gpioInputLevels[]` 只是 Debug 网格自己的镜像,不是权威状态;权威状态在设备里
(`onEmuReady` 时重驱动)。
- **配置**:页面 `PerisPanel.config` 是唯一事实源;每次变更全量 `peris_config` 下发
(worker 销毁重建实例)并写 localStorage。引脚冲突由 worker 校验后回报
`peris_ready.errors`,页面黄条展示。
- **Firmware → device**: after each `run_batch()` the worker calls
`runtime.onBatchEnd()` → `sigbus.sample()` reads `gpio_state()` (four 32-bit
words) and, using the **union change mask** (out|oe), dispatches one
`onPinState` per changed pin. With no changes the cost is 1 wasm call + 4
XORs.
- **Device → page**: `markDirty()` → `flushStates` throttled to ~30 fps; **a
trailing timer is attached inside the throttle window so the last dirty flag
is always delivered** (otherwise a final state resting inside the window
would be dropped).
- **Page → device**: View `sendCmd()` → `peris_command` → `runtime.command()` →
`onCommand`.
- **Input pin levels**: the only writers are `ctx.setPinInput` (devices) and
the Debug grid's `gpio_input`. The page-side `gpioInputLevels[]` is only the
Debug grid's own mirror, not authoritative state; the authority lives in the
devices (re-driven on `onEmuReady`).
- **Config**: the page's `PerisPanel.config` is the single source of truth;
every change is sent in full via `peris_config` (the worker destroys and
rebuilds instances) and written to localStorage. Pin conflicts are validated
by the worker and reported back through `peris_ready.errors`, shown as a
yellow bar on the page.
## 4. 目录职责
## 4. Directory responsibilities
```
src/core/ worker 侧框架(TS 源) → 编译到 build/core/
src/ui/ 页面侧模块(TS 源) → 编译到 build/ui/
build/ tsc 产物(未入 git;HTML/worker/测试都引用这里)
peris/devices/ 设备插件(JS 源,不编译;manifest.json 注册;各自版本号)
peris/vendor/ xterm 本地化
peris/peris.css peris 专属样式
tests/unit/ 单元测试(node:test,零依赖)
tests/m1_smoke.mjs 集成冒烟(真实 wasm+固件+runtime+设备)
tests/m0_probe.mjs 模拟器能力探针(升级上游时先跑它)
tests/firmware/ IDF 测试固件
src/core/ worker-side framework (TS sources) → compiled to build/core/
src/ui/ page-side modules (TS sources) → compiled to build/ui/
build/ tsc output (not in git; HTML/worker/tests reference it)
peris/devices/ device plugins (JS sources, not compiled; manifest.json
registers them; each carries its own version)
peris/vendor/ vendored xterm
peris/peris.css peris-specific styles
tests/unit/ unit tests (node:test, zero dependencies)
tests/m1_smoke.mjs integration smoke (real wasm + firmware + runtime + devices)
tests/m0_probe.mjs emulator capability probe (run first on upstream upgrades)
tests/firmware/ IDF test firmware
```
## 5. 构建与测试
## 5. Build & tests
```bash
npm install # 一次性(typescript 走 npmmirror 源)
npm run build # tsc → peris/core + peris/ui(改 src 后必须)
npm run check # 仅类型检查
npm test # 单测 + 集成冒烟
npm run serve # python serve.py(合并服务仓库与 www/pkg)
npm run sync # 发布进官方 www(先 build)
npm install # one-time (typescript from the npmmirror registry)
npm run build # tsc → peris/core + peris/ui (mandatory after src changes)
npm run check # type-check only
npm test # unit + integration smoke
npm run serve # python serve.py (merges the service repo and www/pkg)
npm run sync # publish into the official www (build first)
```
测试分层:**单元**(纯逻辑:位运算/电平语义/校验/行式徽章推导——快、定位准)→
**集成**(真实 wasm+固件全链路)→ **GUI**(手动清单:页面加载、三栏同屏可见、加设备
后行式渲染、行内改名/改 GPIO/翻 V·G、行内图标交互(点灯换色/按压/开关)、展开详情
与配置编辑、拖动排序、载固件、Run 后 LED 翻转、配置导入导出往返;浏览器自动化待
网络环境允许时引入 Playwright,
`PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright`)。
Test layering: **unit** (pure logic: bit math / level semantics / validation /
row-badge derivation — fast, pinpoints failures) → **integration** (real wasm
+ firmware, full chain) → **GUI** (manual checklist: page load, three panes
visible at once, row rendering after adding a device, inline rename / GPIO
change / V·G flip, row-icon interactions (LED colour / press / switch), expand
details & config editing, drag sorting, firmware load, LED toggling while
running, config import/export round-trip; browser automation to be introduced
with Playwright once the network allows —
`PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright`).
## 6. 升级流程(摘要,详见 UPGRADE.md)
## 6. Upgrade procedure (summary; details in UPGRADE.md)
1. 新 wasm 包就位后先跑 `npm run test:m0`,对比 FEASIBILITY.md 结论;
2. diff 上游 worker.js ↔ `src/core/worker.ts`(改动均标 `peris:`);
3. diff 上游 app.js/index.html ↔ `src/ui/*.ts` 与 HTML 的上游样式段;
4. `npm run build && npm test`,GUI 冒测。
1. After a new wasm package lands, run `npm run test:m0` first and compare
against FEASIBILITY.md;
2. diff the upstream worker.js ↔ `src/core/worker.ts` (all changes carry
`peris:` markers);
3. diff upstream app.js/index.html ↔ `src/ui/*.ts` and the stock style section
of the HTML;
4. `npm run build && npm test`, manual GUI pass.
## 7. 扩展点
## 7. Extension points
- **新外设**:`peris/devices/` 放模块 + manifest 一行(见 docs/DEVICE-API.md);Worker/页面
自动加载,元数据即时校验。
- **新芯片**:`src/core/mmio/<chip>.ts` 加地址表 + 注册到 `mmio.ts`;`chips.ts` 补引脚数。
- **新页面面板**:ui/ 加模块,在 app.ts 的消息路由注册——worker 协议不动。
- **New peripheral**: drop a module into `peris/devices/` + one manifest line
(see docs/DEVICE-API.md); worker and page load it automatically with
immediate metadata validation.
- **New chip**: add the address table in `src/core/mmio/<chip>.ts` and register
it in `mmio.ts`; add the pin count in `chips.ts`.
- **New page panel**: add a module in ui/ and register it in app.ts's message
routing — the worker protocol stays untouched.
+107
View File
@@ -0,0 +1,107 @@
[English](ARCHITECTURE.md) | **简体中文**
# esp-emu-peris 系统架构
## 1. 分层总览
```
┌───────────────────────────────── 页面(page)─────────────────────────────────┐
│ esp-emu-peris.html 上游样式原块(便于升级 diff)+ <link peris/peris.css> │
│ 三栏同屏:UART0 控制台 │ 外设面板 │ Debug(0.3.0 起不再标签切换) │
│ build/ui/* (TypeScript 编译产物) │
│ app.ts 编排器:机器状态、Run/Pause/固件/网络控件、消息路由 │
│ worker-client Worker 唯一入口(其它模块一律经由它收发消息) │
│ terminal / chart / debug-panels UART 控制台 / 性能图 / Debug 只读面板 │
│ peris-panel + config-editor 行式外设列表(图标/名称/GPIO/V·G 徽章 │
│ + 展开/删除/拖动)与详情配置编辑器 │
└───────────────▲───────────────────────────────────────────────▲────────────┘
postMessage(协议见 core/protocol.ts) │ DOM
┌───────────────┴───────────────────────────────────────────────▼────────────┐
│ 页面侧设备 View(peris/devices/*.js 的 View 类,由 peris-panel 实例化) │
├─────────────────────────────────────────────────────────────────────────────┤
│ ← peris_state / peris_command → │
├─────────────────────────────────────────────────────────────────────────────┤
│ build/core/worker.ts 上游 worker 复刻 + 6 处 peris: 钩子(唯一可碰上游逻辑) │
│ build/core/runtime.ts 设备宿主:配置校验/实例化/命令/状态节流推送 │
│ build/core/sigbus.ts 引脚信号总线:每批 gpio_state() 差分分发 │
│ build/core/registry.ts 插件注册表(manifest 驱动 + 元数据校验) │
│ build/core/{version,types,protocol,chips,mmio}.ts 版本/契约/消息/芯片表/寄存器表 │
│ worker 侧设备 Device(peris/devices/*.js 的 Device 类) │
└───────────────────────────────────────────────────┬─────────────────────────┘
│ WasmEmulator(闭源 wasm,
│ 只此一处触碰)
esp_emu_bg.wasm v0.51.0
```
## 2. 关键决策与理由
| 决策 | 理由 |
|---|---|
| 模拟器只存在于 worker | 页面永不触碰 wasm;所有交互走 postMessage,线程模型不可能被破坏 |
| 框架层 TypeScript、设备插件原生 JS | 类型检查覆盖 bug 高发区(本次重构即修掉状态丢弃缺陷);插件保持"丢文件即用",不引入构建依赖 |
| tsc 逐文件编译、不打包 | 产物仍是标准 ESM 目录,部署模型不变(www 只增不改);上游同构,升级 diff 容易 |
| 样式拆 peris.css,上游样式原块留 HTML | 升级时直接 diff HTML 里的上游段 |
| 数据在 TS 常量(chips/mmio)而非 JSON | tsc 原生携带、类型检查、无运行时拷贝问题 |
| registry 运行时校验插件元数据 | 插件作者没有编译期检查,缺 `pins()`/`typeId` 在加载时即报错(真实 bug 类) |
## 3. 数据流与状态归属
- **固件 → 设备**:worker 每批 `run_batch()` 后 `runtime.onBatchEnd()` → `sigbus.sample()`
读 `gpio_state()`(4 个 32 位字),按**联合变化掩码**(out|oe)对每个变化引脚分发一次
`onPinState`。无变化时成本 = 1 次 wasm 调用 + 4 次 XOR。
- **设备 → 页面**:设备 `markDirty()` → `flushStates` 节流 ~30fps;**节流窗口内挂尾随
定时器,保证最后一次 dirty 必达**(否则静止在窗口内的最终状态会被丢弃)。
- **页面 → 设备**:View `sendCmd()` → `peris_command` → `runtime.command()` → `onCommand`。
- **输入引脚电平**:唯一写者是 `ctx.setPinInput`(设备)与 Debug 网格的 `gpio_input`。
页面侧 `gpioInputLevels[]` 只是 Debug 网格自己的镜像,不是权威状态;权威状态在设备里
(`onEmuReady` 时重驱动)。
- **配置**:页面 `PerisPanel.config` 是唯一事实源;每次变更全量 `peris_config` 下发
(worker 销毁重建实例)并写 localStorage。引脚冲突由 worker 校验后回报
`peris_ready.errors`,页面黄条展示。
## 4. 目录职责
```
src/core/ worker 侧框架(TS 源) → 编译到 build/core/
src/ui/ 页面侧模块(TS 源) → 编译到 build/ui/
build/ tsc 产物(未入 git;HTML/worker/测试都引用这里)
peris/devices/ 设备插件(JS 源,不编译;manifest.json 注册;各自版本号)
peris/vendor/ xterm 本地化
peris/peris.css peris 专属样式
tests/unit/ 单元测试(node:test,零依赖)
tests/m1_smoke.mjs 集成冒烟(真实 wasm+固件+runtime+设备)
tests/m0_probe.mjs 模拟器能力探针(升级上游时先跑它)
tests/firmware/ IDF 测试固件
```
## 5. 构建与测试
```bash
npm install # 一次性(typescript 走 npmmirror 源)
npm run build # tsc → peris/core + peris/ui(改 src 后必须)
npm run check # 仅类型检查
npm test # 单测 + 集成冒烟
npm run serve # python serve.py(合并服务仓库与 www/pkg)
npm run sync # 发布进官方 www(先 build)
```
测试分层:**单元**(纯逻辑:位运算/电平语义/校验/行式徽章推导——快、定位准)→ **集成**(真实
wasm+固件全链路)→ **GUI**(手动清单:页面加载、三栏同屏可见、加设备
后行式渲染、行内改名/改 GPIO/翻 V·G、行内图标交互(点灯换色/按压/开关)、展开详情
与配置编辑、拖动排序、载固件、Run 后 LED 翻转、配置导入导出往返;浏览器自动化待
网络环境允许时引入 Playwright,
`PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright`)。
## 6. 升级流程(摘要,详见 UPGRADE.zh-cn.md)
1. 新 wasm 包就位后先跑 `npm run test:m0`,对比 FEASIBILITY.zh-cn.md 结论;
2. diff 上游 worker.js ↔ `src/core/worker.ts`(改动均标 `peris:`);
3. diff 上游 app.js/index.html ↔ `src/ui/*.ts` 与 HTML 的上游样式段;
4. `npm run build && npm test`,GUI 冒测。
## 7. 扩展点
- **新外设**:`peris/devices/` 放模块 + manifest 一行(见 docs/DEVICE-API.zh-cn.md);Worker/页面
自动加载,元数据即时校验。
- **新芯片**:`src/core/mmio/<chip>.ts` 加地址表 + 注册到 `mmio.ts`;`chips.ts` 补引脚数。
- **新页面面板**:ui/ 加模块,在 app.ts 的消息路由注册——worker 协议不动。
+114 -71
View File
@@ -1,94 +1,137 @@
# 变更日志
**English** | [简体中文](CHANGELOG.zh-cn.md)
版本三处同步:`src/core/version.ts`、`package.json`、本文件。
规则:主版本=协议/架构破坏性变更;次版本=新能力;修订=修复与小改。
# Changelog
The project version is synced in three places: `src/core/version.ts`,
`package.json`, and this file (kept in both languages — CHANGELOG.md /
CHANGELOG.zh-cn.md).
Rules: major = breaking protocol/architecture change; minor = new capability;
patch = fixes and small changes.
## peris 0.5.1
- **Bilingual documentation**: every document now has an English version under
the default filename and a Chinese version as `*.zh-cn.md` (README, AGENT,
docs/*); the two cross-link in their first line for one-click switching, and
in-document links stay within the same language. AGENT.md §7 records the
keep-both-languages-in-sync rule.
## peris 0.5.0
- **恢复缓存时自动选中芯片**:localStorage 保存的配置附带 `chip`,页面加载时
自动同步 Target 芯片下拉与机器信息(GPIO 网格、寄存器表、引脚可选项随之
正确),不用再手选一次。未知芯片名安全忽略。
- **面板头部合并**:项目名(导出文件名)输入框移到 `PERIPHERALS` 标题后,
保存/打开改为图标按钮紧跟其后(节约一行版面);工具栏只留 添加下拉+图标
Add 按钮。
- 实现要点:面板构造提前到 GPIO 默认初始化之前,恢复的芯片才能喂给初始网格;
`PerisPanelDeps` 新增 `selectChip(chip)`。
- **Chip auto-selected when the cached wiring loads**: the localStorage config
now carries `chip`; on page load the Target chip select and machine info are
synced automatically (GPIO grid, register table and pin options follow), so
the user does not have to pick the chip again. Unknown chip names are
safely ignored.
- **Panel head merged**: the project-name (export file name) input moved next
to the `PERIPHERALS` title, followed by save/open icon buttons (saves one
toolbar row); the toolbar now holds only the add select + an icon-only Add
button.
- Implementation notes: the PerisPanel is constructed before the GPIO
defaults are initialised so the restored chip feeds the initial grid;
`PerisPanelDeps` gained `selectChip(chip)`.
## peris 0.4.0
- **导出文件名可设置**:外设工具栏新增文件名输入框,导出按它命名(自动去非法
字符、补单个 .json 后缀);留空回退自动名 `peris-{chip}-{date}.json`。输入
记忆于 localStorage(`peris.exportName.v1`)。命名逻辑为纯函数
`exportFileName()`(单测覆盖)。
- **导出附带固件信息**:导出的 JSON 新增 `firmware {name, size, path?}` 备忘
字段(芯片 `chip` 原已有)。受浏览器安全模型限制,磁盘全路径一般拿不到:
`path` 仅在宿主环境暴露非标准 `File.path`/`filePath` 属性时填充,常规浏览器
只有文件名与大小。导入时忽略该字段,不自动加载固件。
- 工具栏改为两行(添加行 + 导入导出行)。
- **Configurable export file name**: a file-name input in the peripherals
toolbar; exports use it (illegal characters stripped, a single .json suffix
appended); empty falls back to the auto name `peris-{chip}-{date}.json`.
The input is remembered in localStorage (`peris.exportName.v1`). Naming
lives in the pure function `exportFileName()` (unit-tested).
- **Firmware provenance in exports**: exported JSON gains an informational
`firmware {name, size, path?}` field (the `chip` field already existed).
Limited by the browser security model, the full disk path is usually
unavailable: `path` is filled only when the host exposes the non-standard
`File.path`/`filePath` property; regular browsers get name and size only.
Import ignores the field and never auto-loads firmware.
- Toolbar split into two rows (add row + import/export row).
## peris 0.3.1
- **修复**:LED 行内图标颜色不随配置(与详情灯泡不一致)。根因有二:面板先
`setConfig` 后 `rowIcon`(图标元素尚不存在,颜色没应用到它),插件里两处各自
设色。现 led 插件统一 `setColour()` 同时应用到灯泡与行图标,面板固定
**先 rowIcon 后 setConfig**。
- **修复**:加载固件换芯片后,展开详情的 GPIO 选项仍停留在旧芯片(行内 chip
一直正确)。引脚选项构建抽取为 `fillPinOptions()` 单一实现(详情编辑器与
行内 chip 共用);`PerisPanel.refreshForChip()` 在固件加载/复位重载后检测
芯片变化并重建详情编辑器。
- **AGENT.md 新增铁律 §3.8**:"同语义代码必须复用"——以上两个 bug 皆为复制
代码只改一处所致;同时登记两条 bug 记录(§4)。
- `npm test`/`npm run test:unit` 改为显式传 .test.mjs 文件列表(AGENT.md §1
记录的 Windows/Git Bash 目录解析坑,目录形式在本机直接 MODULE_NOT_FOUND)。
- **Fix**: the LED row-icon colour did not follow the config (disagreed with
the details bulb). Two root causes: the panel called `setConfig` before
`rowIcon` (the icon element did not exist yet when the colour was applied),
and the plugin styled the two visuals separately. The led plugin now has a
single `setColour()` applied to both bulb and row icon, and the panel
fixed the order: **`rowIcon` first, then `setConfig`**.
- **Fix**: after loading firmware for another chip, the details editor's GPIO
options stayed on the old chip (the row chip was always correct). Pin-option
building was extracted into `fillPinOptions()` as the single implementation
(shared by the details editor and the row chip);
`PerisPanel.refreshForChip()` detects chip changes after firmware
load/reset-reload and rebuilds the details editors.
- **AGENT.md gained iron rule §3.8**: "same-semantic code must be reused" —
both bugs above came from copying code and changing one copy; two bug
records added (§4).
- `npm test`/`npm run test:unit` now pass the explicit .test.mjs file list
(the Windows/Git Bash directory-resolution pitfall recorded in AGENT.md §1;
the directory form fails outright with MODULE_NOT_FOUND on this machine).
## peris 0.3.0
- **三栏同屏布局**:外设面板独立成栏,置于 UART 控制台与 Debug 之间,与 Debug
同宽(360px,窄屏自适应收窄/堆叠);移除 Peripherals/Debug 标签切换,三栏同时可见。
外设面板改为内部滚动(工具栏常驻),Debug 面板加标题条。
- **行式外设列表**(类 Photoshop 图层):每外设一行——左侧依次为交互图标、名称
(点击原地改名)、GPIO 号方框(点击弹出选单)、V/G 闲置电平徽章(点击翻转,
V=上拉到 VCC、G=下拉到 GND);右侧靠齐展开按钮、删除按钮、拖动排序柄。
展开后的详情区(类 Blender 属性分组)显示设备控件主体 + 全量配置编辑器
(一条属性一行);行内放不下的高级功能都收进详情。
- **View API 扩展**(可选项,向后兼容):`View.rowIcon(el)` 定制行内交互图标;
View 构造函数新增第三参 `host.updateConfig(patch)`,供 View 请求改配置
(如 LED 行内图标点击换色);configSchema 新增 `chip:'pull'` /
`pullWhenChecked` 字段声明 V/G 徽章。类型契约见 `build/core/types.d.ts`。
- **拖动排序**:拖柄按下才允许拖拽(避免误触);松手后按 DOM 序回写配置并持久化
(设备集合不变,不重建 worker 实例)。文件拖放导入与行排序互不干扰(按
dataTransfer 类型区分)。
- 新增单测:内置插件 rowIcon 存在性、`rowChips()` 引脚/V·G 徽章推导
(含 GPIO0、`pullWhenChecked:'low'` 反例)。
- **Three-pane layout**: the peripherals pane became its own column between
the UART console and Debug, the same width as Debug (360px, narrowing/
stacking on small screens); the Peripherals/Debug tab switch was removed —
all three panes are visible at once. The peripherals pane scrolls
internally (toolbar stays visible); the Debug pane gained a title bar.
- **Row-based peripherals list** (photoshop-layers style): each device is one
row — on the left the interactive icon, the name (click to rename in
place), the GPIO-number box (click for a picker) and the V/G idle-pull
badge (click to flip; V = pull-up to VCC, G = pull-down to GND); on the
right the expand button, remove button and drag-sort grip. The expanded
details (blender-style property groups) show the device widget body + the
full config editor (one property per line); advanced features that do not
fit the row live there.
- **View API extension** (optional, backwards compatible): `View.rowIcon(el)`
customises the row icon; the View constructor gained a third argument
`host.updateConfig(patch)` so Views can request config changes (e.g. the
LED row icon's colour picker); configSchema gained `chip:'pull'` /
`pullWhenChecked` to declare the V/G badge. Type contracts in
`build/core/types.d.ts`.
- **Drag sorting**: dragging is armed only while the grip is held (no
accidental drags); on release the DOM order is written back to the config
and persisted (device set unchanged → no worker rebuild). File-drop import
and row sorting do not interfere (distinguished by dataTransfer type).
- New unit tests: rowIcon presence on built-in plugins, `rowChips()` pin/V·G
badge derivation (including GPIO0 and a `pullWhenChecked:'low'` case).
## peris 0.2.0
- **架构重构**:框架层 TypeScript 化(`src/core` + `src/ui`,tsc 逐文件编译到
`build/`,不打包);千行 app.js 拆为 7 个 UI 模块;peris 样式独立 `peris/peris.css`;
chips/MMIO/协议常量单一来源;构建产物集中 `build/`(gitignore)。
- **测试金字塔**:单元测试(node:test,14 用例)+ 集成冒烟 + M0 探针接入 npm scripts。
- **修复**:状态推送节流窗口静默丢弃最后一次变化的缺陷(尾随 flush)。
- **版本管理**:项目版本(`version.ts`/`package.json`/本文件)与设备版本
(`Device.version`,卡片头展示,registry 校验)。
- **文档**:ARCHITECTURE.md(分层/数据流/状态归属)、EMU-API.md(闭源 wasm 能力
摘要)、AGENT.md(环境坑与约定)。
- xterm 本地化(无 CDN 依赖)。
- **Architecture refactor**: framework layer moved to TypeScript
(`src/core` + `src/ui`, tsc per-file compilation to `build/`, no bundling);
the 1000-line app.js split into 7 UI modules; peris styles extracted into
`peris/peris.css`; chips/MMIO/protocol constants single-sourced; build
output centralised in `build/` (gitignored).
- **Test pyramid**: unit tests (node:test, 14 cases) + integration smoke +
M0 probe wired into npm scripts.
- **Fix**: silently dropped last state inside the throttle window (trailing
flush).
- **Versioning**: project version (`version.ts`/`package.json`/CHANGELOG)
and device versions (`Device.version`, shown in the row header, validated
by the registry).
- **Docs**: ARCHITECTURE.md (layering/data flow/state ownership), EMU-API.md
(closed-source wasm capability summary), AGENT.md (environment pitfalls and
conventions).
- xterm vendored locally (no CDN dependency).
## peris 0.1.0
- M0 可行性验证(GPIO 轮询/输入中断 ✅;引脚级 PWM/RMT 不可见;LEDC 寄存器与
RMT RAM 可读)→ docs/FEASIBILITY.md。
- M1 外设框架:入口页(Peripherals/Debug 双标签)、自有 worker(上游复刻+
采样钩子)、设备插件 API v1(manifest 注册)、LED/按键/触摸按键/拨动开关、
配置导入导出 + localStorage、serve.py/sync_www.py、IDF 测试固件与探针。
- M0 feasibility verification (GPIO polling/input interrupts ✅; pin-level
PWM/RMT invisible; LEDC registers and RMT RAM readable) → docs/FEASIBILITY.
- M1 peripherals framework: entry page (Peripherals/Debug tabs at the time),
own worker (upstream replica + sampling hooks), device plugin API v1
(manifest registration), LED/button/touch button/toggle switch, config
import/export + localStorage, serve.py/sync_www.py, IDF test firmware and
probes.
## 设备插件版本
## Device plugin versions
| typeId | 版本 | 变更 |
| typeId | version | change |
|---|---|---|
| led | 1.1.1 | 修复:行图标颜色与配置不一致——统一 setColour 应用到灯泡+行图标 |
| button | 1.1.0 | 行内按压图标(press/release);activeLow 声明为 V/G 徽章 |
| touch-button | 1.1.0 | 行内触摸图标(继承 button 的徽章声明) |
| toggle-switch | 1.1.0 | 行内迷你拨动开关(点击 toggle);activeLow 声明为 V/G 徽章 |
| led | 1.1.1 | fix: row-icon colour disagreed with config — one `setColour` for bulb + row icon |
| button | 1.1.0 | row press icon (press/release); activeLow declared as the V/G badge |
| touch-button | 1.1.0 | row touch icon (inherits button's badge declaration) |
| toggle-switch | 1.1.0 | mini row switch (click to toggle); activeLow declared as the V/G badge |
历史:led/button/touch-button/toggle-switch 1.0.0 初版(见 peris 0.1.0/0.2.0 条目)。
History: led/button/touch-button/toggle-switch 1.0.0 initial versions (see the
peris 0.1.0/0.2.0 entries).
+103
View File
@@ -0,0 +1,103 @@
[English](CHANGELOG.md) | **简体中文**
# 变更日志
版本三处同步:`src/core/version.ts`、`package.json`、本文件(双语维护——CHANGELOG.zh-cn.md
与英文版 CHANGELOG.md)。
规则:主版本=协议/架构破坏性变更;次版本=新能力;修订=修复与小改。
## peris 0.5.1
- **文档双语化**:所有文档提供英文版(默认文件名)与中文版(`*.zh-cn.md`)
(README、AGENT、docs/*);两版顶部互链一键切换,文内链接保持同语言跳转。
AGENT.zh-cn.md §7 记录"两种语言需同步修改"的约定。
## peris 0.5.0
- **恢复缓存时自动选中芯片**:localStorage 保存的配置附带 `chip`,页面加载时
自动同步 Target 芯片下拉与机器信息(GPIO 网格、寄存器表、引脚可选项随之
正确),不用再手选一次。未知芯片名安全忽略。
- **面板头部合并**:项目名(导出文件名)输入框移到 `PERIPHERALS` 标题后,
保存/打开改为图标按钮紧跟其后(节约一行版面);工具栏只留 添加下拉+图标
Add 按钮。
- 实现要点:面板构造提前到 GPIO 默认初始化之前,恢复的芯片才能喂给初始网格;
`PerisPanelDeps` 新增 `selectChip(chip)`。
## peris 0.4.0
- **导出文件名可设置**:外设工具栏新增文件名输入框,导出按它命名(自动去非法
字符、补单个 .json 后缀);留空回退自动名 `peris-{chip}-{date}.json`。输入
记忆于 localStorage(`peris.exportName.v1`)。命名逻辑为纯函数
`exportFileName()`(单测覆盖)。
- **导出附带固件信息**:导出的 JSON 新增 `firmware {name, size, path?}` 备忘
字段(芯片 `chip` 原已有)。受浏览器安全模型限制,磁盘全路径一般拿不到:
`path` 仅在宿主环境暴露非标准 `File.path`/`filePath` 属性时填充,常规浏览器
只有文件名与大小。导入时忽略该字段,不自动加载固件。
- 工具栏改为两行(添加行 + 导入导出行)。
## peris 0.3.1
- **修复**:LED 行内图标颜色不随配置(与详情灯泡不一致)。根因有二:面板先
`setConfig` 后 `rowIcon`(图标元素尚不存在,颜色没应用到它),插件里两处各自
设色。现 led 插件统一 `setColour()` 同时应用到灯泡与行图标,面板固定
**先 rowIcon 后 setConfig**。
- **修复**:加载固件换芯片后,展开详情的 GPIO 选项仍停留在旧芯片(行内 chip
一直正确)。引脚选项构建抽取为 `fillPinOptions()` 单一实现(详情编辑器与
行内 chip 共用);`PerisPanel.refreshForChip()` 在固件加载/复位重载后检测
芯片变化并重建详情编辑器。
- **AGENT.md 新增铁律 §3.8**:"同语义代码必须复用"——以上两个 bug 皆为复制
代码只改一处所致;同时登记两条 bug 记录(§4)。
- `npm test`/`npm run test:unit` 改为显式传 .test.mjs 文件列表(AGENT.md §1
记录的 Windows/Git Bash 目录解析坑,目录形式在本机直接 MODULE_NOT_FOUND)。
## peris 0.3.0
- **三栏同屏布局**:外设面板独立成栏,置于 UART 控制台与 Debug 之间,与 Debug
同宽(360px,窄屏自适应收窄/堆叠);移除 Peripherals/Debug 标签切换,三栏同时可见。
外设面板改为内部滚动(工具栏常驻),Debug 面板加标题条。
- **行式外设列表**(类 Photoshop 图层):每外设一行——左侧依次为交互图标、名称
(点击原地改名)、GPIO 号方框(点击弹出选单)、V/G 闲置电平徽章(点击翻转,
V=上拉到 VCC、G=下拉到 GND);右侧靠齐展开按钮、删除按钮、拖动排序柄。
展开后的详情区(类 Blender 属性分组)显示设备控件主体 + 全量配置编辑器
(一条属性一行);行内放不下的高级功能都收进详情。
- **View API 扩展**(可选项,向后兼容):`View.rowIcon(el)` 定制行内交互图标;
View 构造函数新增第三参 `host.updateConfig(patch)`,供 View 请求改配置
(如 LED 行内图标点击换色);configSchema 新增 `chip:'pull'` /
`pullWhenChecked` 字段声明 V/G 徽章。类型契约见 `build/core/types.d.ts`。
- **拖动排序**:拖柄按下才允许拖拽(避免误触);松手后按 DOM 序回写配置并持久化
(设备集合不变,不重建 worker 实例)。文件拖放导入与行排序互不干扰(按
dataTransfer 类型区分)。
- 新增单测:内置插件 rowIcon 存在性、`rowChips()` 引脚/V·G 徽章推导
(含 GPIO0、`pullWhenChecked:'low'` 反例)。
## peris 0.2.0
- **架构重构**:框架层 TypeScript 化(`src/core` + `src/ui`,tsc 逐文件编译到
`build/`,不打包);千行 app.js 拆为 7 个 UI 模块;peris 样式独立 `peris/peris.css`;
chips/MMIO/协议常量单一来源;构建产物集中 `build/`(gitignore)。
- **测试金字塔**:单元测试(node:test,14 用例)+ 集成冒烟 + M0 探针接入 npm scripts。
- **修复**:状态推送节流窗口静默丢弃最后一次变化的缺陷(尾随 flush)。
- **版本管理**:项目版本(`version.ts`/`package.json`/本文件)与设备版本
(`Device.version`,卡片头展示,registry 校验)。
- **文档**:ARCHITECTURE(分层/数据流/状态归属)、EMU-API(闭源 wasm 能力
摘要)、AGENT(环境坑与约定)。
- xterm 本地化(无 CDN 依赖)。
## peris 0.1.0
- M0 可行性验证(GPIO 轮询/输入中断 ✅;引脚级 PWM/RMT 不可见;LEDC 寄存器与
RMT RAM 可读)→ docs/FEASIBILITY。
- M1 外设框架:入口页(当时为 Peripherals/Debug 双标签)、自有 worker(上游复刻+
采样钩子)、设备插件 API v1(manifest 注册)、LED/按键/触摸按键/拨动开关、
配置导入导出 + localStorage、serve.py/sync_www.py、IDF 测试固件与探针。
## 设备插件版本
| typeId | 版本 | 变更 |
|---|---|---|
| led | 1.1.1 | 修复:行图标颜色与配置不一致——统一 setColour 应用到灯泡+行图标 |
| button | 1.1.0 | 行内按压图标(press/release);activeLow 声明为 V/G 徽章 |
| touch-button | 1.1.0 | 行内触摸图标(继承 button 的徽章声明) |
| toggle-switch | 1.1.0 | 行内迷你拨动开关(点击 toggle);activeLow 声明为 V/G 徽章 |
历史:led/button/touch-button/toggle-switch 1.0.0 初版(见 peris 0.1.0/0.2.0 条目)。
+120 -86
View File
@@ -1,93 +1,111 @@
# esp-emu-peris 外设接口规范(v1)
**English** | [简体中文](DEVICE-API.zh-cn.md)
一个外设 = `peris/devices/` 下的一个 ES 模块 + `manifest.json` 里的一行。除此之外不需要改动项目任何其它文件。
# esp-emu-peris peripheral interface spec (v1)
> **语言**:插件用**原生 JavaScript**(丢文件即用,无需构建)。框架是 TypeScript 编译
> 产物,完整契约类型在 `peris/core/types.d.ts`(`DeviceMeta`/`DeviceLike`/`DeviceCtx`/
> `PinState` 等),插件里可通过 JSDoc 引用获得编辑器提示。注册表加载时会**运行时校验**
> 元数据(缺 `typeId`/`title`/`pins()`/`defaultConfig` 立即报错),单元测试
> `tests/unit/devices.test.mjs` 是插件电气语义的范例。
A peripheral = one ES module under `peris/devices/` + one line in
`manifest.json`. Nothing else in the project needs to change.
## 模块导出
> **Language**: plugins are **plain JavaScript** (drop-in, no build). The
> framework is compiled TypeScript; the full contract types live in
> `peris/core/types.d.ts` (`DeviceMeta`/`DeviceLike`/`DeviceCtx`/`PinState`
> etc.) and can be referenced from JSDoc for editor hints. The registry
> **validates metadata at load time** (a missing `typeId`/`title`/`pins()`/
> `defaultConfig` errors immediately); the unit tests in
> `tests/unit/devices.test.mjs` are the reference for electrical semantics.
## Module exports
```js
export class Device { // 运行在 worker,与模拟器交互
static typeId = 'led'; // 唯一、稳定、kebab-case
static version = '1.0.0'; // 插件自身版本(卡片头展示,登记于 docs/CHANGELOG.md)
static title = 'LED'; // UI 显示名
static category = 'basic'; // Add 菜单分组
static description = '...'; // 一行说明(tooltip)
static defaultConfig = {...}; // 新建实例的默认配置
static configSchema = [ // 通用配置编辑器字段(展开详情内,一条一行)
export class Device { // runs in the worker, talks to the emulator
static typeId = 'led'; // unique, stable, kebab-case
static version = '1.0.0'; // plugin's own version (row header; logged in docs/CHANGELOG.md)
static title = 'LED'; // UI display name
static category = 'basic'; // Add-menu grouping
static description = '...'; // one-line explanation (tooltip)
static defaultConfig = {...}; // defaults for new instances
static configSchema = [ // generic config editor fields (in the
// expanded details, one per line)
{ key: 'pin', label: 'GPIO pin', type: 'pin' },
{ key: 'color', label: 'Colour', type: 'color' },
{ key: 'name', label: 'Label', type: 'text' },
// type 还支持: number(min/max/step)、select(options:[{value,label}])、checkbox
// checkbox 可加 chip:'pull' + pullWhenChecked:'high'|'low'
// → 设备行上显示 V/G 闲置电平徽章(点击翻转),详见下文"行式 UI"
// also supported: number(min/max/step), select(options:[{value,label}]), checkbox
// a checkbox may add chip:'pull' + pullWhenChecked:'high'|'low'
// → the device row shows a V/G idle-pull badge (click to flip),
// see "Row UI conventions" below
];
static pins(cfg) { return [cfg.pin]; } // 声明占用的 GPIO(用于冲突检测)
static pins(cfg) { return [cfg.pin]; } // claimed GPIOs (conflict detection)
constructor(ctx, cfg) {} // cfg = { id, type, ...设备配置 }
onPinState(pin, s) {} // 引脚输出变化回调 s={out, oe, cycles}
onCommand(cmd, params) {} // 页面 View 发来的用户操作
onEmuReady() {} // 模拟器(重)加载后:重新驱动输入电平
getState() { return {...}; } // 可序列化状态,推给页面 View(~30fps)
destroy() {} // 释放/恢复引脚
constructor(ctx, cfg) {} // cfg = { id, type, ...device settings }
onPinState(pin, s) {} // pin-output change callback s={out, oe, cycles}
onCommand(cmd, params) {} // user action sent from the page View
onEmuReady() {} // emulator (re)loaded: re-drive input levels
getState() { return {...}; } // serialisable state pushed to the View (~30fps)
destroy() {} // release / restore pins
}
export class View { // 运行在页面,渲染设备视图
constructor(el, sendCmd, host) {} // el=详情区主体容器; sendCmd(cmd, params);
// host.updateConfig(patch) 请求改本设备配置
rowIcon(el) {} // 可选:行内交互图标(el≈24px 容器),见下文
render(state) {} // state 来自 Device.getState();行图标与详情主体一起驱动
setConfig(cfg) {} // 可选:配置编辑后回调(如换 LED 颜色)
export class View { // runs on the page, renders the device view
constructor(el, sendCmd, host) {} // el = details body container;
// sendCmd(cmd, params);
// host.updateConfig(patch) requests a config change
rowIcon(el) {} // optional: compact interactive row icon (~24px), see below
render(state) {} // state comes from Device.getState(); drives icon and body together
setConfig(cfg) {} // optional: called after config edits (e.g. LED colour)
}
```
## 行式 UI 约定(0.3.0+)
## Row UI conventions (0.3.0+)
外设面板是三栏布局中的独立一栏(控制台 | 外设 | Debug),每种外设占一行:
The peripherals pane is its own column in the three-pane layout
(console | peripherals | debug); each device occupies one row:
```
[交互图标] [名称] [GPIO号] [V/G] [▸展开] [×删除] [⠿拖动]
[icon] [name] [GPIO#] [V/G] [▸ expand] [× remove] [⠿ drag]
```
- **交互图标**(`View.rowIcon(el)`):可点击的迷你控件,操作语义由插件定义——
LED 点击换色(取色器,经 `host.updateConfig({color})` 写回配置)、按键按下/
松开(连发 press/release)、拨动开关点击 toggle。`render(state)` 里同步刷新
图标状态(亮灭/按下/ON)。不实现 rowIcon 时显示首字母占位块。
**调用顺序**:面板固定先 `rowIcon` 后 `setConfig`,配置式样可覆盖到图标;
图标与详情主体是同一状态的两个视觉,插件必须共用同一 setter/render
(参考 led.js 的 `setColour()`),禁止为图标单写一份样式逻辑。
- **名称 / GPIO 方框 / V·G 徽章**:由面板通用渲染,点击原地修改
(名称与 GPIO 走内联编辑,V/G 徽章直接翻转)。GPIO 方框取 schema 第一个
`type:'pin'` 字段;V/G 徽章取标记 `chip:'pull'` 的 checkbox——勾选时若
`pullWhenChecked:'high'`(默认)显示 V(上拉到 VCC),否则 G;未勾选相反。
- **展开详情**:设备控件主体(`el`,即构造函数第一个参数)+ 全量配置编辑器
(一条属性一行)。行内放不下的高级功能一律放详情。
- **拖动排序** ⠿:按下拖柄才启用拖拽,松手回写 devices 顺序(不重建 worker)。
- **Interactive icon** (`View.rowIcon(el)`): a clickable mini widget whose
semantics the plugin defines — LED opens a colour picker on click (writes
back via `host.updateConfig({color})`), buttons send press/release while
held, the toggle switch flips on click. `render(state)` refreshes the icon
state (lit / pressed / ON) as well. Without rowIcon a placeholder letter
chip is shown.
**Call order**: the panel always calls `rowIcon` before `setConfig`, so
config-driven styling covers the icon; the icon and the details body are two
visuals of the same state — plugins must share one setter/render
(see led.js `setColour()`), never a separate styling path for the icon.
- **Name / GPIO box / V·G badge**: rendered generically by the panel, edited
in place on click (name and GPIO via inline editors, the V/G badge flips
directly). The GPIO box maps to the schema's first `type:'pin'` field; the
V/G badge maps to the checkbox marked `chip:'pull'` — when checked it shows
V (pull-up to VCC) for `pullWhenChecked:'high'` (the default), otherwise G;
unchecked is the opposite.
- **Expanded details**: the device widget body (`el`, the constructor's first
argument) + the full config editor (one property per line). Advanced
features that do not fit in the row live here.
- **Drag sorting** ⠿: dragging is armed only while the grip is held; on
release the devices order is written back (no worker rebuild).
## ctx(worker 侧设备上下文)
## ctx (worker-side device context)
| 成员 | 说明 |
| member | description |
|---|---|
| `ctx.id` / `ctx.chip` / `ctx.gpioCount` | 设备 id、芯片型号、GPIO 数 |
| `ctx.setPinInput(pin, high)` | 驱动引脚输入电平(固件可读到、可触发 GPIO 中断) |
| `ctx.subscribe(pins)` / `ctx.unsubscribe()` | 订阅引脚输出变化(onPinState) |
| `ctx.readMem(addr, len)` / `ctx.readU32(addr)` | 总线/MMIO 读取(LEDC duty、RMT RAM 等) |
| `ctx.cycles()` | 模拟器周期计数 |
| `ctx.markDirty()` | 请求尽快把 getState() 推给页面 |
| `ctx.log(text, level?)` | 在 UART 控制台打一条 `[Peris]` 日志 |
| `ctx.id` / `ctx.chip` / `ctx.gpioCount` | device id, chip model, GPIO count |
| `ctx.setPinInput(pin, high)` | drive a pin's input level (firmware reads it; raises GPIO interrupts) |
| `ctx.subscribe(pins)` / `ctx.unsubscribe()` | subscribe to pin-output changes (onPinState) |
| `ctx.readMem(addr, len)` / `ctx.readU32(addr)` | bus/MMIO reads (LEDC duty, RMT RAM, …) |
| `ctx.cycles()` | emulator cycle counter |
| `ctx.markDirty()` | request getState() to be pushed to the page soon |
| `ctx.log(text, level?)` | print a `[Peris]` line on the UART console |
## 消息协议(页面 ↔ worker)
## Message protocol (page ↔ worker)
在原版 worker 协议之外增加(见 `peris/protocol.js`):
Added on top of the stock worker protocol (see `peris/protocol.js`):
- 页面→worker:`peris_config {config}`(全量应用配置)、`peris_command {id, cmd, params}`
- worker→页面:`peris_ready {ok, errors, devices}`、`peris_state {id, state}`、`peris_log {level, text}`
- page→worker: `peris_config {config}` (apply a config in full),
`peris_command {id, cmd, params}`
- worker→page: `peris_ready {ok, errors, devices}`, `peris_state {id, state}`,
`peris_log {level, text}`
## 配置文件
## Config files
```json
{
@@ -98,30 +116,46 @@ export class View { // 运行在页面,渲染设备视图
}
```
- 自动保存于 localStorage(`peris.config.v1`,含 `chip`,页面加载时自动选中
对应芯片),面板头部的保存/打开图标导出/导入 `.json`。
- **导出文件名**:面板头部输入框设置(即项目名;留空 = 自动名
`peris-{chip}-{date}.json`),记忆于 localStorage;命名逻辑见
`peris-panel.ts exportFileName()`。
- **`firmware` 为备忘字段**:导出时记录最近加载固件的文件名/大小(/磁盘路径,
仅当宿主环境暴露非标准 `File.path` 时存在——浏览器安全模型不提供全路径)。
导入时忽略,不会自动加载固件;`chip` 在 localStorage 自动保存/恢复时会
选中对应芯片,Import 导入的 JSON 中则为备忘(不改当前选择)。
- v1 规则:每个 GPIO 同时只能被一个设备占用(任意方向);冲突会在面板顶部黄条提示。
- Autosaved to localStorage (`peris.config.v1`, including `chip`, which is
auto-selected when the page loads); the save/open icons in the panel head
export/import `.json`.
- **Export file name**: set in the panel-head input (it doubles as the project
name; empty = auto name `peris-{chip}-{date}.json`), remembered in
localStorage; naming logic in `peris-panel.ts exportFileName()`.
- **`firmware` is an informational field**: records the name/size(/disk path)
of the most recently loaded firmware at export time (the path exists only
when the host exposes the non-standard `File.path` — the browser security
model provides no full path). Import ignores it and never auto-loads
firmware; `chip` selects the matching chip when the localStorage config is
saved/restored, but in an imported JSON it is informational only (it does
not change the current selection).
- v1 rule: each GPIO can be claimed by at most one device (either direction);
conflicts surface in the yellow bar at the top of the pane.
## 设备开发注意事项(源自 M0 实测,见 FEASIBILITY.md)
## Device development notes (from M0 measurements; see FEASIBILITY.md)
1. **引脚输出**:固件主动 GPIO 输出在 `onPinState` 可见(out+oe);**LEDC/RMT 走 GPIO matrix 的信号不出现在引脚寄存器**,必须读 MMIO(LEDC duty 寄存器、RMT 通道 RAM),地址表在 `peris/mmio/`。
2. **引脚输入**:`ctx.setPinInput` 会真实进入固件(读值+中断),按键类设备在 `onEmuReady` 里要重新驱动电平(模拟器重载后电平复位)。
3. **时间**:`vTaskDelay` 期间模拟器时间快进,不要试图在 CPU 睡眠窗口做引脚级波形捕获;CPU 活跃的位绑定波形可在 run_batch 批间采样,但请节制(细批约 99 MIPS,粗批 5000+ MIPS)。
4. 状态推送节流 ~30fps;`getState()` 返回的对象会 postMessage,保持小而可序列化。
5. View 中触发**设备动作**统一走 `sendCmd(cmd, params)`;**修改配置**(如行内换色)
走 `host.updateConfig(patch)`——配置的唯一事实源在页面 PerisPanel,worker 侧
不得缓存/改配置。不要在页面侧直接操作模拟器。
1. **Pin outputs**: firmware GPIO writes are visible in `onPinState`
(out+oe); **signals routed through the GPIO matrix (LEDC/RMT) never appear
in the pin registers** — you must read MMIO (LEDC duty registers, RMT
channel RAM); address tables are in `peris/mmio/`.
2. **Pin inputs**: `ctx.setPinInput` genuinely reaches the firmware (reads +
interrupts); button-type devices must re-drive their level in
`onEmuReady` (levels reset when the emulator reloads).
3. **Time**: emulator time fast-forwards during `vTaskDelay`; do not attempt
pin-level waveform capture inside CPU sleep windows. Bit-banged waveforms
while the CPU is active can be sampled between run_batch calls, but keep
it modest (~99 MIPS with fine batches, 5000+ MIPS with coarse ones).
4. State pushes are throttled to ~30fps; the object returned by `getState()`
is postMessaged — keep it small and serialisable.
5. **Device actions** from the View always go through `sendCmd(cmd, params)`;
**config changes** (e.g. picking a colour from the row icon) go through
`host.updateConfig(patch)` — the single source of truth for config is the
page's PerisPanel; the worker side must never cache/mutate it. Never touch
the emulator from the page side.
## 示例
## Examples
- `peris/devices/led.js` — 输出型设备(sigbus 订阅)
- `peris/devices/button.js` — 输入型设备(setPinInput + onEmuReady)
- `peris/devices/touch-button.js` — 继承 button 的 Device,换 View
- `peris/devices/toggle-switch.js` — 保持型输入设备
- `peris/devices/led.js` — output device (sigbus subscription)
- `peris/devices/button.js` — input device (setPinInput + onEmuReady)
- `peris/devices/touch-button.js` — inherits button's Device, different View
- `peris/devices/toggle-switch.js` — latching input device
+129
View File
@@ -0,0 +1,129 @@
[English](DEVICE-API.md) | **简体中文**
# esp-emu-peris 外设接口规范(v1)
一个外设 = `peris/devices/` 下的一个 ES 模块 + `manifest.json` 里的一行。除此之外不需要改动项目任何其它文件。
> **语言**:插件用**原生 JavaScript**(丢文件即用,无需构建)。框架是 TypeScript 编译
> 产物,完整契约类型在 `peris/core/types.d.ts`(`DeviceMeta`/`DeviceLike`/`DeviceCtx`/
> `PinState` 等),插件里可通过 JSDoc 引用获得编辑器提示。注册表加载时会**运行时校验**
> 元数据(缺 `typeId`/`title`/`pins()`/`defaultConfig` 立即报错),单元测试
> `tests/unit/devices.test.mjs` 是插件电气语义的范例。
## 模块导出
```js
export class Device { // 运行在 worker,与模拟器交互
static typeId = 'led'; // 唯一、稳定、kebab-case
static version = '1.0.0'; // 插件自身版本(行头展示,登记于 docs/CHANGELOG.zh-cn.md)
static title = 'LED'; // UI 显示名
static category = 'basic'; // Add 菜单分组
static description = '...'; // 一行说明(tooltip)
static defaultConfig = {...}; // 新建实例的默认配置
static configSchema = [ // 通用配置编辑器字段(展开详情内,一条一行)
{ key: 'pin', label: 'GPIO pin', type: 'pin' },
{ key: 'color', label: 'Colour', type: 'color' },
{ key: 'name', label: 'Label', type: 'text' },
// type 还支持: number(min/max/step)、select(options:[{value,label}])、checkbox
// checkbox 可加 chip:'pull' + pullWhenChecked:'high'|'low'
// → 设备行上显示 V/G 闲置电平徽章(点击翻转),详见下文"行式 UI 约定"
];
static pins(cfg) { return [cfg.pin]; } // 声明占用的 GPIO(用于冲突检测)
constructor(ctx, cfg) {} // cfg = { id, type, ...设备配置 }
onPinState(pin, s) {} // 引脚输出变化回调 s={out, oe, cycles}
onCommand(cmd, params) {} // 页面 View 发来的用户操作
onEmuReady() {} // 模拟器(重)加载后:重新驱动输入电平
getState() { return {...}; } // 可序列化状态,推给页面 View(~30fps)
destroy() {} // 释放/恢复引脚
}
export class View { // 运行在页面,渲染设备视图
constructor(el, sendCmd, host) {} // el=详情区主体容器; sendCmd(cmd, params);
// host.updateConfig(patch) 请求改本设备配置
rowIcon(el) {} // 可选:行内交互图标(el≈24px 容器),见下文
render(state) {} // state 来自 Device.getState();行图标与详情主体一起驱动
setConfig(cfg) {} // 可选:配置编辑后回调(如换 LED 颜色)
}
```
## 行式 UI 约定(0.3.0+)
外设面板是三栏布局中的独立一栏(控制台 | 外设 | Debug),每种外设占一行:
```
[交互图标] [名称] [GPIO号] [V/G] [▸展开] [×删除] [⠿拖动]
```
- **交互图标**(`View.rowIcon(el)`):可点击的迷你控件,操作语义由插件定义——
LED 点击换色(取色器,经 `host.updateConfig({color})` 写回配置)、按键按下/
松开(连发 press/release)、拨动开关点击 toggle。`render(state)` 里同步刷新
图标状态(亮灭/按下/ON)。不实现 rowIcon 时显示首字母占位块。
**调用顺序**:面板固定先 `rowIcon` 后 `setConfig`,配置式样可覆盖到图标;
图标与详情主体是同一状态的两个视觉,插件必须共用同一 setter/render
(参考 led.js 的 `setColour()`),禁止为图标单写一份样式逻辑。
- **名称 / GPIO 方框 / V·G 徽章**:由面板通用渲染,点击原地修改
(名称与 GPIO 走内联编辑,V/G 徽章直接翻转)。GPIO 方框取 schema 第一个
`type:'pin'` 字段;V/G 徽章取标记 `chip:'pull'` 的 checkbox——勾选时若
`pullWhenChecked:'high'`(默认)显示 V(上拉到 VCC),否则 G;未勾选相反。
- **展开详情**:设备控件主体(`el`,即构造函数第一个参数)+ 全量配置编辑器
(一条属性一行)。行内放不下的高级功能一律放详情。
- **拖动排序** ⠿:按下拖柄才启用拖拽,松手回写 devices 顺序(不重建 worker)。
## ctx(worker 侧设备上下文)
| 成员 | 说明 |
|---|---|
| `ctx.id` / `ctx.chip` / `ctx.gpioCount` | 设备 id、芯片型号、GPIO 数 |
| `ctx.setPinInput(pin, high)` | 驱动引脚输入电平(固件可读到、可触发 GPIO 中断) |
| `ctx.subscribe(pins)` / `ctx.unsubscribe()` | 订阅引脚输出变化(onPinState) |
| `ctx.readMem(addr, len)` / `ctx.readU32(addr)` | 总线/MMIO 读取(LEDC duty、RMT RAM 等) |
| `ctx.cycles()` | 模拟器周期计数 |
| `ctx.markDirty()` | 请求尽快把 getState() 推给页面 |
| `ctx.log(text, level?)` | 在 UART 控制台打一条 `[Peris]` 日志 |
## 消息协议(页面 ↔ worker)
在原版 worker 协议之外增加(见 `peris/protocol.js`):
- 页面→worker:`peris_config {config}`(全量应用配置)、`peris_command {id, cmd, params}`
- worker→页面:`peris_ready {ok, errors, devices}`、`peris_state {id, state}`、`peris_log {level, text}`
## 配置文件
```json
{
"perisVersion": 1,
"chip": "esp32c3",
"firmware": { "name": "merged-binary.bin", "size": 250048 },
"devices": [ { "id": "led-1", "type": "led", "pin": 8, "color": "#ff4d4d", "name": "Red" } ]
}
```
- 自动保存于 localStorage(`peris.config.v1`,含 `chip`,页面加载时自动选中
对应芯片);面板头部的保存/打开图标导出/导入 `.json`。
- **导出文件名**:面板头部输入框设置(即项目名;留空 = 自动名
`peris-{chip}-{date}.json`),记忆于 localStorage;命名逻辑见
`peris-panel.ts exportFileName()`。
- **`firmware` 为备忘字段**:导出时记录最近加载固件的文件名/大小(/磁盘路径,
仅当宿主环境暴露非标准 `File.path` 时存在——浏览器安全模型不提供全路径)。
导入时忽略,不会自动加载固件;`chip` 在 localStorage 自动保存/恢复时会
选中对应芯片,Import 导入的 JSON 中则为备忘(不改当前选择)。
- v1 规则:每个 GPIO 同时只能被一个设备占用(任意方向);冲突会在面板顶部黄条提示。
## 设备开发注意事项(源自 M0 实测,见 FEASIBILITY.zh-cn.md)
1. **引脚输出**:固件主动 GPIO 输出在 `onPinState` 可见(out+oe);**LEDC/RMT 走 GPIO matrix 的信号不出现在引脚寄存器**,必须读 MMIO(LEDC duty 寄存器、RMT 通道 RAM),地址表在 `peris/mmio/`。
2. **引脚输入**:`ctx.setPinInput` 会真实进入固件(读值+中断),按键类设备在 `onEmuReady` 里要重新驱动电平(模拟器重载后电平复位)。
3. **时间**:`vTaskDelay` 期间模拟器时间快进,不要试图在 CPU 睡眠窗口做引脚级波形捕获;CPU 活跃的位绑定波形可在 run_batch 批间采样,但请节制(细批约 99 MIPS,粗批 5000+ MIPS)。
4. 状态推送节流 ~30fps;`getState()` 返回的对象会 postMessage,保持小而可序列化。
5. View 中触发**设备动作**统一走 `sendCmd(cmd, params)`;**修改配置**(如行内换色)
走 `host.updateConfig(patch)`——配置的唯一事实源在页面 PerisPanel,worker 侧
不得缓存/改配置。不要在页面侧直接操作模拟器。
## 示例
- `peris/devices/led.js` — 输出型设备(sigbus 订阅)
- `peris/devices/button.js` — 输入型设备(setPinInput + onEmuReady)
- `peris/devices/touch-button.js` — 继承 button 的 Device,换 View
- `peris/devices/toggle-switch.js` — 保持型输入设备
+63 -50
View File
@@ -1,75 +1,88 @@
# esp-emulator 浏览器 API 摘要(wasm v0.51.0)
**English** | [简体中文](EMU-API.zh-cn.md)
本文档是对 **闭源** `esp_emu_bg.wasm` 对 JS 暴露的全部能力的逆向整理,来源:官方
`www/pkg/esp_emu.js`(wasm-bindgen 胶水)、官方文档(esp-emulator README/BROWSER.md)
与本项目的 M0 实测(`tests/m0_probe.mjs`,结论详见 [FEASIBILITY.md](FEASIBILITY.md))。
上游升级后请重跑探针核对本文。
# esp-emulator browser API summary (wasm v0.51.0)
## 1. 加载与初始化
A reverse-engineered inventory of everything the **closed-source**
`esp_emu_bg.wasm` exposes to JS. Sources: the official `www/pkg/esp_emu.js`
(wasm-bindgen glue), official docs (esp-emulator README/BROWSER.md), and this
project's M0 measurements (`tests/m0_probe.mjs`, conclusions in
[FEASIBILITY.md](FEASIBILITY.md)). Re-run the probe and re-check this document
after an upstream upgrade.
## 1. Loading & initialisation
```js
import init, { WasmEmulator, initSync } from './pkg/esp_emu.js';
await init(); // fetch+instantiate esp_emu_bg.wasm(相对 esp_emu.js)
// 或 Node 环境:initSync(fs.readFileSync('esp_emu_bg.wasm')) // 同步初始化
await init(); // fetch+instantiate esp_emu_bg.wasm (relative to esp_emu.js)
// or in Node: initSync(fs.readFileSync('esp_emu_bg.wasm')) // synchronous init
const emu = new WasmEmulator('esp32c3'); // 'esp32c3'|'esp32c5'|'esp32c6'|'esp32h2'|'esp32p4'|'esp32s3'|'esp32s31'
```
## 2. 方法一览(全部同步;**无任何回调,只能轮询**)
## 2. Method inventory (all synchronous; **no callbacks — polling only**)
### 生命周期 / 加载
| 方法 | 说明 |
### Lifecycle / loading
| method | notes |
|---|---|
| `load_firmware(Uint8Array)` | **必须是 merged flash image**(bootloader@0x0+分区表@0x8000+app@0x10000,`idf.py merge-bin` 生成);裸 app bin 无法启动 |
| `load_rom_elf(Uint8Array)` / `load_default_rom()` / `has_default_rom()` | ROM;缺省用内嵌版本 |
| `load_efuse(Uint8Array)` / `load_app_elf(Uint8Array)` | eFuse / app ELF(BLE 符号拦截用) |
| `set_boot_from_rom(bool)` | 在 `load_firmware` 前调用 |
| `restart()` / `needs_restart()` | 软复位(保留 flash)/ 固件调用了 esp_restart |
| `load_firmware(Uint8Array)` | **must be a merged flash image** (bootloader@0x0 + partition table@0x8000 + app@0x10000, from `idf.py merge-bin`); a bare app bin never boots |
| `load_rom_elf(Uint8Array)` / `load_default_rom()` / `has_default_rom()` | ROM; embedded version used by default |
| `load_efuse(Uint8Array)` / `load_app_elf(Uint8Array)` | eFuse / app ELF (for BLE symbol interception) |
| `set_boot_from_rom(bool)` | call before `load_firmware` |
| `restart()` / `needs_restart()` | soft reset (flash preserved) / firmware called esp_restart |
### 执行(宿主驱动循环)
| 方法 | 说明 |
### Execution (host-driven loop)
| method | notes |
|---|---|
| `run_batch(n) → string` | 执行 ≤n 条指令,返回这批的 **UART0 输出字符串**(唯一输出通道) |
| `throttle_delay_ms(now) → number` | 距墙钟的漂移,宿主按此 sleep 以配速到实时 |
| `cycles()` / `cycles_per_us()` | 周期计数 / 主频(C3=160) |
| `run_batch(n) → string` | executes ≤n instructions, returns the batch's **UART0 output string** (the only output channel) |
| `throttle_delay_ms(now) → number` | drift from wall clock; the host sleeps by this to pace to real time |
| `cycles()` / `cycles_per_us()` | cycle counter / clock (C3 = 160) |
### CPU 状态
| 方法 | 说明 |
### CPU state
| method | notes |
|---|---|
| `pc()` / `hart_pc(h)` / `num_harts()` / `is_xtensa()` | PC、每核 PC、核数、ISA |
| `registers() → Uint32Array` / `get_reg(i)` | RV32: x0..x31;Xtensa: a0..a15 + PS,SAR,WINDOWBASE,WINDOWSTART,EXCCAUSE,EXCVADDR,EPC1,LBEG,LEND,LCOUNT |
| `read_memory(addr, len) → Uint8Array` | **总线读,MMIO 可读**(LEDC/RMT/GPIO matrix 等),未映射返回 0,单次上限 4 KiB |
| `pc()` / `hart_pc(h)` / `num_harts()` / `is_xtensa()` | PC, per-hart PC, hart count, ISA |
| `registers() → Uint32Array` / `get_reg(i)` | RV32: x0..x31; Xtensa: a0..a15 + PS,SAR,WINDOWBASE,WINDOWSTART,EXCCAUSE,EXCVADDR,EPC1,LBEG,LEND,LCOUNT |
| `read_memory(addr, len) → Uint8Array` | **bus read; MMIO is readable** (LEDC/RMT/GPIO matrix etc.), unmapped returns 0, 4 KiB per call |
### GPIO(本项目的主要接口)
| 方法 | 说明 |
### GPIO (this project's main interface)
| method | notes |
|---|---|
| `gpio_set_input(pin, high)` | 驱动 pad 输入电平;固件可读到,**可触发 GPIO 中断**(M0 实测 6/6);默认电平高 |
| `gpio_state() → [out_lo, out_hi, enable_lo, enable_hi]` | GPIO0-63 输出与输出使能寄存器镜像;**仅 CPU 直写 GPIO 出现于此**——LEDC/RMT 等经 GPIO matrix 的信号不体现(M0 实测) |
| `gpio_set_input(pin, high)` | drives a pad input level; firmware reads it and **GPIO interrupts fire** (M0: 6/6); default level high |
| `gpio_state() → [out_lo, out_hi, enable_lo, enable_hi]` | mirror of the GPIO0-63 output and output-enable registers; **only CPU-direct GPIO writes appear here** — signals through the GPIO matrix (LEDC/RMT etc.) do not (M0 measurement) |
### UART / 网络
| 方法 | 说明 |
### UART / network
| method | notes |
|---|---|
| `uart_input(Uint8Array)` | 字节注入 UART0 RX |
| `set_wifi_config(ssid, pwd)` / `set_wifi_auth(ssid, pwd, auth, sae_pwe)` | 模拟 soft AP |
| `wifi_rx_push(Uint8Array)` / `wifi_tx_drain() → Uint8Array` | 以太网帧 ↔ WiFi;TX 为 u32 长度前缀拼接,需外部 WebSocket TAP 代理(`tools/ws-net-proxy.py`) |
| `uart_input(Uint8Array)` | inject bytes into UART0 RX |
| `set_wifi_config(ssid, pwd)` / `set_wifi_auth(ssid, pwd, auth, sae_pwe)` | simulated soft AP |
| `wifi_rx_push(Uint8Array)` / `wifi_tx_drain() → Uint8Array` | ethernet frames ↔ WiFi; TX is u32-length-prefixed concatenation, needs an external WebSocket TAP proxy (`tools/ws-net-proxy.py`) |
## 3. M0 实测的寄存器级事实(ESP32-C3)
## 3. Register-level facts measured in M0 (ESP32-C3)
| 事实 | 数值/说明 |
| fact | value / notes |
|---|---|
| LEDC duty 寄存器 | `0x60019000 + 0x14*n + {0,4,8,C,10}`(CONF0/HPOINT/DUTY/CONF1/DUTY_R);**reg = duty×16**(bits[18:4]);`read_memory` 精确可读(M0 Q2b 53/53) |
| RMT 通道 RAM | `0x60016400 + 0xC0*n`,48 字/通道;符号发送后**留存**可事后解码(M0 Q3b);字格式 `d0:15,l0:1,d1:15,l1:1` |
| GPIO matrix 路由 | `GPIO_FUNCn_OUT_SEL = 0x60004000+0x554+4n`;LEDC0 信号=0x2d,RMT ch0=0x33 → **可自动发现接线** |
| IO_MUX(C3) | GPIO0..11 连续于 `0x60009000+0x04+4n` |
| 时间快进 | `vTaskDelay` 期间 `cycles()` 大跳、不消耗完整 CPU 周期 → **CPU 睡眠窗口的引脚波形不可捕获**(与采样率无关) |
| 其它 | GPIO_COUNT:C3=22/C5=29/C6=31/H2=28/P4=55/S3=49(无 22-25)/S31=32 |
| LEDC duty registers | `0x60019000 + 0x14*n + {0,4,8,C,10}` (CONF0/HPOINT/DUTY/CONF1/DUTY_R); **reg = duty×16** (bits[18:4]); precisely readable via `read_memory` (M0 Q2b 53/53) |
| RMT channel RAM | `0x60016400 + 0xC0*n`, 48 words/channel; symbols **remain after transmission** and can be decoded afterwards (M0 Q3b); word format `d0:15,l0:1,d1:15,l1:1` |
| GPIO matrix routing | `GPIO_FUNCn_OUT_SEL = 0x60004000+0x554+4n`; LEDC0 signal = 0x2d, RMT ch0 = 0x33 → **wiring can be auto-discovered** |
| IO_MUX (C3) | GPIO0..11 contiguous at `0x60009000+0x04+4n` |
| Time fast-forward | during `vTaskDelay` `cycles()` jumps ahead without spending full CPU cycles → **pin waveforms inside CPU sleep windows cannot be captured** (independent of sample rate) |
| Misc | GPIO_COUNT: C3=22 / C5=29 / C6=31 / H2=28 / P4=55 / S3=49 (no 22-25) / S31=32 |
## 4. 官方 worker 消息协议(www/worker.js,本项目 worker 复刻并扩展)
## 4. Official worker message protocol (www/worker.js; this project replicates and extends it)
- 页面→worker:`init{wasmUrl}`、`load{chip,firmware,ssid,password,rom?,efuse?}`、`start`、`stop`、`step`、`reset{chip}`、`uart_input{data}`、`gpio_input{pin,high}`、`read_mem{addr,len}`、`mem_watch{addr,len|null}`、`net_connect{url}`、`net_disconnect`、`set_batch_size{size}`
- worker→页面:`ready`、`loaded{chip,pc,isXtensa,numHarts,cyclesPerUs}`、`uart_output{data}`、`status{pc,cycles,mips,now}`、`snapshot{regs,pcs,gpio,cycles,mem?}`(150ms 节流)、`mem{addr,bytes}`、`step{output,pc,cycles}`、`restarted`、`reset{reloaded,pc?}`、`net_status{connected}`、`error{message}`
- page→worker: `init{wasmUrl}`, `load{chip,firmware,ssid,password,rom?,efuse?}`,
`start`, `stop`, `step`, `reset{chip}`, `uart_input{data}`,
`gpio_input{pin,high}`, `read_mem{addr,len}`, `mem_watch{addr,len|null}`,
`net_connect{url}`, `net_disconnect`, `set_batch_size{size}`
- worker→page: `ready`, `loaded{chip,pc,isXtensa,numHarts,cyclesPerUs}`,
`uart_output{data}`, `status{pc,cycles,mips,now}`,
`snapshot{regs,pcs,gpio,cycles,mem?}` (150ms throttle), `mem{addr,bytes}`,
`step{output,pc,cycles}`, `restarted`, `reset{reloaded,pc?}`,
`net_status{connected}`, `error{message}`
## 5. wasm 导出面之外(浏览器形态缺失,CLI 才有)
## 5. Beyond the wasm surface (browser build lacks these; CLI-only)
`--uart1-tcp`(外部串口设备)、`--i2c-dev`(哑寄存器 I2C 从机)、`--rmt-loopback`、
`--control-tcp`(flash 写擦)、`--ble-hci`、`--thread-sim`、GDB stub、快照保存。
这些正是 M5 路线图等待上游暴露的能力(见 [ROADMAP.md](ROADMAP.md))。
`--uart1-tcp` (external serial device), `--i2c-dev` (dumb-register I2C slave),
`--rmt-loopback`, `--control-tcp` (flash write/erase), `--ble-hci`,
`--thread-sim`, GDB stub, snapshot saving.
These are exactly the capabilities the M5 roadmap waits on
(see [ROADMAP.md](ROADMAP.md)).
+77
View File
@@ -0,0 +1,77 @@
[English](EMU-API.md) | **简体中文**
# esp-emulator 浏览器 API 摘要(wasm v0.51.0)
本文档是对 **闭源** `esp_emu_bg.wasm` 对 JS 暴露的全部能力的逆向整理,来源:官方
`www/pkg/esp_emu.js`(wasm-bindgen 胶水)、官方文档(esp-emulator README/BROWSER.md)
与本项目的 M0 实测(`tests/m0_probe.mjs`,结论详见 [FEASIBILITY.zh-cn.md](FEASIBILITY.zh-cn.md))。
上游升级后请重跑探针核对本文。
## 1. 加载与初始化
```js
import init, { WasmEmulator, initSync } from './pkg/esp_emu.js';
await init(); // fetch+instantiate esp_emu_bg.wasm(相对 esp_emu.js)
// 或 Node 环境:initSync(fs.readFileSync('esp_emu_bg.wasm')) // 同步初始化
const emu = new WasmEmulator('esp32c3'); // 'esp32c3'|'esp32c5'|'esp32c6'|'esp32h2'|'esp32p4'|'esp32s3'|'esp32s31'
```
## 2. 方法一览(全部同步;**无任何回调,只能轮询**)
### 生命周期 / 加载
| 方法 | 说明 |
|---|---|
| `load_firmware(Uint8Array)` | **必须是 merged flash image**(bootloader@0x0+分区表@0x8000+app@0x10000,`idf.py merge-bin` 生成);裸 app bin 无法启动 |
| `load_rom_elf(Uint8Array)` / `load_default_rom()` / `has_default_rom()` | ROM;缺省用内嵌版本 |
| `load_efuse(Uint8Array)` / `load_app_elf(Uint8Array)` | eFuse / app ELF(BLE 符号拦截用) |
| `set_boot_from_rom(bool)` | 在 `load_firmware` 前调用 |
| `restart()` / `needs_restart()` | 软复位(保留 flash)/ 固件调用了 esp_restart |
### 执行(宿主驱动循环)
| 方法 | 说明 |
|---|---|
| `run_batch(n) → string` | 执行 ≤n 条指令,返回这批的 **UART0 输出字符串**(唯一输出通道) |
| `throttle_delay_ms(now) → number` | 距墙钟的漂移,宿主按此 sleep 以配速到实时 |
| `cycles()` / `cycles_per_us()` | 周期计数 / 主频(C3=160) |
### CPU 状态
| 方法 | 说明 |
|---|---|
| `pc()` / `hart_pc(h)` / `num_harts()` / `is_xtensa()` | PC、每核 PC、核数、ISA |
| `registers() → Uint32Array` / `get_reg(i)` | RV32: x0..x31;Xtensa: a0..a15 + PS,SAR,WINDOWBASE,WINDOWSTART,EXCCAUSE,EXCVADDR,EPC1,LBEG,LEND,LCOUNT |
| `read_memory(addr, len) → Uint8Array` | **总线读,MMIO 可读**(LEDC/RMT/GPIO matrix 等),未映射返回 0,单次上限 4 KiB |
### GPIO(本项目的主要接口)
| 方法 | 说明 |
|---|---|
| `gpio_set_input(pin, high)` | 驱动 pad 输入电平;固件可读到,**可触发 GPIO 中断**(M0 实测 6/6);默认电平高 |
| `gpio_state() → [out_lo, out_hi, enable_lo, enable_hi]` | GPIO0-63 输出与输出使能寄存器镜像;**仅 CPU 直写 GPIO 出现于此**——LEDC/RMT 等经 GPIO matrix 的信号不体现(M0 实测) |
### UART / 网络
| 方法 | 说明 |
|---|---|
| `uart_input(Uint8Array)` | 字节注入 UART0 RX |
| `set_wifi_config(ssid, pwd)` / `set_wifi_auth(ssid, pwd, auth, sae_pwe)` | 模拟 soft AP |
| `wifi_rx_push(Uint8Array)` / `wifi_tx_drain() → Uint8Array` | 以太网帧 ↔ WiFi;TX 为 u32 长度前缀拼接,需外部 WebSocket TAP 代理(`tools/ws-net-proxy.py`) |
## 3. M0 实测的寄存器级事实(ESP32-C3)
| 事实 | 数值/说明 |
|---|---|
| LEDC duty 寄存器 | `0x60019000 + 0x14*n + {0,4,8,C,10}`(CONF0/HPOINT/DUTY/CONF1/DUTY_R);**reg = duty×16**(bits[18:4]);`read_memory` 精确可读(M0 Q2b 53/53) |
| RMT 通道 RAM | `0x60016400 + 0xC0*n`,48 字/通道;符号发送后**留存**可事后解码(M0 Q3b);字格式 `d0:15,l0:1,d1:15,l1:1` |
| GPIO matrix 路由 | `GPIO_FUNCn_OUT_SEL = 0x60004000+0x554+4n`;LEDC0 信号=0x2d,RMT ch0=0x33 → **可自动发现接线** |
| IO_MUX(C3) | GPIO0..11 连续于 `0x60009000+0x04+4n` |
| 时间快进 | `vTaskDelay` 期间 `cycles()` 大跳、不消耗完整 CPU 周期 → **CPU 睡眠窗口的引脚波形不可捕获**(与采样率无关) |
| 其它 | GPIO_COUNT:C3=22/C5=29/C6=31/H2=28/P4=55/S3=49(无 22-25)/S31=32 |
## 4. 官方 worker 消息协议(www/worker.js,本项目 worker 复刻并扩展)
- 页面→worker:`init{wasmUrl}`、`load{chip,firmware,ssid,password,rom?,efuse?}`、`start`、`stop`、`step`、`reset{chip}`、`uart_input{data}`、`gpio_input{pin,high}`、`read_mem{addr,len}`、`mem_watch{addr,len|null}`、`net_connect{url}`、`net_disconnect`、`set_batch_size{size}`
- worker→页面:`ready`、`loaded{chip,pc,isXtensa,numHarts,cyclesPerUs}`、`uart_output{data}`、`status{pc,cycles,mips,now}`、`snapshot{regs,pcs,gpio,cycles,mem?}`(150ms 节流)、`mem{addr,bytes}`、`step{output,pc,cycles}`、`restarted`、`reset{reloaded,pc?}`、`net_status{connected}`、`error{message}`
## 5. wasm 导出面之外(浏览器形态缺失,CLI 才有)
`--uart1-tcp`(外部串口设备)、`--i2c-dev`(哑寄存器 I2C 从机)、`--rmt-loopback`、
`--control-tcp`(flash 写擦)、`--ble-hci`、`--thread-sim`、GDB stub、快照保存。
这些正是 M5 路线图等待上游暴露的能力(见 [ROADMAP.zh-cn.md](ROADMAP.zh-cn.md))。
+75 -51
View File
@@ -1,65 +1,89 @@
# M0 可行性验证报告(esp-emu-peris)
**English** | [简体中文](FEASIBILITY.zh-cn.md)
日期:2026-10-29 · 环境:esp-emulator browser wasm v0.51.0(`_tools/esp-emu-browser/www/pkg`)
固件:ESP-IDF master(`C:\esp\master\esp-idf`,经 `C:\Users\A\Desktop\quick\idfm.bat` 环境)
复现:`node tests/m0_probe.mjs`(探针)+ `tests/firmware/peris_probe`(测试固件,`idf.py build && idf.py merge-bin` 后加载 `build/merged-binary.bin`)
# M0 feasibility report (esp-emu-peris)
## 结论总表
Date: 2026-10-29 · Environment: esp-emulator browser wasm v0.51.0
(`_tools/esp-emu-browser/www/pkg`)
Firmware: ESP-IDF master (`C:\esp\master\esp-idf`, env via
`C:\Users\A\Desktop\quick\idfm.bat`)
Reproduce: `node tests/m0_probe.mjs` (the probe) + `tests/firmware/peris_probe`
(test firmware; `idf.py build && idf.py merge-bin`, then load
`build/merged-binary.bin`)
| # | 问题 | 结论 | 证据 |
## Summary of conclusions
| # | question | result | evidence |
|---|---|---|---|
| Q1a | 固件 GPIO 输出能否被 `gpio_state()` 轮询观察到 | **PASS** | 8 次翻转全部可见(采样粒度 5 万指令/批) |
| Q1b | `gpio_set_input()` 驱动引脚能否触发固件 GPIO 中断 | **PASS** | 探针驱动 6 个下降沿,固件 ISR 计数 6/6 |
| Q2a | LEDC PWM 波形是否体现在 GPIO 输出寄存器(pad 级轮询测占空比) | **FAIL** | 引脚 oe=1 但 out 恒 0;duty=50%/100% 期间估计值恒 0 |
| Q2b | LEDC duty 寄存器能否用 `read_memory()` 读到且跟踪设定值 | **PASS** | 53/53 样本跟踪;`half(4096)→DUTY_R=0x10000=4096×16`,`max(8191)→0x1fff0` |
| Q3a | RMT/WS2812 波形能否用小批量 `run_batch(40)` 在引脚级采样解码 | **FAIL** | 2472 批采样中 out4 恒 0,无任何电平变化 |
| Q3b | RMT 通道 RAM 能否用 `read_memory()` 读出并解码 WS2812 帧 | **PASS** | 93 次快照解出 46 帧,匹配固件日志颜色 0x111111/0x222222/0x333333 |
| Q1a | can firmware GPIO output be observed by polling `gpio_state()` | **PASS** | all 8 toggles visible (sampling granularity 50k instructions/batch) |
| Q1b | can `gpio_set_input()` on a pin raise the firmware's GPIO interrupt | **PASS** | probe drove 6 falling edges; firmware ISR counted 6/6 |
| Q2a | do LEDC PWM waveforms show up in the GPIO output registers (pad-level duty-cycle polling) | **FAIL** | pin oe=1 but out constantly 0; estimates stuck at 0 during duty=50%/100% |
| Q2b | can LEDC duty registers be read via `read_memory()` and do they track the setpoint | **PASS** | 53/53 samples tracked; `half(4096)→DUTY_R=0x10000=4096×16`, `max(8191)→0x1fff0` |
| Q3a | can RMT/WS2812 waveforms be sampled at pin level with small `run_batch(40)` calls | **FAIL** | out4 constant 0 across 2472 sampled batches, no level change at all |
| Q3b | can RMT channel RAM be read via `read_memory()` and decoded into WS2812 frames | **PASS** | 46 frames decoded from 93 snapshots, colours matching the firmware log 0x111111/0x222222/0x333333 |
## 对外设方案的决定性影响
## Decisive impact on the peripheral plans
- **LED/物理按键/触摸按键(GPIO 语义)**:直接可行——Q1a+Q1b 全通过。
- **PWM 调光 LED**:走 **MMIO 寄存器路径**(Q2b),不走引脚采样(Q2a 已证不可行)。读
`LEDC_CHn_DUTY_R` 即得精确占空比,无混叠、无开销。
- **WS2812**:走 **RMT 通道 RAM 路径**(Q3b),不走引脚采样(Q3a 已证不可行)。符号在
发送后仍留存于 RAM,可事后解码。
- **位绑定(bit-bang)I2C/SPI/IR**:仍可行——Q1a 证明 CPU 主动翻转 GPIO_OUT 可见;
CPU 在翻转时不睡眠,不存在下面的"时间快进"问题。高速采样仅在 CPU 活跃窗口有效。
- **LED / push button / touch button (GPIO semantics)**: directly viable —
Q1a + Q1b fully pass.
- **PWM dimmed LED**: take the **MMIO register path** (Q2b), not pin sampling
(Q2a proven unviable). Reading `LEDC_CHn_DUTY_R` gives the exact duty cycle
— no aliasing, no overhead.
- **WS2812**: take the **RMT channel RAM path** (Q3b), not pin sampling (Q3a
proven unviable). Symbols remain in RAM after transmission and can be
decoded afterwards.
- **Bit-banged I2C/SPI/IR**: still viable — Q1a shows CPU-driven GPIO_OUT
flips are visible; the CPU does not sleep while flipping, so the
"time fast-forward" issue below does not apply. High-rate sampling only
works inside CPU-active windows.
## 关键技术细节
## Key technical details
### ESP32-C3 寄存器地址(来自 IDF soc 头文件)
### ESP32-C3 register addresses (from IDF soc headers)
| 寄存器 | 地址 | 备注 |
| register | address | notes |
|---|---|---|
| GPIO_OUT / GPIO_ENABLE | 0x60004004 / 0x60004020 | `gpio_state()` 即这两个字的镜像 |
| GPIO_FUNCn_OUT_SEL_CFG | 0x60004000+0x554+4n | 低 9 位 = GPIO matrix 输出信号选择 |
| IO_MUX_GPIO0..11_REG | 0x60009000+0x04+4n | bit10..12 MCU_SEL 等pad配置 |
| LEDC_CHn_(CONF0/HPOINT/DUTY/CONF1/DUTY_R) | 0x60019000+0x14n(+0/4/8/C/10) | C3 用 LSCHn 命名 |
| RMT_CHnCONF0 / 通道 RAM(RMTMEM) | 0x60016010+8n / 0x60016400+0xC0n | 每通道 48 字(192 字节) |
| GPIO_OUT / GPIO_ENABLE | 0x60004004 / 0x60004020 | `gpio_state()` mirrors exactly these words |
| GPIO_FUNCn_OUT_SEL_CFG | 0x60004000+0x554+4n | low 9 bits = GPIO-matrix output signal select |
| IO_MUX_GPIO0..11_REG | 0x60009000+0x04+4n | bits 10..12 MCU_SEL and other pad config |
| LEDC_CHn_(CONF0/HPOINT/DUTY/CONF1/DUTY_R) | 0x60019000+0x14n(+0/4/8/C/10) | C3 uses the LSCHn naming |
| RMT_CHnCONF0 / channel RAM (RMTMEM) | 0x60016010+8n / 0x60016400+0xC0n | 48 words (192 bytes) per channel |
### 实测行为要点
### Measured behaviour highlights
1. **LEDC duty 布局**:`DUTY_CH0` 字段在 bits[18:4],寄存器值 = duty × 16(与真机一致)。
2. **GPIO matrix 路由可读**:固件把 GPIO9 配给 LEDC0 后 `FUNC9_OUT_SEL=0x2d`(=45,
LEDC_LS_CH0_SIG),GPIO4 配给 RMT 后 `FUNC4_OUT_SEL=0x33`(=51,RMT_SIG_0)。
**外设接线的自动发现**可据此实现:轮询所有引脚的 OUT_SEL + IO_MUX 即可知道每个
pad 当前由哪个外设信号驱动。
3. **RMT RAM 留存**:发送完成后符号不清除;48 字/通道;>192 符号的帧走 ping-pong
(TX_THR_EVENT 重填+wrap),快照中可能同时存在相邻帧的片段,帧间以 reset 符号
(低电平 ≥30µs,即 d0≥300 且 l0=0)分界。
4. **位网格重同步**:本测试中 RAM 内位流相对 LED 字节网格缺头部 2 位(ping-pong 写入
顺序所致),前置补 0 搜索(0..7)即可对齐;真实设备解码器需实现该重同步。
5. **时间快进**:`vTaskDelay` 期间模拟器不消耗完整 CPU 周期(一批 `run_batch(40)` 中
`cycles()` 可跳数千),因此 **CPU 睡眠期间的外设波形在引脚级不可捕获**——这是 Q3a
失败的根本原因,与采样率无关。
6. **性能**(Node 24,直接 wasm 调用):粗粒度(50k 指令/批)≈ **5000+ MIPS**;细粒度
(40 指令/批 + 每批 `gpio_state()`)≈ **99 MIPS**(约 53 倍开销)。浏览器 worker 内
大致同量级——细粒度采样只应作为 CPU 活跃窗口的临时手段,常驻采样用粗粒度+MMIO。
7. **加载固件必须是合并镜像**(bootloader@0x0 + 分区表@0x8000 + app@0x10000),
用 `idf.py merge-bin` 生成;裸 app bin 不能启动。
1. **LEDC duty layout**: the `DUTY_CH0` field sits in bits[18:4]; register
value = duty × 16 (same as real hardware).
2. **GPIO-matrix routing is readable**: after the firmware maps GPIO9 to
LEDC0, `FUNC9_OUT_SEL=0x2d` (=45, LEDC_LS_CH0_SIG); after mapping GPIO4 to
RMT, `FUNC4_OUT_SEL=0x33` (=51, RMT_SIG_0). **Auto-discovery of peripheral
wiring** can be built on this: poll every pin's OUT_SEL + IO_MUX to learn
which peripheral signal currently drives each pad.
3. **RMT RAM retention**: symbols are not cleared after transmission;
48 words per channel; frames with >192 symbols use ping-pong
(TX_THR_EVENT refill + wrap), so a snapshot may contain fragments of
adjacent frames; frames are delimited by the reset symbol (low ≥30µs,
i.e. d0≥300 with l0=0).
4. **Bit-grid resynchronisation**: in this test the RAM bitstream was missing
2 head bits relative to the LED byte grid (ping-pong write order); a
leading-zero search (0..7) re-aligns it; real device decoders must
implement that resync.
5. **Time fast-forward**: during `vTaskDelay` the emulator does not spend
full CPU cycles (`cycles()` can jump by thousands inside one
`run_batch(40)`), therefore **peripheral waveforms during CPU sleep are
not capturable at pin level** — the root cause of the Q3a failure,
independent of sample rate.
6. **Performance** (Node 24, direct wasm calls): coarse (50k instr/batch) ≈
**5000+ MIPS**; fine (40 instr/batch + `gpio_state()` each batch) ≈
**99 MIPS** (~53× overhead). A browser worker is in the same ballpark —
fine-grained sampling should only be a temporary measure inside
CPU-active windows; standing sampling uses coarse batches + MMIO.
7. **Firmware must be a merged image** (bootloader@0x0 + partition
table@0x8000 + app@0x10000), produced by `idf.py merge-bin`; a bare app
bin does not boot.
## 已排除 / 仍受上游限制的能力
## Ruled out / still limited by upstream
- 硬件 I2C/SPI 外接设备模型、真触摸外设、ADC、I2S、SPI/RGB565 屏、SD 卡、UART1:
wasm 无对应 API,需等 esp-emulator 上游(见 docs/ROADMAP.md)。
- 引脚级 PWM/RMT 波形观察:结构性不可行(Q2a/Q3a),不依赖采样率。
- Hardware I2C/SPI external device models, a real touch peripheral, ADC,
I2S, SPI/RGB565 displays, SD cards, UART1: no wasm API — waiting on
esp-emulator upstream (see docs/ROADMAP.md).
- Pin-level PWM/RMT waveform observation: structurally unviable
(Q2a/Q3a), independent of sample rate.
+67
View File
@@ -0,0 +1,67 @@
[English](FEASIBILITY.md) | **简体中文**
# M0 可行性验证报告(esp-emu-peris)
日期:2026-10-29 · 环境:esp-emulator browser wasm v0.51.0(`_tools/esp-emu-browser/www/pkg`)
固件:ESP-IDF master(`C:\esp\master\esp-idf`,经 `C:\Users\A\Desktop\quick\idfm.bat` 环境)
复现:`node tests/m0_probe.mjs`(探针)+ `tests/firmware/peris_probe`(测试固件,`idf.py build && idf.py merge-bin` 后加载 `build/merged-binary.bin`)
## 结论总表
| # | 问题 | 结论 | 证据 |
|---|---|---|---|
| Q1a | 固件 GPIO 输出能否被 `gpio_state()` 轮询观察到 | **PASS** | 8 次翻转全部可见(采样粒度 5 万指令/批) |
| Q1b | `gpio_set_input()` 驱动引脚能否触发固件 GPIO 中断 | **PASS** | 探针驱动 6 个下降沿,固件 ISR 计数 6/6 |
| Q2a | LEDC PWM 波形是否体现在 GPIO 输出寄存器(pad 级轮询测占空比) | **FAIL** | 引脚 oe=1 但 out 恒 0;duty=50%/100% 期间估计值恒 0 |
| Q2b | LEDC duty 寄存器能否用 `read_memory()` 读到且跟踪设定值 | **PASS** | 53/53 样本跟踪;`half(4096)→DUTY_R=0x10000=4096×16`,`max(8191)→0x1fff0` |
| Q3a | RMT/WS2812 波形能否用小批量 `run_batch(40)` 在引脚级采样解码 | **FAIL** | 2472 批采样中 out4 恒 0,无任何电平变化 |
| Q3b | RMT 通道 RAM 能否用 `read_memory()` 读出并解码 WS2812 帧 | **PASS** | 93 次快照解出 46 帧,匹配固件日志颜色 0x111111/0x222222/0x333333 |
## 对外设方案的决定性影响
- **LED/物理按键/触摸按键(GPIO 语义)**:直接可行——Q1a+Q1b 全通过。
- **PWM 调光 LED**:走 **MMIO 寄存器路径**(Q2b),不走引脚采样(Q2a 已证不可行)。读
`LEDC_CHn_DUTY_R` 即得精确占空比,无混叠、无开销。
- **WS2812**:走 **RMT 通道 RAM 路径**(Q3b),不走引脚采样(Q3a 已证不可行)。符号在
发送后仍留存于 RAM,可事后解码。
- **位绑定(bit-bang)I2C/SPI/IR**:仍可行——Q1a 证明 CPU 主动翻转 GPIO_OUT 可见;
CPU 在翻转时不睡眠,不存在下面的"时间快进"问题。高速采样仅在 CPU 活跃窗口有效。
## 关键技术细节
### ESP32-C3 寄存器地址(来自 IDF soc 头文件)
| 寄存器 | 地址 | 备注 |
|---|---|---|
| GPIO_OUT / GPIO_ENABLE | 0x60004004 / 0x60004020 | `gpio_state()` 即这两个字的镜像 |
| GPIO_FUNCn_OUT_SEL_CFG | 0x60004000+0x554+4n | 低 9 位 = GPIO matrix 输出信号选择 |
| IO_MUX_GPIO0..11_REG | 0x60009000+0x04+4n | bit10..12 MCU_SEL 等pad配置 |
| LEDC_CHn_(CONF0/HPOINT/DUTY/CONF1/DUTY_R) | 0x60019000+0x14n(+0/4/8/C/10) | C3 用 LSCHn 命名 |
| RMT_CHnCONF0 / 通道 RAM(RMTMEM) | 0x60016010+8n / 0x60016400+0xC0n | 每通道 48 字(192 字节) |
### 实测行为要点
1. **LEDC duty 布局**:`DUTY_CH0` 字段在 bits[18:4],寄存器值 = duty × 16(与真机一致)。
2. **GPIO matrix 路由可读**:固件把 GPIO9 配给 LEDC0 后 `FUNC9_OUT_SEL=0x2d`(=45,
LEDC_LS_CH0_SIG),GPIO4 配给 RMT 后 `FUNC4_OUT_SEL=0x33`(=51,RMT_SIG_0)。
**外设接线的自动发现**可据此实现:轮询所有引脚的 OUT_SEL + IO_MUX 即可知道每个
pad 当前由哪个外设信号驱动。
3. **RMT RAM 留存**:发送完成后符号不清除;48 字/通道;>192 符号的帧走 ping-pong
(TX_THR_EVENT 重填+wrap),快照中可能同时存在相邻帧的片段,帧间以 reset 符号
(低电平 ≥30µs,即 d0≥300 且 l0=0)分界。
4. **位网格重同步**:本测试中 RAM 内位流相对 LED 字节网格缺头部 2 位(ping-pong 写入
顺序所致),前置补 0 搜索(0..7)即可对齐;真实设备解码器需实现该重同步。
5. **时间快进**:`vTaskDelay` 期间模拟器不消耗完整 CPU 周期(一批 `run_batch(40)` 中
`cycles()` 可跳数千),因此 **CPU 睡眠期间的外设波形在引脚级不可捕获**——这是 Q3a
失败的根本原因,与采样率无关。
6. **性能**(Node 24,直接 wasm 调用):粗粒度(50k 指令/批)≈ **5000+ MIPS**;细粒度
(40 指令/批 + 每批 `gpio_state()`)≈ **99 MIPS**(约 53 倍开销)。浏览器 worker 内
大致同量级——细粒度采样只应作为 CPU 活跃窗口的临时手段,常驻采样用粗粒度+MMIO。
7. **加载固件必须是合并镜像**(bootloader@0x0 + 分区表@0x8000 + app@0x10000),
用 `idf.py merge-bin` 生成;裸 app bin 不能启动。
## 已排除 / 仍受上游限制的能力
- 硬件 I2C/SPI 外接设备模型、真触摸外设、ADC、I2S、SPI/RGB565 屏、SD 卡、UART1:
wasm 无对应 API,需等 esp-emulator 上游(见 docs/ROADMAP.zh-cn.md)。
- 引脚级 PWM/RMT 波形观察:结构性不可行(Q2a/Q3a),不依赖采样率。
+46 -32
View File
@@ -1,44 +1,58 @@
# 路线图
**English** | [简体中文](ROADMAP.zh-cn.md)
## M2 · PWM 调光 LED(下一期)
# Roadmap
- `pwm-led.js` + `rgb-led.js`:读 LEDC 各通道 `DUTY_R` 寄存器(地址表 `peris/mmio/`),
精确占空比,无混叠(路线已在 M0 验证:Q2b PASS)。
- 通道→引脚映射:读 `GPIO_FUNCn_OUT_SEL_CFG`(M0 实测 LEDC0=0x2d、RMT0=0x33),
实现"自动发现接线"辅助;也允许手动指定通道/引脚。
- 呼吸灯平滑渲染(canvas 亮度插值)。
## M2 · PWM dimmed LED (next)
## M3 · WS2812 幻彩灯
- `pwm-led.js` + `rgb-led.js`: read each LEDC channel's `DUTY_R` register
(address tables in `peris/mmio/`) for exact duty cycles, no aliasing
(approach verified in M0: Q2b PASS).
- Channel→pin mapping: read `GPIO_FUNCn_OUT_SEL_CFG` (M0 measured LEDC0 =
0x2d, RMT0 = 0x33) to implement "auto-discovered wiring" assistance;
manual channel/pin selection also allowed.
- Breathing effect with smooth rendering (canvas brightness interpolation).
- `ws2812.js`:轮询 RMT 通道 RAM(M0 验证 Q3b PASS),符号→位流→GRB 帧;
ping-pong 分组 + reset 符号重同步 + 前置补零对齐(细节见 FEASIBILITY.md)。
- 灯条/灯环/矩阵三种 View;GRB/RGB 顺序、亮度限制、Gamma 可配。
- 性能策略:JS 解码先行;若实测不够,把"采样+解码"热路径用 C/C++ 编成独立 wasm
辅助模块在 worker 内调用(不动模拟器的闭源 wasm,不影响设备 JS 接口)。
## M3 · WS2812 addressable LEDs
## M4 · 位绑定协议设备 + IR
- `ws2812.js`: poll the RMT channel RAM (M0 verified Q3b PASS),
symbols → bitstream → GRB frames; ping-pong grouping + reset-symbol
resync + leading-zero alignment (details in FEASIBILITY.md).
- Strip / ring / matrix Views; GRB/RGB order, brightness cap and gamma
configurable.
- Performance strategy: JS decoding first; if measurements show it is not
enough, compile the hot "sample + decode" path into a separate wasm helper
module (C/C++) invoked from the worker (the emulator's closed-source wasm
stays untouched, the device JS interface unchanged).
- `bitbang-i2c-slave.js`(SCL/SDA 上模拟 24Cxx/BME280 等)、`ir-nec.js`(捕获固件 RMT/位绑定发射)。
- 前提:固件用 GPIO 位绑定(硬件 I2C/SPI 总线无法从外部接入,M0 已确认)。
需要配套测试固件与文档说明此约束。
- 高速采样调度(sigbus 扩展:临时小批量窗口,CPU 活跃期有效)。
## M4 · Bit-bang protocol devices + IR
## M5 · 上游能力跟进(不可控)
- `bitbang-i2c-slave.js` (emulating 24Cxx/BME280 etc. on SCL/SDA) and
`ir-nec.js` (capturing firmware RMT/bit-bang transmissions).
- Prerequisite: firmware uses GPIO bit-banging (hardware I2C/SPI buses
cannot be tapped from outside — confirmed in M0). Needs accompanying test
firmware and documentation of this constraint.
- High-rate sampling scheduler (sigbus extension: temporary fine-batch
windows, effective while the CPU is active).
以下能力当前 wasm 无 API,只能跟进 esp-emulator 新版本(升级流程见 UPGRADE.md):
## M5 · Upstream capability tracking (not under our control)
| 能力 | 依赖的上游接口 | 现状 |
These need APIs the wasm does not expose today; progress depends on new
esp-emulator releases (upgrade procedure in UPGRADE.md):
| capability | required upstream interface | status |
|---|---|---|
| 硬件 I2C 设备模型 | i2c slave/设备桥 API(仅 CLI `--i2c-dev`,且只有哑寄存器) | ❌ |
| 硬件 SPI 外设(SD 卡等) | spi 设备桥 | ❌ |
| SPI/RGB565 显示屏 | spi/lcd_cam(外设本身未模拟) | ❌ |
| 真触摸外设 | touch 外设模拟 | ❌(触摸按键以 GPIO 语义代替) |
| ADC(电位器等) | adc 模拟 | ❌ |
| I2S 音频 | i2s 模拟 | ❌ |
| UART1 外设 | wasm 暴露 uart1(CLI 有 `--uart1-tcp`) | ❌ |
| 状态快照保存/恢复 | save/load API | ❌ |
| hardware I2C device models | i2c slave/device bridge API (CLI-only `--i2c-dev`, dumb registers only) | ❌ |
| hardware SPI peripherals (SD card etc.) | spi device bridge | ❌ |
| SPI/RGB565 displays | spi/lcd_cam (the peripheral itself is not emulated) | ❌ |
| real touch peripheral | touch peripheral emulation | ❌ (touch button stands in with GPIO semantics) |
| ADC (potentiometers etc.) | adc emulation | ❌ |
| I2S audio | i2s emulation | ❌ |
| UART1 peripheral | wasm-exposed uart1 (CLI has `--uart1-tcp`) | ❌ |
| state snapshot save/restore | save/load API | ❌ |
## 维护项
## Maintenance items
- `update_esp_emu_browser.bat`(在 `_tools`,当前为空):按 UPGRADE.md 流程脚本化。
- `peris/mmio/`:补 C5/C6/H2/P4/S3 地址表(从各自 IDF soc 头文件提取,方法同 M0)。
- `update_esp_emu_browser.bat` (in `_tools`, currently empty): script the
UPGRADE.md procedure.
- `peris/mmio/`: add the C5/C6/H2/P4/S3 address tables (extracted from each
chip's IDF soc headers, same method as M0).
+46
View File
@@ -0,0 +1,46 @@
[English](ROADMAP.md) | **简体中文**
# 路线图
## M2 · PWM 调光 LED(下一期)
- `pwm-led.js` + `rgb-led.js`:读 LEDC 各通道 `DUTY_R` 寄存器(地址表 `peris/mmio/`),
精确占空比,无混叠(路线已在 M0 验证:Q2b PASS)。
- 通道→引脚映射:读 `GPIO_FUNCn_OUT_SEL_CFG`(M0 实测 LEDC0=0x2d、RMT0=0x33),
实现"自动发现接线"辅助;也允许手动指定通道/引脚。
- 呼吸灯平滑渲染(canvas 亮度插值)。
## M3 · WS2812 幻彩灯
- `ws2812.js`:轮询 RMT 通道 RAM(M0 验证 Q3b PASS),符号→位流→GRB 帧;
ping-pong 分组 + reset 符号重同步 + 前置补零对齐(细节见 FEASIBILITY.zh-cn.md)。
- 灯条/灯环/矩阵三种 View;GRB/RGB 顺序、亮度限制、Gamma 可配。
- 性能策略:JS 解码先行;若实测不够,把"采样+解码"热路径用 C/C++ 编成独立 wasm
辅助模块在 worker 内调用(不动模拟器的闭源 wasm,不影响设备 JS 接口)。
## M4 · 位绑定协议设备 + IR
- `bitbang-i2c-slave.js`(SCL/SDA 上模拟 24Cxx/BME280 等)、`ir-nec.js`(捕获固件 RMT/位绑定发射)。
- 前提:固件用 GPIO 位绑定(硬件 I2C/SPI 总线无法从外部接入,M0 已确认)。
需要配套测试固件与文档说明此约束。
- 高速采样调度(sigbus 扩展:临时小批量窗口,CPU 活跃期有效)。
## M5 · 上游能力跟进(不可控)
以下能力当前 wasm 无 API,只能跟进 esp-emulator 新版本(升级流程见 UPGRADE.zh-cn.md):
| 能力 | 依赖的上游接口 | 现状 |
|---|---|---|
| 硬件 I2C 设备模型 | i2c slave/设备桥 API(仅 CLI `--i2c-dev`,且只有哑寄存器) | ❌ |
| 硬件 SPI 外设(SD 卡等) | spi 设备桥 | ❌ |
| SPI/RGB565 显示屏 | spi/lcd_cam(外设本身未模拟) | ❌ |
| 真触摸外设 | touch 外设模拟 | ❌(触摸按键以 GPIO 语义代替) |
| ADC(电位器等) | adc 模拟 | ❌ |
| I2S 音频 | i2s 模拟 | ❌ |
| UART1 外设 | wasm 暴露 uart1(CLI 有 `--uart1-tcp`) | ❌ |
| 状态快照保存/恢复 | save/load API | ❌ |
## 维护项
- `update_esp_emu_browser.bat`(在 `_tools`,当前为空):按 UPGRADE.zh-cn.md 流程脚本化。
- `peris/mmio/`:补 C5/C6/H2/P4/S3 地址表(从各自 IDF soc 头文件提取,方法同 M0)。
+37 -22
View File
@@ -1,29 +1,44 @@
# 跟进上游 esp-emulator / esp-emu-browser 升级
**English** | [简体中文](UPGRADE.zh-cn.md)
## 锁定版本
# Tracking upstream esp-emulator / esp-emu-browser upgrades
当前基线:**esp-emulator v0.51.0**(`_tools/esp-emu-browser/www/pkg`,wasm-bindgen `--target web`)。
## Pinned baseline
## 升级步骤
Current baseline: **esp-emulator v0.51.0** (`_tools/esp-emu-browser/www/pkg`,
wasm-bindgen `--target web`).
1. 下载新版 wasm 包(如 `esp-emu-<ver>-wasm.tar.gz`)解压出新的 `www/`
(`_tools` 下已有 `update_esp_emu_browser.bat`,目前为空,可按此逻辑补全)。
2. **先跑 M0 探针**:`node tests/m0_probe.mjs <新固件路径不动,直接对 pkg>`。
探针直接 import 新 `pkg/esp_emu.js`,六项结论(Q1a/Q1b/Q2a/Q2b/Q3a/Q3b)若有变化,
先更新 `docs/FEASIBILITY.md` 与 `peris/mmio/` 地址表,再动其它代码。
3. **diff 官方 worker.js**:
## Upgrade steps
1. Download the new wasm package (e.g. `esp-emu-<ver>-wasm.tar.gz`) and
unpack the new `www/` (`_tools` already has
`update_esp_emu_browser.bat`, currently empty — fill it following this
procedure).
2. **Run the M0 probe first**: `node tests/m0_probe.mjs <firmware path
unchanged; run straight against the pkg>`. The probe imports the new
`pkg/esp_emu.js` directly; if any of the six conclusions
(Q1a/Q1b/Q2a/Q2b/Q3a/Q3b) changed, update `docs/FEASIBILITY.md` and the
`peris/mmio/` address tables before touching other code.
3. **Diff the official worker.js**:
`diff <new_www>/worker.js peris/worker.js`
我们对原文件的改动全部带 `peris:` 注释,逐块把上游新逻辑移植进 `peris/worker.js`:
- 新增/变更的消息类型 → 同步到 peris/worker.js 与 peris/app.js 的 switch
- 运行循环(run_batch/throttle/restart)变化 → 保持 `peris.onBatchEnd()` 钩子在每批之后
4. **diff 官方 app.js / index.html**:页面功能(终端/图表/寄存器/内存面板)改动按需
移植进 `peris/app.js` 与 `esp-emu-peris.html`(我们的文件是它们的超集复刻)。
5. 若 `WasmEmulator` API 变化(新增 GPIO/PWM/RMT 接口!),优先改用官方 API 简化设备实现——
那正是 ROADMAP.md 里等的东西。
6. GUI 冒测(加载固件、LED/按键、配置导入导出),提交。
All our changes to that file carry `peris:` comments; port the new
upstream logic block by block:
- new/changed message types → sync into the worker and the page's message
routing switch
- run-loop changes (run_batch/throttle/restart) → keep the
`peris.onBatchEnd()` hook after every batch
4. **Diff the official app.js / index.html**: port page features
(terminal/chart/register/memory panels) into our UI modules and
`esp-emu-peris.html` as needed (our files are superset replicas of
theirs).
5. If the `WasmEmulator` API changed (new GPIO/PWM/RMT interfaces!), prefer
the official API to simplify device implementations — that is exactly
what the roadmap has been waiting for.
6. Manual GUI pass (firmware load, LED/button, config import/export),
commit.
## 需要盯的上游能力(详见 ROADMAP.md)
## Upstream capabilities to watch (see ROADMAP.md)
- GPIO 之外的外设 API(i2c/spi 设备模型、rmt 回调、touch、adc)
- 事件回调机制(目前只能轮询)
- 浏览器包的 UART1 / 快照保存
- Peripheral APIs beyond GPIO (i2c/spi device models, rmt callbacks, touch,
adc)
- An event-callback mechanism (today it is polling only)
- UART1 / snapshot saving in the browser package
+31
View File
@@ -0,0 +1,31 @@
[English](UPGRADE.md) | **简体中文**
# 跟进上游 esp-emulator / esp-emu-browser 升级
## 锁定版本
当前基线:**esp-emulator v0.51.0**(`_tools/esp-emu-browser/www/pkg`,wasm-bindgen `--target web`)。
## 升级步骤
1. 下载新版 wasm 包(如 `esp-emu-<ver>-wasm.tar.gz`)解压出新的 `www/`
(`_tools` 下已有 `update_esp_emu_browser.bat`,目前为空,可按此逻辑补全)。
2. **先跑 M0 探针**:`node tests/m0_probe.mjs <新固件路径不动,直接对 pkg>`。
探针直接 import 新 `pkg/esp_emu.js`,六项结论(Q1a/Q1b/Q2a/Q2b/Q3a/Q3b)若有变化,
先更新 `docs/FEASIBILITY.zh-cn.md` 与 `peris/mmio/` 地址表,再动其它代码。
3. **diff 官方 worker.js**:
`diff <new_www>/worker.js peris/worker.js`
我们对原文件的改动全部带 `peris:` 注释,逐块把上游新逻辑移植进来:
- 新增/变更的消息类型 → 同步到 worker 与页面的消息路由 switch
- 运行循环(run_batch/throttle/restart)变化 → 保持 `peris.onBatchEnd()` 钩子在每批之后
4. **diff 官方 app.js / index.html**:页面功能(终端/图表/寄存器/内存面板)改动按需
移植进我们的 UI 模块与 `esp-emu-peris.html`(我们的文件是它们的超集复刻)。
5. 若 `WasmEmulator` API 变化(新增 GPIO/PWM/RMT 接口!),优先改用官方 API 简化设备实现——
那正是路线图等的东西。
6. GUI 冒测(加载固件、LED/按键、配置导入导出),提交。
## 需要盯的上游能力(详见 ROADMAP.zh-cn.md)
- GPIO 之外的外设 API(i2c/spi 设备模型、rmt 回调、touch、adc)
- 事件回调机制(目前只能轮询)
- 浏览器包的 UART1 / 快照保存
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "esp-emu-peris",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"type": "module",
"description": "Simulated peripherals for the esp-emulator browser (wasm) build",
+1 -1
View File
@@ -2,4 +2,4 @@
* Project version (keep in sync with package.json and docs/CHANGELOG.md).
* Device plugins carry their own per-device version in Device.version.
*/
export const PERIS_VERSION = '0.5.0';
export const PERIS_VERSION = '0.5.1';