mirror of
https://github.com/ruvnet/RuView
synced 2026-08-07 20:01:43 +00:00
9744d367a2
The hardware side of ruview-gamma: an ESP32 driving an LED + audio flicker at
a commanded 36-44 Hz envelope with a hardware emergency stop. This is the
actuator the hil::verify_hil contract grades.
Split for testability:
- main/stim_core.{h,c}: pure C safety core, no ESP-IDF deps. Envelope
validation mirroring SafetyEnvelope::conservative(), a latched
START/STOP/e-stop state machine (a session can never silently resume after
an e-stop), exact integer timing in millihertz (the +/-0.1 Hz HIL target is
exact: 40.0 Hz = 40000 mHz -> 12500 us half-period), and a fail-closed line
parser. 15 host tests pass under gcc, no hardware needed.
- main/main.c: ESP-IDF binding. GPTimer ISR at 1 MHz crystal ticks, LEDC PWM
for LED (19.5 kHz carrier) and audio tone, sync-out GPIO for logic-analyzer
capture, e-stop GPIO ISR that turns outputs off in the ISR (microseconds,
vs the 100 ms HIL budget) then latches, USB-CDC line console.
Defense in depth: the device re-enforces the safety envelope independently of
the Rust host, so a buggy or compromised host cannot command an
out-of-envelope output. Emits a canonical integer SESSION {...} record per run
for witness-hash reproduction (HIL 100% reproducibility target).
Kconfig pins, 4 MB single-app, radio-off deterministic actuator profile.
Maps 1:1 to the five hil::verify_hil targets.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
85 lines
3.8 KiB
Markdown
85 lines
3.8 KiB
Markdown
# esp32-gamma-stim — ESP32 gamma stimulation actuator (ADR-250 §21 M2)
|
||
|
||
The **device harness** for `ruview-gamma`: an ESP32 that drives a light + sound
|
||
flicker at a commanded frequency, gated by a hardware emergency stop, with a
|
||
compiled-in safety envelope that mirrors `SafetyEnvelope::conservative()` in the
|
||
Rust crate. This is the actuator the `hil::verify_hil` contract grades.
|
||
|
||
> **Not a medical device.** Research/engineering harness. The host
|
||
> (`ruview-gamma`) decides *what* to play and never claims a therapeutic effect;
|
||
> this firmware only plays it safely and reports exactly what it did.
|
||
|
||
## Design: safety core vs hardware binding
|
||
|
||
| File | Role | Tested |
|
||
|------|------|--------|
|
||
| `main/stim_core.{h,c}` | Pure C safety core: envelope validation, START/STOP/e-stop **latched** state machine, exact integer timing math, line protocol parser. No ESP-IDF deps. | `tests/test_stim_core.c` on the host (gcc), 15 tests |
|
||
| `main/main.c` | ESP-IDF binding: GPTimer ISR, LEDC PWM (LED + audio), sync GPIO, e-stop ISR, USB-CDC console. Only moves registers. | on hardware (HIL) |
|
||
|
||
Every safety decision lives in the host-tested core — **defense in depth**: the
|
||
Rust host gates the stimulus *and* the device gates it again independently, so a
|
||
buggy or compromised host still cannot command an out-of-envelope output.
|
||
|
||
## Run the safety-core tests (no hardware, no ESP-IDF)
|
||
|
||
```bash
|
||
cd firmware/esp32-gamma-stim
|
||
gcc -Wall -Wextra -Werror -O2 -I main tests/test_stim_core.c main/stim_core.c -o /tmp/test_stim && /tmp/test_stim
|
||
# -> all 15 stim_core tests passed
|
||
```
|
||
|
||
## Build & flash (ESP-IDF v5.2+)
|
||
|
||
```bash
|
||
idf.py set-target esp32s3 # or esp32c6
|
||
idf.py menuconfig # Gamma Stimulation -> pins, tone freq
|
||
idf.py build flash monitor
|
||
```
|
||
|
||
Default pins (Kconfig-overridable): LED GPIO 4, audio GPIO 5, sync-out GPIO 6,
|
||
e-stop button GPIO 7 (to GND, active-low).
|
||
|
||
## Host protocol (line-based, 115200, USB-CDC/UART0)
|
||
|
||
```
|
||
START <freq_mhz> <brightness_pct> <volume_pct> <duration_s>
|
||
STOP
|
||
STATUS
|
||
UNLOCK # clear a latched e-stop
|
||
VERSION
|
||
```
|
||
|
||
Frequency is **millihertz** (40.0 Hz = `40000`) so the ±0.1 Hz HIL target is
|
||
exact integer math (±100 mHz). Example — 40.0 Hz, 30% brightness, 28% volume,
|
||
10 min:
|
||
|
||
```
|
||
> START 40000 30 28 600
|
||
OK start seq=1 half_period_us=12500
|
||
... (session runs) ...
|
||
SESSION {"seq":1,"freq_mhz":40000,"brightness_pct":30,"volume_pct":28,"duration_s":600,"half_periods":48000,"stop":"completed","fw":"0.1.0"}
|
||
```
|
||
|
||
The `SESSION {...}` line is canonical (quantized integers, fixed field order) so
|
||
the host pairs it with the RuFlo session builder to reproduce the witness hash
|
||
(HIL: 100% hash reproducibility).
|
||
|
||
## How it maps to the HIL targets (`v2/crates/ruview-gamma/src/hil.rs`)
|
||
|
||
| HIL target | How this firmware meets it |
|
||
|------------|----------------------------|
|
||
| LED frequency ±0.1 Hz | GPTimer at 1 MHz crystal-derived ticks; half-period from exact integer division; worst-case truncation at 44 Hz is ~3 mHz (35× inside budget) |
|
||
| A/V sync < 5 ms | LED and audio duty written in the **same ISR**; skew is a few register writes |
|
||
| Stop → actuator off < 100 ms | e-stop GPIO ISR turns outputs off **in the ISR** before latching — microseconds |
|
||
| Session-hash reproducibility 100% | canonical integer `SESSION {...}` record, no float formatting |
|
||
| EEG lift ≥ 20% vs fixed 40 Hz | provided by the host's adaptive optimizer choosing the frequency this firmware plays |
|
||
|
||
## Hardware notes
|
||
|
||
- **Drive the LED through a MOSFET/constant-current driver**, not the GPIO
|
||
directly. Keep brightness within eye-safe flicker limits — the firmware caps
|
||
duty at the envelope's 40%, but the optical design owns absolute luminance.
|
||
- **Photosensitivity/epilepsy is a hard exclusion** at the host
|
||
(`ExclusionScreen`); the device is the last line, not the only line.
|
||
- The e-stop button is mandatory for any human-facing bench run.
|