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.
English | 简体中文
esp-emu-peris
Simulated peripherals for the browser (wasm) build of Espressif's
esp-emulator: LEDs, push buttons,
touch buttons, toggle switches, PWM/RGB LEDs… — added as plugins, without
touching any stock file in the official www/.
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/ peripheral plugins (plain JS, drop-in, each with its own version):
led/button/touch-button/toggle-switch + pwm-led/rgb-led (register-level,
shared helpers in ledc-lib.js) + ws2812 (RMT channel RAM, ws2812-lib.js)
+ bitbang-i2c-slave (24C02/BME280, i2c-lib.js) + ir-nec (NEC receiver,
nec-lib.js) + max7219 (8-digit LED display, max7219-lib.js)
+ logic-analyzer (multi-GPIO waveform capture in a floating
window, la-lib.js; the bit-bang/protocol/instrument devices
ride the M4 fine-batch sampling scheduler)
vendor/ peris.css vendored xterm + panel styles
examples/ wiring-config examples (JSON, directly importable;
esp32c3-probe.json wires all 12 probe-firmware devices;
esp32s3-gun.json wires the MAX7219 for the gun 0.1.0
firmware)
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 environment/architecture/gotchas (read before contributing)
Architecture, data flow, state ownership and extension points: docs/ARCHITECTURE.md; versioning rules: docs/CHANGELOG.md (project version synced in three places; devices carry their own versions).
Quick start
# one-time: npm deps (typescript installs from the npmmirror registry)
npm install
# 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
# 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
# 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)
Publish the peripherals panel into the official www directory (add-only, never modifies; build first):
npm run sync # copies the entry html + peris/ (incl. build output) + examples/
Capabilities & limits (measured; details in docs/FEASIBILITY.md)
| Peripheral | Status | Approach |
|---|---|---|
| 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 LED | ✅ M2 | reads LEDC duty registers; channel→pin wiring auto-discovered from the GPIO matrix (pin-level PWM is invisible); canvas brightness interpolation for breathing effects |
| WS2812 addressable LEDs | ✅ M3 | decodes from RMT channel RAM (pin-level waveform invisible); strip/ring/matrix Views. Long strips recover only their tail — the 48-word channel RAM keeps the last 46 bits of a frame (same as real C3 hardware) |
| Bit-bang I2C slave (24C02 EEPROM, BME280) | ✅ M4 | level-driven protocol engine on two bit-banged GPIOs (SCL/SDA); answers via the SDA input path (open-drain idiom); several slaves may share one bus (wired-AND arbitration); BME280 compensation mirrored bit-exactly |
| IR NEC receiver | ✅ M4 | captures firmware codes from a bit-banged GPIO (fine-batch edge sampling) or an RMT TX channel (channel RAM); ratio-based decode (the guest clock is not wall-accurate); repeat frames included |
| MAX7219 display (8×8 matrix / 7-segment) | ✅ 0.9.0 | level-driven shift engine on a firmware bit-banged DIN/CLK/LOAD bus (hardware SPI is invisible, M0); datasheet register model incl. Code B decode, intensity, scan limit, shutdown/test; daisy chains share the bus; misaligned frames dropped and counted, never garbled |
| Logic analyzer (multi-GPIO waveforms) | ✅ 0.10.0 | up to 8 pads, channel+edge or immediate trigger, guest-time window, auto re-arm; Z shown when the pad is released; rides the M4 sampling scheduler (resolution presets); shown in a draggable/resizable non-modal floating window (the page underneath keeps working); Saleae-style plot: wheel zoom at the cursor, drag pan, hover pulse/period/frequency/duty measurement, A/B Δt cursors, edge jumping; taps other bus devices' pads legally (sharedBus) |
| Hardware I2C/SPI buses (bus-attached displays, SD, etc.), ADC, real touch, I2S | ⏸ M5 | waiting for esp-emulator upstream APIs |
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
- Drop a module into
peris/devices/exportingDeviceandView(interface: docs/DEVICE-API.md); - Add one line to
peris/devices/manifest.json. Done — page and worker pick it up automatically.
// 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) { }
}
Test firmware & tests
# build the firmware (IDF env from C:\Users\A\Desktop\quick\idfm.bat)
powershell -ExecutionPolicy Bypass -NoProfile -File tests\firmware\build.ps1
# merged image for loading (-o is relative to the build dir!):
powershell -ExecutionPolicy Bypass -NoProfile -File tests\firmware\merge.ps1
# tests/firmware/peris_probe/build/merged-binary.bin
npm test # unit + integration smoke (test layering in docs/ARCHITECTURE.md)
npm run test:m0 # emulator capability probe (run first on upstream upgrades)
The firmware runs a five-phase loop: GPIO8 blink / digital-in sampling
(###P1b: toggle-switch level on GPIO0, touch pad on GPIO18, button level on
GPIO3, one line per 125 ms, LED mirrors the switch, touch edges counted) /
LEDC breathing on GPIO9+GPIO6+GPIO7 (ch0 ramps, ch1 mirrors down, ch2 holds
half duty) / GPIO4 RMT-WS2812 frames / a ~20 kHz soft-I2C master on
SCL=GPIO1+SDA=GPIO2 (24C02 + BME280 transactions, BME compensation
cross-checked bit-exactly) plus NEC IR codes bit-banged on GPIO5 and sent via
RMT ch1 on GPIO10 — plus GPIO3 input-interrupt counting, for end-to-end
verification. Import examples/esp32c3-probe.json (wires all 12 devices to
those pins — the logic analyzer taps the I2C pair) after loading the merged
image to exercise every peripheral.
A second, real-world integration example: examples/esp32s3-gun.json wires
the MAX7219 display for the gun 0.1.0 release (ESP32-S3,
DIN=GPIO21/CLK=GPIO13/LOAD=GPIO14, bit-banged transport) — load
gun-0.1.0-merged.bin and the boot register-init sequence and the value
pushes decode live. The smoke test runs the same firmware as S12 when the
release binary is on this machine.
Note: the page itself has no CDN dependency — xterm/xterm-addon-fit are
vendored into peris/vendor/ (unminified npm builds), so it works offline.