0.9.0 - MAX7219 Display (max7219.js v1.0.0, engine in max7219-lib.js): - 8-digit LED driver on a firmware bit-banged DIN/CLK/LOAD bus (hardware SPI stays invisible, M0). Level-driven shift engine samples DIN on the CLK rising edge, latches MSB-first 16-bit words on the LOAD rising edge; misaligned frames are dropped and counted in `dropped`, never garbled. Datasheet register model: Code B decode (independent DP), intensity PWM, scan limit, shutdown/display test, power-on defaults. 8x8 matrix and 7-segment Views auto-selected from the decode-mode register; daisy chains share the three pads (sharedBus) and latch their chain position. Lease sizing measured against the tightest real firmware (gun 0.1.0, delay-free pulses ~20 instructions apart): fine at the scheduler floor 20, arm 60. Smoke S12 boots the gun 0.1.0 release (machine-local, skips when absent) with zero drops; examples/esp32s3-gun.json wires it. 0.10.0 - Logic analyzer (logic-analyzer.js v1.0.0, logic in la-lib.js): - Multi-channel GPIO waveform capture: up to 8 pads, channel+edge or immediate trigger, 1-1000 ms guest-time window, auto re-arm, Z shown when the pad is released; rides the M4 scheduler (arm 1000, fine lease per resolution preset 20/100/300, renewed per edge); 20 000-edge buffer freezes with an overflow flag. sharedBus makes it a pure tap on other bus devices' pads (smoke S13 arms it on the P4 soft-I2C bus: SCL 1950 / SDA 352 alternating edges in a 40 ms window, frozen afterwards). - Floating analyzer window: draggable, resizable, non-modal popup (position fixed, no backdrop - the page underneath keeps every pointer event; geometry clamped + persisted). One drawWaveform painter feeds the popup, the details mini-preview and the row icon (AGENT 3.8); channels picked from a GPIO grid, occupied pins greyed. - Saleae-style plot interactions: wheel zoom anchored at the cursor (1 ns floor), left/middle-drag pan, double-click/F fit; hover measurement shades the pulse and reads level + width + period + frequency + PWM duty (rise-to-rise cycle); A/B cursors with delta-t and 1/delta-t; lane selection + </> buttons and arrow keys jump view-centre onto the previous/next edge; lane gutter shows the right-edge level; a fresh capture re-fits the view and drops cursors (capId). - Device API v1 additions (backwards compatible): ctx.pinLevel(pin) snapshot read of the driven level; ViewHost.pinInfo() for multi-pin pickers; View.destroy() teardown on row removal / list rebuild so Views owning DOM outside their row (floating windows) clean it up. - Tests: la-lib units (parsePins, levelAt, axis ticks, fit span, pixel transform, zoom/pan view model, pulseAround, pwmOf, fmtHz/levelChar), the capture state machine via fake ctx (immediate/triggered starts, direction filtering incl. Z-as-falling, window end, stop/clear/auto, buffer overflow, onEmuReady reset, capId), sigbus.pinLevel against a fake emulator; smoke S13 + S1 grows to 12 devices; examples/esp32c3-probe.json wires the analyzer on the I2C pair. - Docs (both languages): CHANGELOG 0.9.0 + 0.10.0 + device version table, ROADMAP shipped sections, DEVICE-API instrument section + new ctx/host/ View members, ARCHITECTURE GUI checklist + floating-window note, README capability table + tree, AGENT rule 3.11 + GUI-smoke bug-table rows; version synced to 0.10.0 in all three places.
11 KiB
English | 简体中文
esp-emu-peris system architecture
1. Layered overview
┌───────────────────────────────── 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 (protocol in core/protocol.ts) │ DOM
┌───────────────┴───────────────────────────────────────────────▼────────────┐
│ page-side device Views (View classes of peris/devices/*.js, instantiated │
│ by peris-panel) │
├─────────────────────────────────────────────────────────────────────────────┤
│ ← peris_state / peris_command → │
├─────────────────────────────────────────────────────────────────────────────┤
│ 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 │
│ /onBatch MMIO tick + 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; shared helpers │
│ such as ledc-lib.js sit outside the manifest) │
└───────────────────────────────────────────────────┬─────────────────────────┘
│ WasmEmulator (closed-source
│ wasm, touched only here)
esp_emu_bg.wasm v0.51.0
2. Key decisions and why
| decision | rationale |
|---|---|
| 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. Data flow and state ownership
- Firmware → device: after each
run_batch()the worker callsruntime.onBatchEnd()→sigbus.sample()readsgpio_state()(four 32-bit words) and, using the union change mask (out|oe), dispatches oneonPinStateper changed pin. With no changes the cost is 1 wasm call + 4 XORs. Register-level devices (LEDC/RMT) additionally receive anonBatch()tick in the same call and poll MMIO throughctx.readU32/ctx.mmio— their signals never surface as pin changes (GPIO matrix, M0 Q2a/Q3a). - Device → page:
markDirty()→flushStatesthrottled 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. Views may own page-side DOM beyond their row (the logic-analyzer floating window isposition: fixedondocument.body, non-modal by design — no backdrop, so the rest of the page keeps every pointer event); the panel calls the optionalView.destroy()on row removal and list rebuilds so such DOM cannot outlive its device. - Input pin levels: the only writers are
ctx.setPinInput(devices) and the Debug grid'sgpio_input. The page-sidegpioInputLevels[]is only the Debug grid's own mirror, not authoritative state; the authority lives in the devices (re-driven ononEmuReady). - Config: the page's
PerisPanel.configis the single source of truth; every change is sent in full viaperis_config(the worker destroys and rebuilds instances) and written to localStorage. Pin conflicts are validated by the worker and reported back throughperis_ready.errors, shown as a yellow bar on the page.
4. Directory responsibilities
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; shared
helper modules like ledc-lib.js live here but outside the
manifest)
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. Build & tests
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)
Test layering: unit (pure logic: bit math / level semantics / validation /
row-badge derivation / LEDC register decode against fake MMIO, plus the M4
protocol layer — scheduler leases, the I2C slave engine under a synthetic
open-drain master, register models, NEC decode — fast, pinpoints failures) →
integration (real wasm + firmware, full chain; S7/S8 drive by the ###P2
markers, S9 by ###P3, S10/S11 — soft-I2C slaves + both NEC capture paths —
by ###P4; every window is marker-bounded because armed batches fast-forward
whole sleep phases) → 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, PWM/RGB LED auto-binding to the firmware's LEDC channels with smooth
breathing during the P2 phase, the I2C transaction log filling during P4 with
the EEPROM hex dump editable from the View, IR codes appearing on both
ir-nec rows, logic-analyzer: add one, pick channels from the GPIO grid in
the floating window (occupied pins greyed), arm + waveform rendering while
the P4 soft-I2C bus runs, window drag/resize with the page underneath
still interactive, Saleae-style plot checks (wheel zoom at the cursor,
drag pan, hover measurement shows pulse/period/frequency/duty, two clicks
drop A/B cursors with Δt + 1/Δt, double-click re-fits), window closes
when the row is removed, 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 procedure (summary; details in UPGRADE.md)
- After a new wasm package lands, run
npm run test:m0first and compare against FEASIBILITY.md; - diff the upstream worker.js ↔
src/core/worker.ts(all changes carryperis:markers); - diff upstream app.js/index.html ↔
src/ui/*.tsand the stock style section of the HTML; npm run build && npm test, manual GUI pass.
7. Extension points
- 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>.tsand register it inmmio.ts; add the pin count inchips.ts. - New page panel: add a module in ui/ and register it in app.ts's message routing — the worker protocol stays untouched.