- passive probes (new `static passive = true` device metadata): a pure
listener never drives an input and claims no pad, so it may clamp onto
ANY GPIO - including pads other devices own (an LED, an I2C pair, a
MAX7219 bus) and other analyzers. The runtime's pin-conflict check and
the wired-AND arbitration skip passive pads; the panel's usedPins
ignores them, so other devices' pickers never show them as "in use";
the analyzer's channel grid never disables a GPIO (pads owned elsewhere
get a dashed "shared" hint). Replaces the analyzer's sharedBus flag.
- trigger on any GPIO: trigCh (channel index) becomes trigPin (GPIO
number) - the trigger pad gets its own subscription and need not be an
analysed channel (off-channel triggers land in no lane, channel levels
still snapshot at capture start). The window dropdown lists every valid
GPIO. Tooltip documents the gpio_state visibility caveat: a trigger
fires only when the FIRMWARE drives the pad - pure-input pads (driven
by our button devices) never toggle the output registers.
- window cap lifted 1 s -> 1 h of guest time (was a conservative guard;
dense signals still freeze earlier at the 20 000-edge buffer with the
buffer-full note - the natural limit, since Saleae streams into host
RAM while we capture into a fixed buffer).
- tests: runtime-level passive sharing (probe onto an LED pad + I2C pair
+ second probe, zero conflicts), off-channel trigger, 1 h clamp,
trigger cases migrated to trigPin; device bumped to 1.1.0; GUI checked
(shared chip clickable, 23 trigger options, 5000 ms window accepted).
- docs (both languages): CHANGELOG 0.11.0 + device version table,
DEVICE-API passive metadata + instrument section, README capability
row, ROADMAP probe-freedoms bullet, AGENT rule 3.11; version synced to
0.11.0.
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.
- pwm-led.js + rgb-led.js: duty read from the LEDC DUTY_R register every
batch (exact, no aliasing - M0 Q2b path; pad-level PWM never reaches the
GPIO registers). Channel->pin wiring auto-discovered from the GPIO-matrix
FUNCn_OUT_SEL_CFG registers and followed across firmware rewiring; manual
channel selection too. RGB View mixes the three channels additively.
- Smooth breathing rendering: ~30fps state pushes are interpolated on a
canvas; one rAF animator per plugin repaints bulb + row icon through a
single _apply path (AGENT 3.8)
- Device API v1 additions (backwards compatible): optional onBatch() tick
after every run_batch, ctx.mmio (chip register tables via mmioFor); C3
table gains LEDC timer conf addresses + full ch0-5 signal ids (45-50);
shared device-side logic in peris/devices/ledc-lib.js (outside the
manifest, imported by both plugins and the tests)
- Test firmware: P2 now drives LEDC ch1 (GPIO6, mirrored ramp) + ch2
(GPIO7, half duty) alongside ch0; ###P2 markers unchanged, all six M0
conclusions re-verified. New tests/firmware/merge.ps1 (merge-bin -o is
relative to the build dir)
- Tests: fake-MMIO unit tests (DUTY_R decode across resolutions/timers,
matrix scan, auto/rebind, render math); smoke S7/S8 on real firmware
(auto-binding + duty tracking incl. mirrored r+g sum); terminal banner
padding adjusted for the version tag
- Docs (both languages): ROADMAP M2 -> shipped, CHANGELOG 0.6.0 + device
version table, DEVICE-API onBatch/ctx.mmio + register-level section,
ARCHITECTURE data flow, README capability table, AGENT rule 3.9 +
gotchas, FEASIBILITY follow-up note; examples/esp32c3-probe.json gains
the two LEDC devices; version synced to 0.6.0
- 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
- localStorage config now carries chip; on load the panel re-selects the
Target chip (select + machine info), so the GPIO grid / register table /
pin options match the wiring without picking the chip again; unknown
chip names are ignored safely
- Panel head merged into one row: PERIPHERALS title + project-name input
+ save/open icon buttons (Export/Import); toolbar reduced to the add
select + icon-only Add button
- PerisPanel construction moved before initGpioDefaults so the restored
chip feeds the initial debug panels; deps gain selectChip(chip)
- Docs: CHANGELOG 0.5.0, DEVICE-API config section, README
- Peripherals toolbar second row: export file name input (empty = auto
peris-{chip}-{date}.json); illegal chars stripped, single .json suffix;
remembered in localStorage (peris.exportName.v1); pure exportFileName()
unit-tested
- Exported JSON now records firmware {name, size, path?} alongside chip;
browsers do not expose disk paths, path filled only when the host
provides non-standard File.path/filePath; import ignores it
- app.ts tracks last loaded firmware info, clears on firmware-less reset
- Docs: DEVICE-API config section, CHANGELOG 0.4.0, README
- Build output moved to build/ (tsc outDir; worker-client, registry
DEVICES_DIR, html script src, tests, sync_www ITEMS updated; build/
gitignored, stale peris/core+ui removed)
- Version management: src/core/version.ts (project, 0.2.0) shown in the
header and terminal banner; per-device Device.version validated by the
registry and shown on card headers; docs/CHANGELOG.md records both
(project + device table)
- docs/EMU-API.md: full reverse-engineered summary of the closed-source
esp_emu_bg.wasm surface (lifecycle, execution, CPU state, GPIO/UART/
WiFi, M0-verified MMIO facts, stock worker protocol, CLI-only gaps)
- AGENT.md: environment pitfalls (mirrors, idfm.bat/MSYSTEM, merge-bin),
emulator behavior facts, architecture invariants, past-bug table,
test conventions and versioning rules for future agents
- README/ARCHITECTURE/DEVICE-API updated for the new layout
Architecture (docs/ARCHITECTURE.md):
- Framework (worker host + page UI) rewritten in TypeScript under src/,
compiled per-file to peris/core + peris/ui (tsc, no bundler; ESM layout
unchanged so the drop-into-www deployment model still works)
- Monolithic app.js split into 7 UI modules: app orchestrator,
worker-client (sole Worker owner), terminal, chart, debug-panels,
peris-panel, schema-driven config-editor; device plugins stay plain JS
(drop-in model) with runtime meta validation in the registry
- peris-specific CSS extracted to peris/peris.css; upstream style block
stays verbatim in the HTML for easy re-diff
- chips/MMIO tables single-sourced in TS data modules
Type contracts (src/core/types.ts): DeviceMeta/DeviceLike/DeviceCtx/
PinState/PerisConfig + normaliseConfig + validateDeviceModule, shipped
as .d.ts for plugin authors
Tests:
- tests/unit (node:test, zero deps): sigbus bit dispatch, chips/mmio
tables, config normalisation, module validation, device electrical
semantics (button active-low levels, LED invert, toggle persistence)
- integration smoke + M0 probe unchanged in role, wired into npm scripts
Bug found & fixed by the refactor: throttled flushStates silently
dropped the last dirty device state when changes stopped inside the
33 ms window - now schedules a trailing flush so the page always
converges
npm run build/check/test/serve/sync (typescript via npmmirror)