Logic analyzer probe freedoms: passive pads, any-GPIO trigger, 1 h window (0.11.0)
- 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.
This commit is contained in:
1 parent
58452f4031
commit
7f5f1aaa88
19 files changed
+278
-88
No files matched your search
@@ -153,6 +153,10 @@ and `docs/FEASIBILITY.md` (M0 measurements).
|
||||
from `host.pinInfo()` — the same candidate-aware pin set the generic
|
||||
editor uses, never a forked copy. `ctx.pinLevel(pin)` reads the current
|
||||
driven level for snapshots only; the change stream stays `onPinState`.
|
||||
Pure listeners declare `static passive = true` (0.11): the runtime's
|
||||
conflict check and wired-AND skip their pads, and other devices' pin
|
||||
pickers never show them as used — a probe clamps anywhere, an active
|
||||
device does not.
|
||||
|
||||
## 4. Past bugs (prevent regressions)
|
||||
|
||||
|
||||
+3
-1
@@ -117,7 +117,9 @@ esp-emulator wasm 能力边界见 `docs/EMU-API.zh-cn.md` 与 `docs/FEASIBILITY.
|
||||
View 必须实现 `destroy()`(面板在行移除与列表重建时调用),多引脚选择
|
||||
器必须基于 `host.pinInfo()` 构建——与通用编辑器同一份、按候选设备感知
|
||||
的引脚集合,禁止另抄一份。`ctx.pinLevel(pin)` 只用于快照式读取当前驱动
|
||||
电平;变化流仍然走 `onPinState`。
|
||||
电平;变化流仍然走 `onPinState`。纯监听设备声明 `static passive =
|
||||
true`(0.11):runtime 的冲突检测与线与仲裁跳过其焊盘,其他设备的引脚
|
||||
选择器也不把它们显示为已占用——探针可搭接任意位置,主动设备不行。
|
||||
|
||||
## 4. 曾踩过的 bug(防止回潮)
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ esp-emulator version: `./install.sh --version 0.52.0` (or
|
||||
| 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`) |
|
||||
| Logic analyzer (multi-GPIO waveforms) | ✅ 0.10.0 | up to 8 pads, any-GPIO + edge or immediate trigger (0.11), guest-time window up to 1 h, 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; `passive` probe — clamps onto ANY in-use GPIO without conflicts |
|
||||
| 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
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ python serve.py # http://127.0.0.1:8080/esp-emu-peris.html
|
||||
| 位绑定 I2C 从机(24C02 EEPROM、BME280) | ✅ M4 | 两个位绑定 GPIO(SCL/SDA)上的电平驱动协议引擎;经 SDA 输入路径应答(开漏惯用法);多个从机可共享一条总线(线与仲裁);BME280 补偿逐位精确镜像 |
|
||||
| IR NEC 接收器 | ✅ M4 | 双路捕获固件码:位绑定 GPIO(细批边沿采样)或 RMT TX 通道(通道 RAM);按比值解码(客户时钟不按墙钟走);支持重复帧 |
|
||||
| MAX7219 显示(8×8 矩阵/七段数码管) | ✅ 0.9.0 | 固件位绑定 DIN/CLK/LOAD 总线上的电平驱动移位引擎(硬件 SPI 不可见,M0);寄存器模型遵循数据表(Code B 译码、亮度、扫描界、停机/测试);菊花链共享总线;失准帧丢弃并计数,绝不出花屏 |
|
||||
| 逻辑分析仪(多 GPIO 波形) | ✅ 0.10.0 | 最多 8 个引脚,通道+边沿或立即触发,客机时间窗口,自动重新武装;引脚释放显示 Z;依托 M4 采样调度器(分辨率预设);界面在可拖动/可调大小的非模态浮动窗口中(下层页面照常交互);Saleae 风格绘图:滚轮以光标为中心缩放、拖拽平移、悬停测脉宽/周期/频率/占空比、A/B 游标测 Δt、边沿跳转;`sharedBus` 使其可合法搭接其他总线设备的引脚 |
|
||||
| 逻辑分析仪(多 GPIO 波形) | ✅ 0.10.0 | 最多 8 个引脚,任意 GPIO+边沿或立即触发(0.11),客机时间窗口最长 1 小时,自动重新武装;引脚释放显示 Z;依托 M4 采样调度器(分辨率预设);界面在可拖动/可调大小的非模态浮动窗口中(下层页面照常交互);Saleae 风格绘图:滚轮以光标为中心缩放、拖拽平移、悬停测脉宽/周期/频率/占空比、A/B 游标测 Δt、边沿跳转;`passive` 探针——可无冲突搭接任意已占用 GPIO |
|
||||
| 硬件 I2C/SPI 总线(总线挂载的显示屏、SD 等)、ADC、真触摸、I2S | ⏸ M5 | 等 esp-emulator 上游暴露 API |
|
||||
|
||||
注:触摸**外设**未被模拟——触摸按键以 GPIO 输入呈现,固件按 GPIO 读取即可
|
||||
|
||||
@@ -8,6 +8,44 @@ CHANGELOG.zh-cn.md).
|
||||
Rules: major = breaking protocol/architecture change; minor = new capability;
|
||||
patch = fixes and small changes.
|
||||
|
||||
## peris 0.11.0
|
||||
|
||||
- **Logic analyzer probes anything** (`logic-analyzer.js` v1.1.0): the new
|
||||
`static passive = true` device metadata marks a pure listener — it 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 conflict check skips passive pads,
|
||||
they stay out of the wired-AND arbitration, and other devices' pin
|
||||
pickers no longer show them as "in use"; the analyzer's channel grid
|
||||
never disables a GPIO (pads owned elsewhere get a dashed "shared" hint).
|
||||
- **Trigger on any GPIO** (not only analysed channels): `trigCh` (channel
|
||||
index) becomes `trigPin` (GPIO number) — the trigger pad gets its own
|
||||
subscription and need not be a channel; the window dropdown lists every
|
||||
valid GPIO. Visibility caveat, surfaced in the tooltip: a trigger fires
|
||||
only when the FIRMWARE drives the pad (gpio_state shows CPU GPIO
|
||||
writes) — pure-input pads driven by our button devices never toggle.
|
||||
- **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 — that is the natural limit (Saleae streams
|
||||
into host RAM, we capture into a fixed buffer); quiet signals can now
|
||||
record for minutes.
|
||||
- Tests: passive sharing at the runtime level (probe onto an LED pad +
|
||||
I2C pair + second probe, no conflicts), off-channel trigger (edge lands
|
||||
in no lane, channel levels still snapshot), 1 h clamp, migrated
|
||||
trigger cases. GUI: shared chip clickable, 23 trigger options
|
||||
(Immediate + all 22 C3 GPIOs), 5000 ms window accepted.
|
||||
|
||||
| device | version |
|
||||
|---|---|
|
||||
| led | 1.1.1 |
|
||||
| button / touch-button / toggle-switch | 1.1.0 |
|
||||
| pwm-led / rgb-led | 1.0.0 |
|
||||
| ws2812 | 1.0.0 |
|
||||
| bitbang-i2c-slave | 1.0.0 |
|
||||
| ir-nec | 1.0.0 |
|
||||
| max7219 | 1.0.0 |
|
||||
| logic-analyzer | 1.1.0 |
|
||||
|
||||
## peris 0.10.3
|
||||
|
||||
- **`python serve.py` self-bootstraps**: when the default
|
||||
|
||||
@@ -6,6 +6,40 @@
|
||||
与英文版 CHANGELOG.md)。
|
||||
规则:主版本=协议/架构破坏性变更;次版本=新能力;修订=修复与小改。
|
||||
|
||||
## peris 0.11.0
|
||||
|
||||
- **逻辑分析仪可探测任意引脚**(`logic-analyzer.js` v1.1.0):新增
|
||||
`static passive = true` 设备元数据标记纯监听者——从不驱动输入、不声明
|
||||
焊盘,因此可搭接**任意 GPIO**,包括其他设备占用的引脚(LED、I2C 双
|
||||
线、MAX7219 总线)以及其他分析仪。runtime 的冲突检测跳过 passive 焊
|
||||
盘、不参与线与仲裁,其他设备的引脚选择器不再把它们显示为 "in use";
|
||||
分析仪的通道网格不再禁用任何 GPIO(他设备占用的焊盘以虚线 "shared"
|
||||
样式提示)。
|
||||
- **任意 GPIO 触发**(不再限于分析通道):`trigCh`(通道索引)改为
|
||||
`trigPin`(GPIO 号)——触发焊盘独立订阅、无需是分析通道;窗口下拉列出
|
||||
全部有效 GPIO。可见性边界已在提示中说明:触发只在**固件驱动**该焊盘
|
||||
时有效(gpio_state 只反映 CPU 的 GPIO 写)——由我们的按键设备驱动的
|
||||
纯输入焊盘不会翻转。
|
||||
- **窗口上限放宽**:1 秒 → **1 小时**客机时间(原为保守护栏)。密集信
|
||||
号仍会在 2 万条边沿缓冲处提前冻结并显示 `buffer full`——这才是自然限
|
||||
制(Saleae 流式写入主机内存,我们是固定缓冲捕获);安静信号现在可以
|
||||
录制数分钟。
|
||||
- 测试:runtime 层 passive 共享(探针搭 LED 焊盘 + I2C 双线 + 第二个探
|
||||
针,零冲突)、非通道触发(边沿不落入任何泳道、通道电平仍快照)、1 小
|
||||
时钳制、触发用例迁移。GUI:shared 芯片可点击、触发下拉 23 项
|
||||
(Immediate + C3 全部 22 个 GPIO)、5000 ms 窗口被接受。
|
||||
|
||||
| 设备 | 版本 |
|
||||
|---|---|
|
||||
| led | 1.1.1 |
|
||||
| button / touch-button / toggle-switch | 1.1.0 |
|
||||
| pwm-led / rgb-led | 1.0.0 |
|
||||
| ws2812 | 1.0.0 |
|
||||
| bitbang-i2c-slave | 1.0.0 |
|
||||
| ir-nec | 1.0.0 |
|
||||
| max7219 | 1.0.0 |
|
||||
| logic-analyzer | 1.1.0 |
|
||||
|
||||
## peris 0.10.3
|
||||
|
||||
- **`python serve.py` 自举**:默认的 `vendor/esp-emu-browser/www/pkg`
|
||||
|
||||
+12
-6
@@ -25,6 +25,10 @@ export class Device { // runs in the worker, talks to the emulator
|
||||
static defaultConfig = {...}; // defaults for new instances
|
||||
static sharedBus = true; // optional (M4): pins may overlap with
|
||||
// OTHER sharedBus devices (bus wiring)
|
||||
static passive = true; // optional (0.11): pure listener — never
|
||||
// drives an input, claims no pad; may
|
||||
// overlap with ANY device, and other
|
||||
// devices' pin pickers ignore its pads
|
||||
static configSchema = [ // generic config editor fields (in the
|
||||
// expanded details, one per line)
|
||||
{ key: 'pin', label: 'GPIO pin', type: 'pin' },
|
||||
@@ -192,18 +196,20 @@ level- or ratio-based, never absolute-µs. The M4 plugins follow both rules:
|
||||
## Instrument devices (0.10.0)
|
||||
|
||||
- `peris/devices/logic-analyzer.js` — multi-channel GPIO waveform capture
|
||||
(up to 8 pads, one trigger: channel + rising/falling/any edge or
|
||||
Immediate, guest-time window, auto re-arm). Readout math, the waveform
|
||||
painter and the floating window live in `peris/devices/la-lib.js`; the
|
||||
(up to 8 pads, one trigger: ANY GPIO + rising/falling/any edge or
|
||||
Immediate, guest-time window up to 1 h, auto re-arm). Readout math, the
|
||||
waveform painter and the floating window live in `peris/devices/la-lib.js`; the
|
||||
painter is one implementation shared by the popup, the details
|
||||
mini-preview and the row icon. Sampling rides the M4 scheduler like the
|
||||
bit-bang devices (arm lease while waiting for the trigger, fine lease
|
||||
renewed per edge while recording — the `resolution` config picks the
|
||||
fine batch: 20/100/300 instructions). Levels are 1/0 driven and -1 (Z)
|
||||
when the guest releases the pad; the analyzer never drives anything, so
|
||||
it declares `sharedBus` and legally taps pads other bus devices own.
|
||||
Capture data is pushed in full only when the buffer freezes (`done`);
|
||||
run pushes carry progress + live per-channel edge counts.
|
||||
it declares `passive` and legally taps pads ANY device owns (the trigger
|
||||
pad gets its own subscription and need not be a channel; it fires only
|
||||
when the firmware drives the pad — gpio_state never shows pure-input
|
||||
pads). Capture data is pushed in full only when the buffer freezes
|
||||
(`done`); run pushes carry progress + live per-channel edge counts.
|
||||
- **Floating windows** (`la-lib.makeFloatingWindow`): big UIs open a
|
||||
draggable, resizable, **non-modal** popup — `position: fixed`, no
|
||||
backdrop, so the page underneath keeps every pointer event. Geometry is
|
||||
|
||||
+16
-11
@@ -30,6 +30,9 @@ export class Device { // 运行在 worker,与模拟器交互
|
||||
];
|
||||
static sharedBus = true; // 可选(M4):允许与其他 sharedBus 设备
|
||||
// 共享引脚(总线接线,如两个 I2C 从机)
|
||||
static passive = true; // 可选(0.11):纯监听——从不驱动输入、
|
||||
// 不声明焊盘;可与任意设备搭接,其他
|
||||
// 设备的引脚选择器忽略它的焊盘
|
||||
static pins(cfg) { return [cfg.pin]; } // 声明占用的 GPIO(用于冲突检测)
|
||||
|
||||
constructor(ctx, cfg) {} // cfg = { id, type, ...设备配置 }
|
||||
@@ -125,24 +128,26 @@ M3 的 WS2812 插件遵循同一模式,共享库为 **`peris/devices/ws2812-lib.
|
||||
最后 46 位,长灯带只能解码出尾部 LED(见 FEASIBILITY.zh-cn.md
|
||||
"关键技术细节" 3–4)。
|
||||
|
||||
## 仪器类设备(0.10.0)
|
||||
## 仪器类设备(0.10.0;0.11 增强)
|
||||
|
||||
- `peris/devices/logic-analyzer.js` —— 多通道 GPIO 波形捕获(最多 8 个引
|
||||
脚,一个触发:通道 + 上升/下降/任意沿或"立即",客机时间窗口,自动重新
|
||||
武装)。读数数学、波形绘制器与浮动窗口都在 `peris/devices/la-lib.js`;
|
||||
绘制器只有一份实现,弹窗、详情迷你预览与行图标共用。采样复用 M4 调度
|
||||
器,与位绑定设备一致(等触发时持 arm 租约,录制期间每次边沿续租细批
|
||||
——`resolution` 配置决定细批:20/100/300 条指令)。电平为驱动 1/0、
|
||||
客机释放引脚时 -1(Z);分析仪从不驱动任何东西,因此声明 `sharedBus`,
|
||||
合法搭接其他总线设备占用的引脚。捕获数据仅在缓冲冻结(`done`)时全量
|
||||
推送;运行中的推送只带进度 + 各通道实时边沿计数。
|
||||
脚,一个触发:**任意 GPIO** + 上升/下降/任意沿或"立即",客机时间窗口
|
||||
最长 1 小时,自动重新武装)。读数数学、波形绘制器与浮动窗口都在
|
||||
`peris/devices/la-lib.js`;绘制器只有一份实现,弹窗、详情迷你预览与行
|
||||
图标共用。采样复用 M4 调度器,与位绑定设备一致(等触发时持 arm 租约,
|
||||
录制期间每次边沿续租细批——`resolution` 配置决定细批:20/100/300 条指
|
||||
令)。电平为驱动 1/0、客机释放引脚时 -1(Z);分析仪从不驱动任何东西,
|
||||
因此声明 `passive`,合法搭接**任意设备**占用的焊盘(触发焊盘独立订阅、
|
||||
无需是分析通道;且仅在固件驱动该焊盘时才可能触发——gpio_state 看不到
|
||||
纯输入焊盘)。捕获数据仅在缓冲冻结(`done`)时全量推送;运行中的推送只
|
||||
带进度 + 各通道实时边沿计数。
|
||||
- **浮动窗口**(`la-lib.makeFloatingWindow`):大界面以可拖动、可调大小、
|
||||
**非模态**弹窗打开——`position: fixed`、无遮罩,下层页面的一切指针事件
|
||||
照常。几何位置钳制在视口内并持久化到 localStorage。此类窗口挂在
|
||||
`document.body`(不在设备行内),其 View 必须实现 `destroy()` 在行销毁
|
||||
时移除——面板在行移除与列表重建时都会调用 `destroy()`。View 内的多引
|
||||
脚选择器通过 `host.pinInfo()` 将被占用 GPIO 置灰(与通用编辑器同一份、
|
||||
按候选设备感知的引脚集合)。
|
||||
脚选择器通过 `host.pinInfo()` 取得有效/已占用引脚集合(passive 探针的
|
||||
网格从不禁用 GPIO,他设备占用的焊盘仅给 shared 提示)。
|
||||
- **Saleae 风格绘图模型**(同在 `la-lib`):可缩放视图 `{t0, spanUs}` 配
|
||||
`viewXform`/`clampView`/`zoomAt`(滚轮以光标为锚点缩放、拖拽平移、双击
|
||||
复位适配),悬停测量(`pulseAround` 找光标所在脉冲,`pwmOf` 按相邻上升
|
||||
|
||||
+11
-5
@@ -2,21 +2,27 @@
|
||||
|
||||
# Roadmap
|
||||
|
||||
## Logic analyzer — shipped in peris 0.10.0
|
||||
## Logic analyzer — shipped in peris 0.10.0, probe freedoms in 0.11.0
|
||||
|
||||
- ✅ `logic-analyzer.js`: multi-channel GPIO waveform capture (up to 8
|
||||
pads, one trigger: channel + edge or Immediate, guest-time window,
|
||||
auto re-arm). Driven levels 1/0, released pads Z, times in guest µs.
|
||||
Rides the M4 scheduler (arm 1000 watch, fine lease per `resolution`
|
||||
preset: 20/100/300); 20 000-edge buffer freezes with an `overflow`
|
||||
flag. Shared logic in `peris/devices/la-lib.js`; `sharedBus` makes it a
|
||||
pure tap on other bus devices' pads.
|
||||
flag. Shared logic in `peris/devices/la-lib.js`.
|
||||
- ✅ 0.11 probe freedoms: `static passive = true` — a probe listens only,
|
||||
claims no pad and may clamp onto ANY in-use GPIO (conflict check and
|
||||
wired-AND skip passive pads; other devices' pickers ignore them; the
|
||||
channel grid never disables a GPIO). Trigger became `trigPin` (any GPIO,
|
||||
independently subscribed, off-channel triggers fine); the window cap
|
||||
lifted to 1 h of guest time (the edge buffer is the natural limit).
|
||||
- ✅ Floating analyzer window: draggable, resizable, non-modal popup
|
||||
(fixed position, no backdrop — the page underneath keeps working);
|
||||
geometry persisted. One `drawWaveform` painter feeds the popup, the
|
||||
details mini-preview and the row icon; channels picked from a GPIO grid
|
||||
(`host.pinInfo()` disables occupied pins). Framework: `ctx.pinLevel`,
|
||||
`ViewHost.pinInfo`, `View.destroy` (row removal/rebuild teardown).
|
||||
(occupied pins shown as shared, never disabled). Framework:
|
||||
`ctx.pinLevel`, `ViewHost.pinInfo`, `View.destroy` (row
|
||||
removal/rebuild teardown).
|
||||
- ✅ Saleae-style plot interactions: wheel zoom anchored at the cursor,
|
||||
drag pan, double-click/F fit; hover measurement shades the pulse and
|
||||
reads level · width · period · frequency · PWM duty; A/B cursors with
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
|
||||
# 路线图
|
||||
|
||||
## 逻辑分析仪——已于 peris 0.10.0 交付
|
||||
## 逻辑分析仪——已于 peris 0.10.0 交付,0.11.0 探针自由度增强
|
||||
|
||||
- ✅ `logic-analyzer.js`:多通道 GPIO 波形捕获(最多 8 个引脚,一个触
|
||||
发:通道 + 边沿或"立即",客机时间窗口,自动重新武装)。驱动电平
|
||||
1/0,释放引脚记 Z,时间为客机 µs。复用 M4 调度器(警戒 1000,细批随
|
||||
`resolution` 预设:20/100/300);2 万条边沿的缓冲区写满带 `overflow`
|
||||
冻结。共享逻辑在 `peris/devices/la-lib.js`;`sharedBus` 使其成为其他
|
||||
总线设备引脚上的纯搭接。
|
||||
冻结。共享逻辑在 `peris/devices/la-lib.js`。
|
||||
- ✅ 0.11 探针自由度:`static passive = true`——探针只听不占,可搭接
|
||||
**任意已占用 GPIO**(冲突检测与线与仲裁均跳过 passive 焊盘;其他设备
|
||||
的选择器忽略它们;通道网格从不禁用 GPIO)。触发改为 `trigPin`(任意
|
||||
GPIO、独立订阅、可在分析通道之外);窗口上限放宽到 1 小时客机时间
|
||||
(边沿缓冲才是自然限制)。
|
||||
- ✅ 浮动分析窗口:可拖动、可调大小、非模态弹窗(fixed 定位、无遮罩——
|
||||
下层页面照常交互);几何位置持久化。一个 `drawWaveform` 绘制器同时供
|
||||
弹窗、详情迷你预览与行图标使用;通道从 GPIO 网格勾选
|
||||
(`host.pinInfo()` 将被占用引脚置灰)。框架:`ctx.pinLevel`、
|
||||
弹窗、详情迷你预览与行图标使用;通道从 GPIO 网格勾选(被占用引脚仅
|
||||
显示 shared 提示、从不禁用)。框架:`ctx.pinLevel`、
|
||||
`ViewHost.pinInfo`、`View.destroy`(行移除/重建时的清理钩子)。
|
||||
- ✅ Saleae 风格绘图交互:滚轮以光标为锚点缩放、拖拽平移、双击/F 复位
|
||||
适配;悬停测量高亮所在脉冲并读出电平 · 脉宽 · 周期 · 频率 · PWM 占空
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
{ "id": "bme-1", "type": "bitbang-i2c-slave", "name": "BME280 sensor (I2C 0x76)", "model": "bme280", "scl": 1, "sda": 2, "address": 118 },
|
||||
{ "id": "ir-bb", "type": "ir-nec", "name": "IR NEC, bit-bang capture (GPIO5)", "pin": 5, "source": "gpio" },
|
||||
{ "id": "ir-rmt", "type": "ir-nec", "name": "IR NEC, RMT capture (ch1)", "pin": 10, "source": "rmt", "channel": 1 },
|
||||
{ "id": "la-1", "type": "logic-analyzer", "name": "I2C bus LA (SCL+SDA)", "pins": [1, 2], "durationMs": 40, "trigCh": -1 }
|
||||
{ "id": "la-1", "type": "logic-analyzer", "name": "I2C bus LA (SCL+SDA)", "pins": [1, 2], "durationMs": 40, "trigPin": -1 }
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "esp-emu-peris",
|
||||
"version": "0.10.3",
|
||||
"version": "0.11.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Simulated peripherals for the esp-emulator browser (wasm) build",
|
||||
|
||||
@@ -43,27 +43,31 @@ const clampNum = (v, dflt, lo, hi) => {
|
||||
|
||||
export class Device {
|
||||
static typeId = 'logic-analyzer';
|
||||
static version = '1.0.0';
|
||||
static version = '1.1.0';
|
||||
static title = 'Logic analyzer';
|
||||
static category = 'instrument';
|
||||
static description = 'Multi-GPIO waveform capture — click the row icon (or the preview) for the draggable analyzer window; trigger, window and resolution configurable.';
|
||||
static defaultConfig = {
|
||||
name: 'LA', pins: [], durationMs: 50, resolution: 'normal', trigCh: -1, trigEdge: 'any',
|
||||
name: 'LA', pins: [], durationMs: 50, resolution: 'normal', trigPin: -1, trigEdge: 'any',
|
||||
};
|
||||
/* A probe taps lines without driving them: sharing pads with other bus
|
||||
* devices (I2C lines, MAX7219 buses, other analyzers) is legal wiring. */
|
||||
static sharedBus = true;
|
||||
/* A probe only listens — it 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. */
|
||||
static passive = true;
|
||||
static pins(cfg) { return parsePins(cfg.pins); }
|
||||
static configSchema = [
|
||||
{ key: 'durationMs', label: 'Window (ms)', type: 'number', min: 1, max: 1000, step: 1 },
|
||||
// window cap = 1 h of guest time; a dense signal hits the edge
|
||||
// buffer long before and freezes flagged `overflow` (the natural
|
||||
// limit — Saleae streams, we capture into a fixed buffer)
|
||||
{ key: 'durationMs', label: 'Window (ms)', type: 'number', min: 1, max: 3600000, step: 1 },
|
||||
{ key: 'resolution', label: 'Resolution', type: 'select', options: [
|
||||
{ value: 'fine', label: 'Fine — tightest pulses (slowest)' },
|
||||
{ value: 'normal', label: 'Normal (default)' },
|
||||
{ value: 'coarse', label: 'Coarse — fastest' },
|
||||
] },
|
||||
{ key: 'name', label: 'Label', type: 'text' },
|
||||
// channels (pins), trigger channel/edge are edited in the analyzer
|
||||
// window (they need the channel picker); they live in defaultConfig
|
||||
// channels (pins), trigger pin/edge are edited in the analyzer
|
||||
// window (they need the pickers); they live in defaultConfig
|
||||
// so normaliseConfig keeps them across config round-trips
|
||||
];
|
||||
|
||||
@@ -71,10 +75,15 @@ export class Device {
|
||||
this.ctx = ctx;
|
||||
this.cfg = cfg;
|
||||
this.pins = parsePins(cfg.pins);
|
||||
this.windowMs = clampNum(cfg.durationMs, 50, 1, 1000);
|
||||
this.windowMs = clampNum(cfg.durationMs, 50, 1, 3600000);
|
||||
this.fine = FINE_BATCHES[cfg.resolution] ?? FINE_BATCHES.normal;
|
||||
const tc = Number(cfg.trigCh);
|
||||
this.trigCh = Number.isInteger(tc) && tc >= 0 && tc < this.pins.length ? tc : -1;
|
||||
// trigger GPIO: any pad (it gets its own subscription), not only an
|
||||
// analysed channel; -1 = Immediate. Note gpio_state() visibility:
|
||||
// a trigger fires only when the FIRMWARE drives the pad (CPU GPIO
|
||||
// writes) — pure-input pads (driven by our button devices) never
|
||||
// toggle the output registers, so they cannot trigger.
|
||||
const tp = Number(cfg.trigPin);
|
||||
this.trigPin = Number.isInteger(tp) && tp >= 0 ? tp : -1;
|
||||
this.trigEdge = ['any', 'rise', 'fall'].includes(cfg.trigEdge) ? cfg.trigEdge : 'any';
|
||||
this.autoRun = false; // session control (Arm button), not config
|
||||
|
||||
@@ -91,7 +100,9 @@ export class Device {
|
||||
this.lastEdgeCycles = -1;
|
||||
this.lastMarkCycles = 0;
|
||||
this.cpus = ctx.cyclesPerUs || 160;
|
||||
if (this.pins.length) ctx.subscribe(this.pins);
|
||||
const watch = this.trigPin >= 0 && !this.pins.includes(this.trigPin)
|
||||
? [...this.pins, this.trigPin] : this.pins;
|
||||
if (watch.length) ctx.subscribe(watch);
|
||||
}
|
||||
|
||||
onEmuReady() {
|
||||
@@ -107,10 +118,10 @@ export class Device {
|
||||
onPinState(pin, s) {
|
||||
const level = s.oe ? (s.out ? 1 : 0) : -1;
|
||||
if (this.status === 'armed') {
|
||||
// trigger watch: the edge must sit on the trigger channel and
|
||||
// trigger watch: the edge must sit on the trigger GPIO and
|
||||
// match the direction (rising arrives as level 1; a release to
|
||||
// Z counts as falling, the open-drain idiom)
|
||||
if (pin !== this.pins[this.trigCh]) return;
|
||||
if (pin !== this.trigPin) return;
|
||||
if (this.trigEdge === 'rise' && level !== 1) return;
|
||||
if (this.trigEdge === 'fall' && level !== 0 && level !== -1) return;
|
||||
this._start(s.cycles, { pin, level });
|
||||
@@ -153,7 +164,7 @@ export class Device {
|
||||
if (this.status === 'run' || this.status === 'armed') return;
|
||||
this.autoRun = !!(params && params.auto);
|
||||
this.overflow = false;
|
||||
if (this.trigCh >= 0 && this.pins.length) {
|
||||
if (this.trigPin >= 0) {
|
||||
this.status = 'armed';
|
||||
this.ctx.armSampling(ARM_BATCH);
|
||||
} else {
|
||||
@@ -253,7 +264,7 @@ export class Device {
|
||||
this.capturedUs = Math.min(this.windowMs * 1000,
|
||||
Math.max(0, (this.ctx.cycles() - this.t0) / this.cpus));
|
||||
if (this.autoRun) {
|
||||
if (this.trigCh >= 0 && this.pins.length) {
|
||||
if (this.trigPin >= 0) {
|
||||
this.status = 'armed'; // wait for the next trigger
|
||||
this.ctx.armSampling(ARM_BATCH);
|
||||
} else {
|
||||
@@ -282,7 +293,7 @@ export class View {
|
||||
this.el = el;
|
||||
this.send = sendCmd;
|
||||
this.host = host;
|
||||
this.cfg = { name: 'LA', pins: [], durationMs: 50, resolution: 'normal', trigCh: -1, trigEdge: 'any' };
|
||||
this.cfg = { name: 'LA', pins: [], durationMs: 50, resolution: 'normal', trigPin: -1, trigEdge: 'any' };
|
||||
this.state = null;
|
||||
this.autoUI = false; // session Auto re-arm checkbox (an Arm control)
|
||||
this.hover = null;
|
||||
@@ -382,9 +393,9 @@ export class View {
|
||||
this.cfg = {
|
||||
name: String(cfg.name ?? 'LA'),
|
||||
pins: parsePins(cfg.pins),
|
||||
durationMs: clampNum(cfg.durationMs, 50, 1, 1000),
|
||||
durationMs: clampNum(cfg.durationMs, 50, 1, 3600000),
|
||||
resolution: FINE_BATCHES[cfg.resolution] ? cfg.resolution : 'normal',
|
||||
trigCh: Number.isInteger(Number(cfg.trigCh)) ? Number(cfg.trigCh) : -1,
|
||||
trigPin: Number.isInteger(Number(cfg.trigPin)) ? Number(cfg.trigPin) : -1,
|
||||
trigEdge: ['any', 'rise', 'fall'].includes(cfg.trigEdge) ? cfg.trigEdge : 'any',
|
||||
};
|
||||
if (this.win) {
|
||||
@@ -433,9 +444,9 @@ export class View {
|
||||
trigLab.className = 'la-lab';
|
||||
trigLab.textContent = 'Trigger';
|
||||
this.trigSel = document.createElement('select');
|
||||
this.trigSel.title = 'Capture starts on this channel\'s edge (Immediate = start at Arm)';
|
||||
this.trigSel.title = 'Capture starts on this GPIO\'s edge — any pad works, not only analysed channels (Immediate = start at Arm). The trigger needs the FIRMWARE to drive the pad; pure-input pads never toggle the output registers';
|
||||
this.trigSel.addEventListener('change', () =>
|
||||
this.host.updateConfig({ trigCh: Number(this.trigSel.value) }));
|
||||
this.host.updateConfig({ trigPin: Number(this.trigSel.value) }));
|
||||
this.edgeSel = document.createElement('select');
|
||||
for (const [v, l] of [['any', 'any edge'], ['rise', 'rising'], ['fall', 'falling']]) {
|
||||
const o = document.createElement('option');
|
||||
@@ -453,10 +464,10 @@ export class View {
|
||||
winLab.textContent = 'Window';
|
||||
this.winInput = document.createElement('input');
|
||||
this.winInput.type = 'number';
|
||||
this.winInput.min = '1'; this.winInput.max = '1000'; this.winInput.step = '1';
|
||||
this.winInput.title = 'Capture window length in guest milliseconds (emulator time is not wall time)';
|
||||
this.winInput.min = '1'; this.winInput.max = '3600000'; this.winInput.step = '1';
|
||||
this.winInput.title = 'Capture window length in guest milliseconds (emulator time is not wall time). Dense signals freeze earlier at the 20 000-edge buffer (shown as buffer full)';
|
||||
this.winInput.addEventListener('change', () =>
|
||||
this.host.updateConfig({ durationMs: clampNum(this.winInput.value, 50, 1, 1000) }));
|
||||
this.host.updateConfig({ durationMs: clampNum(this.winInput.value, 50, 1, 3600000) }));
|
||||
const winMs = document.createElement('span');
|
||||
winMs.className = 'la-lab';
|
||||
winMs.textContent = 'ms';
|
||||
@@ -589,22 +600,26 @@ export class View {
|
||||
const imm = document.createElement('option');
|
||||
imm.value = '-1'; imm.textContent = 'Immediate';
|
||||
this.trigSel.appendChild(imm);
|
||||
for (let i = 0; i < c.pins.length; i++) {
|
||||
// any valid GPIO may trigger — not only analysed channels, and
|
||||
// including pads other devices own (a probe just listens)
|
||||
const valid = this.host?.pinInfo?.()?.valid ?? c.pins.slice().sort((a, b) => a - b);
|
||||
for (const p of valid) {
|
||||
const o = document.createElement('option');
|
||||
o.value = String(i);
|
||||
o.textContent = `GPIO${c.pins[i]}`;
|
||||
o.value = String(p);
|
||||
o.textContent = `GPIO${p}`;
|
||||
this.trigSel.appendChild(o);
|
||||
}
|
||||
const tc = c.trigCh >= 0 && c.trigCh < c.pins.length ? c.trigCh : -1;
|
||||
this.trigSel.value = String(tc);
|
||||
const tp = c.trigPin >= 0 && valid.includes(c.trigPin) ? c.trigPin : -1;
|
||||
this.trigSel.value = String(tp);
|
||||
this.edgeSel.value = c.trigEdge;
|
||||
this.winInput.value = String(c.durationMs);
|
||||
this.resSel.value = c.resolution;
|
||||
this.autoCb.checked = this.autoUI;
|
||||
}
|
||||
|
||||
/* Channel chips from the panel's candidate-aware pin info — pins used by
|
||||
* other non-bus devices are disabled, shared bus pads stay pickable. */
|
||||
/* Channel chips from the panel's pin info. A probe claims nothing, so
|
||||
* NO GPIO is ever disabled — pads other devices own just get a "shared"
|
||||
* hint (clamping a probe next to an LED disturbs nothing). */
|
||||
_renderChans() {
|
||||
if (!this.win) return;
|
||||
const el = this.chansEl;
|
||||
@@ -614,16 +629,13 @@ export class View {
|
||||
const used = info ? info.used : new Set();
|
||||
for (const p of valid) {
|
||||
const on = this.cfg.pins.includes(p);
|
||||
const taken = !on && used.has(p);
|
||||
const shared = !on && used.has(p);
|
||||
const chip = document.createElement('button');
|
||||
chip.type = 'button';
|
||||
chip.className = 'la-chan' + (on ? ' on' : '');
|
||||
chip.className = 'la-chan' + (on ? ' on' : '') + (shared ? ' shared' : '');
|
||||
chip.textContent = String(p);
|
||||
chip.title = taken
|
||||
? `GPIO${p} — used by another device`
|
||||
: `GPIO${p} — click to ${on ? 'remove' : 'add'}`;
|
||||
if (taken) chip.disabled = true;
|
||||
else chip.addEventListener('click', () => {
|
||||
chip.title = `GPIO${p} — click to ${on ? 'remove' : 'add'}` + (shared ? ' (shared with another device — harmless for a probe)' : '');
|
||||
chip.addEventListener('click', () => {
|
||||
const pins = this.cfg.pins.filter((x) => x !== p);
|
||||
if (!on) {
|
||||
if (pins.length >= MAX_CHANNELS) return;
|
||||
@@ -678,7 +690,7 @@ export class View {
|
||||
|
||||
let text;
|
||||
if (s === 'idle') text = 'idle — Arm to capture';
|
||||
else if (s === 'armed') text = `armed — waiting for trigger: GPIO${this.cfg.pins[this.cfg.trigCh] ?? '?'} ${this.cfg.trigEdge}`;
|
||||
else if (s === 'armed') text = `armed — waiting for trigger: GPIO${this.cfg.trigPin} ${this.cfg.trigEdge}`;
|
||||
else if (s === 'run') text = `capturing… ${fmtUs(st.capturedUs)} / ${fmtUs(st.windowUs)} · ${st.totalEdges} edges`;
|
||||
else text = `done · ${fmtUs(st.capturedUs)} · ${st.totalEdges} edges` + (st.overflow ? ' · buffer full (stopped early)' : '');
|
||||
this.foot.textContent = text
|
||||
@@ -849,13 +861,16 @@ export class View {
|
||||
}, 220);
|
||||
}
|
||||
|
||||
/** Pan to the previous/next edge of the selected channel (zoom kept). */
|
||||
/** Pan to the previous/next edge of the selected channel (zoom kept).
|
||||
* Without a selection the trigger's channel is used when analysed,
|
||||
* else channel 0. */
|
||||
_jumpEdge(dir) {
|
||||
const st = this.state;
|
||||
if (!st || !st.channels.length) return;
|
||||
let ch = this.selCh;
|
||||
if (ch == null || ch < 0 || ch >= st.channels.length) {
|
||||
ch = this.cfg.trigCh >= 0 && this.cfg.trigCh < st.channels.length ? this.cfg.trigCh : 0;
|
||||
const ti = this.cfg.trigPin >= 0 ? st.channels.indexOf(this.cfg.trigPin) : -1;
|
||||
ch = ti >= 0 ? ti : 0;
|
||||
}
|
||||
const flat = st.edges[ch] ?? [];
|
||||
const times = [];
|
||||
|
||||
+4
-2
@@ -481,9 +481,11 @@
|
||||
border: 1px solid var(--border-strong); border-radius: 4px; cursor: pointer;
|
||||
transition: color 0.1s, border-color 0.1s;
|
||||
}
|
||||
.la-chan:hover:not(:disabled) { border-color: var(--accent); color: var(--text); }
|
||||
.la-chan:hover { border-color: var(--accent); color: var(--text); }
|
||||
.la-chan.on { color: var(--accent-text); border-color: var(--accent); background: var(--accent-soft); }
|
||||
.la-chan:disabled { opacity: 0.35; cursor: not-allowed; text-decoration: line-through; }
|
||||
/* pads owned by another device: pickable (a probe disturbs nothing),
|
||||
just visually marked as shared */
|
||||
.la-chan.shared { border-style: dashed; color: var(--faint); }
|
||||
.la-plot { flex: 1; min-height: 0; position: relative; background: var(--bg);
|
||||
border-bottom: 1px solid var(--border); overflow: hidden;
|
||||
touch-action: none; outline: none; cursor: crosshair; }
|
||||
|
||||
+8
-3
@@ -100,15 +100,18 @@ export class PerisRuntime {
|
||||
|
||||
// Pin conflict check (v1: one device per pin, any direction —
|
||||
// except bus devices that declare sharedBus, which may share
|
||||
// pins with each other: two I2C slaves on one SCL/SDA pair).
|
||||
// pins with each other: two I2C slaves on one SCL/SDA pair.
|
||||
// Passive probes (logic analyzer) only listen: they claim
|
||||
// nothing and may overlap with ANY device).
|
||||
const pins = meta.pins(cfg);
|
||||
for (const p of pins) {
|
||||
if (typeof p !== 'number' || p < 0 || p >= gpioCount(this.chip) || absentPins(this.chip).has(p)) {
|
||||
errors.push(`${id}: pin ${String(p)} is not a valid ${this.chip} GPIO`);
|
||||
continue;
|
||||
}
|
||||
if (meta.passive) continue; // probe pads are not claims
|
||||
const claim = usedPins.get(p);
|
||||
if (claim && !(claim.shared && meta.sharedBus)) {
|
||||
if (claim && !(claim.shared && (meta.sharedBus || meta.passive))) {
|
||||
errors.push(`${id}: pin ${p} already used by ${claim.id}`);
|
||||
continue;
|
||||
}
|
||||
@@ -132,11 +135,13 @@ export class PerisRuntime {
|
||||
|
||||
// Open-drain arbitration map: pins claimed by more than one
|
||||
// sharedBus device (the conflict rule above already rejected
|
||||
// mixed sharing).
|
||||
// mixed sharing). Passive probes never drive an input, so they
|
||||
// stay out of the wired-AND entirely.
|
||||
this.busPins = new Set();
|
||||
this.pulls = new Map();
|
||||
const claims = new Map<number, { shared: boolean }[]>();
|
||||
for (const { meta, cfg, instance } of this.devices.values()) {
|
||||
if (meta.passive) continue;
|
||||
for (const p of meta.pins(cfg)) {
|
||||
if (typeof p !== 'number') continue;
|
||||
const list = claims.get(p) ?? [];
|
||||
|
||||
@@ -112,6 +112,15 @@ export interface DeviceMeta {
|
||||
* Default false: one device per GPIO, either direction.
|
||||
*/
|
||||
readonly sharedBus?: boolean;
|
||||
/**
|
||||
* Passive probes (0.10.1): the device only LISTENS (never drives an
|
||||
* input, never claims a pad) — its pins overlap with ANY other device
|
||||
* legally and occupy nothing (other devices' pin pickers ignore it).
|
||||
* The logic analyzer is the pattern: clamping a probe next to an LED
|
||||
* disturbs nothing. Implies sharedBus-level sharing plus overlap with
|
||||
* non-bus devices.
|
||||
*/
|
||||
readonly passive?: boolean;
|
||||
/** GPIO pins the device claims (conflict detection). Required. */
|
||||
pins(cfg: DeviceConfig): number[];
|
||||
new (ctx: DeviceCtx, cfg: DeviceConfig): DeviceLike;
|
||||
|
||||
+1
-1
@@ -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.10.3';
|
||||
export const PERIS_VERSION = '0.11.0';
|
||||
@@ -148,7 +148,9 @@ export class PerisPanel {
|
||||
/**
|
||||
* Pins claimed by other devices, from the perspective of `candidate`
|
||||
* (the device about to use the set): sharedBus devices ignore each
|
||||
* other's pins — two I2C slaves on one bus is legal wiring.
|
||||
* other's pins — two I2C slaves on one bus is legal wiring. Passive
|
||||
* probes (logic analyzer) claim nothing: their pads never show as
|
||||
* "in use" for anyone.
|
||||
*/
|
||||
private usedPins(exceptId: string, candidate?: DeviceMeta | null): Set<number> {
|
||||
const used = new Set<number>();
|
||||
@@ -156,6 +158,7 @@ export class PerisPanel {
|
||||
if (d.id === exceptId) continue;
|
||||
const meta = this.registry?.meta(d.type);
|
||||
if (!meta) continue;
|
||||
if (meta.passive) continue;
|
||||
if (candidate?.sharedBus && meta.sharedBus) continue;
|
||||
for (const p of meta.pins(d)) used.add(p);
|
||||
}
|
||||
|
||||
@@ -862,6 +862,33 @@ test('runtime: sharedBus devices share pins; non-bus devices still conflict', as
|
||||
assert.equal(rt.has('led'), true);
|
||||
});
|
||||
|
||||
test('runtime: passive probes overlap ANY device without a conflict', async () => {
|
||||
const rt = new PerisRuntime();
|
||||
const msgs = [];
|
||||
rt.post = (m) => msgs.push(m);
|
||||
rt.registry = {
|
||||
has: (t) => ['led', 'i2c', 'la'].includes(t),
|
||||
meta: (t) => (t === 'led' ? ledMod.Device : t === 'i2c' ? i2c.Device : la.Device),
|
||||
View: () => null,
|
||||
types: () => [],
|
||||
errors: [],
|
||||
};
|
||||
// a probe clamped onto a plain LED pad, an I2C pair AND another probe
|
||||
msgs.length = 0;
|
||||
await rt.applyConfig({
|
||||
perisVersion: 1,
|
||||
devices: [
|
||||
{ id: 'led', type: 'led', pin: 8 },
|
||||
{ id: 'ee', type: 'i2c', scl: 1, sda: 2 },
|
||||
{ id: 'la1', type: 'la', pins: [8, 1, 2] },
|
||||
{ id: 'la2', type: 'la', pins: [8] },
|
||||
],
|
||||
});
|
||||
const ready = msgs.find((m) => m.type === 'peris_ready');
|
||||
assert.equal(ready.ok, true, ready.errors.join('; '));
|
||||
assert.equal(rt.has('la1') && rt.has('la2') && rt.has('led'), true);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Logic analyzer (0.10.0): la-lib readout math + the capture state machine
|
||||
|
||||
@@ -908,11 +935,11 @@ test('la-lib: axis ticks, formatting, fit span and the pixel transform', () => {
|
||||
assert.ok(Math.abs(xToUs(usToX(123)) - 123) < 1e-9);
|
||||
});
|
||||
|
||||
test('logic-analyzer: meta validation, pin claim, sharedBus probe semantics', () => {
|
||||
test('logic-analyzer: meta validation, pin claim, passive probe semantics', () => {
|
||||
assert.equal(validateDeviceModule(la), null);
|
||||
assert.deepEqual(la.Device.pins({ pins: [1, 2] }), [1, 2]);
|
||||
assert.deepEqual(la.Device.pins({ pins: [] }), []);
|
||||
assert.equal(la.Device.sharedBus, true, 'a probe taps lines other bus devices drive');
|
||||
assert.equal(la.Device.passive, true, 'a probe only listens — it claims no pad');
|
||||
});
|
||||
|
||||
test('logic-analyzer: immediate capture records edges, freezes at window end', () => {
|
||||
@@ -960,17 +987,17 @@ test('logic-analyzer: immediate capture records edges, freezes at window end', (
|
||||
assert.equal(leases.released >= 2, true);
|
||||
});
|
||||
|
||||
test('logic-analyzer: channel trigger — direction, wrong channel, t0 at the edge', () => {
|
||||
test('logic-analyzer: GPIO trigger — direction, other pads, t0 at the edge', () => {
|
||||
const { ctx, leases } = makeDevCtx({ levels: { 4: false, 5: false } });
|
||||
const dev = new la.Device(ctx, {
|
||||
id: 'la', type: 'logic-analyzer', pins: [4, 5], trigCh: 0, trigEdge: 'rise', durationMs: 5,
|
||||
id: 'la', type: 'logic-analyzer', pins: [4, 5], trigPin: 4, trigEdge: 'rise', durationMs: 5,
|
||||
});
|
||||
dev.onCommand('arm');
|
||||
assert.equal(dev.getState().status, 'armed');
|
||||
assert.deepEqual(leases.armed, [la.ARM_BATCH], 'standing watch while waiting');
|
||||
|
||||
dev.onPinState(4, { out: false, oe: true, cycles: 1000 }); // falling: ignored
|
||||
dev.onPinState(5, { out: true, oe: true, cycles: 1500 }); // wrong channel
|
||||
dev.onPinState(5, { out: true, oe: true, cycles: 1500 }); // not the trigger pad
|
||||
assert.equal(dev.getState().status, 'armed');
|
||||
|
||||
dev.onPinState(4, { out: true, oe: true, cycles: 16000 }); // rising on GPIO4
|
||||
@@ -989,9 +1016,39 @@ test('logic-analyzer: channel trigger — direction, wrong channel, t0 at the ed
|
||||
dev.destroy();
|
||||
});
|
||||
|
||||
test('logic-analyzer: trigger GPIO may sit outside the analysed channels', () => {
|
||||
const { ctx } = makeDevCtx({ levels: { 5: false } });
|
||||
const dev = new la.Device(ctx, { id: 'la', type: 'logic-analyzer', pins: [5], trigPin: 4, trigEdge: 'rise' });
|
||||
dev.onCommand('arm');
|
||||
assert.equal(dev.getState().status, 'armed');
|
||||
dev.onPinState(5, { out: true, oe: true, cycles: 1000 }); // channel edge ≠ trigger
|
||||
assert.equal(dev.getState().status, 'armed');
|
||||
dev.onPinState(4, { out: true, oe: true, cycles: 2000 }); // off-channel trigger fires
|
||||
let st = dev.getState();
|
||||
assert.equal(st.status, 'run');
|
||||
assert.equal(st.triggered, true);
|
||||
assert.equal(st.channels.join(), '5', 'only the analysed channel is recorded');
|
||||
ctx.setCycles(2000 + 50 * 1000 * 160 + 1); // default 50 ms window
|
||||
dev.onBatch();
|
||||
st = dev.getState();
|
||||
assert.equal(st.status, 'done');
|
||||
assert.deepEqual(st.edges[0], [], 'the off-channel trigger edge lands in no lane');
|
||||
assert.deepEqual(st.initial, [0], 'channel levels still snapshot via pinLevel');
|
||||
dev.destroy();
|
||||
});
|
||||
|
||||
test('logic-analyzer: window clamps up to 1 h of guest time', () => {
|
||||
const { ctx } = makeDevCtx();
|
||||
const dev = new la.Device(ctx, { id: 'la', type: 'logic-analyzer', pins: [1], durationMs: 999999999 });
|
||||
assert.equal(dev.windowMs, 3600000, 'huge windows clamp at 1 h, not 1 s');
|
||||
dev.onCommand('arm');
|
||||
assert.equal(dev.getState().windowUs, 3600000 * 1000);
|
||||
dev.destroy();
|
||||
});
|
||||
|
||||
test('logic-analyzer: falling trigger counts a release-to-Z as an edge', () => {
|
||||
const { ctx } = makeDevCtx({ levels: { 7: true } });
|
||||
const dev = new la.Device(ctx, { id: 'la', type: 'logic-analyzer', pins: [7], trigCh: 0, trigEdge: 'fall' });
|
||||
const dev = new la.Device(ctx, { id: 'la', type: 'logic-analyzer', pins: [7], trigPin: 7, trigEdge: 'fall' });
|
||||
dev.onCommand('arm');
|
||||
dev.onPinState(7, { out: true, oe: true, cycles: 100 });
|
||||
assert.equal(dev.getState().status, 'armed', 'rising ignored');
|
||||
@@ -1026,7 +1083,7 @@ test('logic-analyzer: stop / clear / auto re-arm / auto+immediate warns', () =>
|
||||
// stop while merely armed disarms without touching data
|
||||
{
|
||||
const { ctx } = makeDevCtx();
|
||||
const dev = new la.Device(ctx, { id: 'la', type: 'logic-analyzer', pins: [2], trigCh: 0 });
|
||||
const dev = new la.Device(ctx, { id: 'la', type: 'logic-analyzer', pins: [2], trigPin: 2 });
|
||||
dev.onCommand('arm');
|
||||
assert.equal(dev.getState().status, 'armed');
|
||||
dev.onCommand('stop');
|
||||
@@ -1036,7 +1093,7 @@ test('logic-analyzer: stop / clear / auto re-arm / auto+immediate warns', () =>
|
||||
// auto re-arm with a trigger: after done → armed again
|
||||
{
|
||||
const { ctx, leases } = makeDevCtx({ levels: { 3: false } });
|
||||
const dev = new la.Device(ctx, { id: 'la', type: 'logic-analyzer', pins: [3], trigCh: 0, durationMs: 1 });
|
||||
const dev = new la.Device(ctx, { id: 'la', type: 'logic-analyzer', pins: [3], trigPin: 3, durationMs: 1 });
|
||||
dev.onCommand('arm', { auto: true });
|
||||
dev.onPinState(3, { out: true, oe: true, cycles: 1000 });
|
||||
ctx.setCycles(1000 + 1000 * 160 + 1);
|
||||
|
||||
Reference in new issue
Block a user