# Tangible Choir review and development testbed

[Vault home](README.md) · [Mega shield](Hardware/Mega%20shield.md) · [ESP32-S3](Hardware/ESP32-S3.md) · [Testbed](Software/Development%20testbed.md)

Reviewed 2026-10-12. This is a review of the files present during the session,
including [ARCHITECTURE.md](ARCHITECTURE.md), the annotated
[wiring export](Hardware/wiring/wiring_bb_2.png), and the electrical
connections stored in [wiring.fzz](Hardware/wiring/wiring.fzz). Firmware, production patches, audio assets,
and Vibe's architecture document were left unchanged. Source fingerprints and
the actual automated results are in [TEST_RESULTS.md](TEST_RESULTS.md).

Additional project communications, shared media, and design references are
indexed in [SOURCES.md](SOURCES.md). The Slack export, Drive folder, and Figma
board were registered after this review baseline; their contents have not yet
been reconciled with these findings.

**Recommendation: keep the Raspberry Pi + Mega arrangement for the soldered
shield revision.** It already provides the needed ten analog inputs and eight
PWM outputs. First agree the pin map and correct the power/driver wiring; then
build a connector-based shield. An ESP32-S3 can replace the I/O controller, but
requires voltage conversion, board-specific pins, and ADC/PWM/USB adjustments.
It does not replace the Pi's current Pure Data audio system by uploading this
Arduino sketch. The Mega's capabilities are documented by
[Arduino](https://docs.arduino.cc/hardware/mega-2560).

## Findings to resolve first

Priority P1 means resolve before relying on the next hardware/software revision.
P2 means a reproducibility, maintenance, or playback-quality issue. Electrical
findings describe the supplied diagram/model; the actual assembly may differ.

| ID | Priority | Finding | Evidence |
| --- | --- | --- | --- |
| R1 | P1 | Latest diagram and firmware disagree about sensor, pot, and strip pins | Actual Fritzing connector nets versus `choir/choir.ino:3,12–16`; failing wiring tests |
| R2 | P1 | Pot R5/R6 ground ends are connected only to each other | Fritzing wire graph; both pot-supply subtests fail |
| R3 | P1 | Strip power and missing data protection need resolving | Strip VDD is on Mega 5 V; DIN goes directly to D23; firmware requests near-full white on 50 pixels |
| R4 | P1 | The sketch folder does not compile as committed | Real Arduino CLI Mega build fails on duplicate definitions in the two `.ino` files |
| R5 | P1 | Closing a door leaves rod LEDs and strip at their last values | `choir/choir.ino:63–68,89–98`; both host behavior tests fail for either door |
| R6 | P1 | Pd can briefly report both doors open when only one is open in either frame | Real Pd control-graph test of `(1,0)` → `(0,1)` emits `doors=1`, then `0` |
| R7 | P1 | Pd starts playback before loading the selected song | `main.pd:126–128`; real Pd event-order test shows start events before Tuljak filename loads |
| R8 | P2 | Host-to-firmware commands are disconnected/incomplete | `arduino.pd:36–42,78–80`; no receiver for `$0-comctl`; main sketch never reads commands |
| R9 | P2 | Serial startup/framing/validation is fragile | `arduino.pd:6–15,35,43,49–50,81`; numbered port, CR-specific framing, no field-count/range/staleness validation |
| R10 | P2 | Audio completion, stereo loading, gain, and asset formats need explicit contracts | `main.pd:44,78,147–148`; `soundfile.pd:6,18,21–27`; measured asset metadata |
| R11 | P2 | Logging and deployment are not established by the current root patch | `logs.pd` is not instantiated by `main.pd`; README/external versions/startup settings incomplete |

### R1: Firmware and Fritzing use different inputs

These are electrical connections decoded from the archive, with connector names
from the official [Fritzing part definitions](https://github.com/fritzing/fritzing-parts).
They are not guesses based on wire colors or crossings in the PNG.

| Fritzing component | Diagram's Mega pin | Firmware interpretation of that pin |
| --- | --- | --- |
| J1 infrared-sensor Vout | A8 | Rod 4 potentiometer |
| J2 infrared-sensor Vout | A9 | Rod 5 potentiometer |
| R1 wiper, top pot | A5 | Rod 7 potentiometer |
| R2 wiper | A4 | Rod 3 potentiometer |
| R3 wiper | A3 | Rod 8 potentiometer |
| R4 wiper | A2 | Rod 1 potentiometer |
| R5 wiper | A7 | Rod 2 potentiometer |
| R6 wiper | A6 | Rod 6 potentiometer |
| R7 wiper | A1 | Door 2 sensor |
| R8 wiper, bottom pot | A0 | Door 1 sensor |
| NeoPixel DIN | D23 | Firmware actually transmits on D6 |
| Q1 base through R9, 470 Ω | D13 | Rod 8 PWM output |

If the physical build follows this drawing, the two bottom pots gate the doors
and the door sensors become volume controls. The strip receives no intended
data because the firmware drives a different pin. Shuffling LED pins alone will
not fix these differences. The physical order of rods/voices also needs labels;
matching a set of ADC pins does not establish channel order.

For the new shield, the proposed map below preserves the current firmware and
Pd channel order. Alternatively, keep the diagram's physical wiring and change
the firmware map deliberately, then re-run the contract tests. D23 itself can
carry NeoPixel data; the problem is disagreement with D6, not a PWM requirement.

### R2: Two potentiometers lack a ground connection in the model

R5 and R6 `connector2` endpoints share one isolated net. That net has no Mega
ground, breadboard-ground endpoint, or connection to the other pots. Their
other endpoints share +5 V. A floating divider end can make a wiper appear
stuck high or respond unpredictably. Connect this pair to the common ground
net in the diagram and verify the real harness with a meter.

The chosen digital breadboard part has continuous power rails in its part
definition. The annotation says the **actual board has split rails**. The
bottom W↔Y and X↔Z jumpers join opposite-side rails; they do not bridge each
rail's middle break. Wire1 connects a terminal-strip row to ground, not the two
halves of a power rail. Check continuity from the supply to every used segment.
This distinction remains relevant while repairing the breadboard, even if the
new shield will use continuous buses.

### R3: Resolve the strip and rod-driver power paths

The model supplies NeoPixel VDD through the breadboard from the Mega 5 V pin.
`setBrightness(250)` is about 98% of the 0–255 range; the loop sets all 50 RGB
pixels white. A conservative older-RGB-pixel budget is **50 × 60 mA ≈ 3 A** for
the strip alone. Actual consumption depends on the installed pixel type and
brightness; measure it and size the supply, wiring, and connector accordingly.
See [Adafruit's power discussion](https://learn.adafruit.com/sipping-power-with-neopixels?view=all).

Route a suitable regulated 5 V lighting supply directly to the strip, with a
common ground to the controller. Keep the lighting-current return separate
from the sensor-ground route until they meet at the supply/distribution point.
The simplest initial arrangement is USB power for the Mega and a separate
lighting supply, with their grounds joined and their positive rails separate.
Plan any single-supply arrangement explicitly so external +5 V does not feed
back into USB. Arduino documents the limitations of supplying its rails on the
[Mega product page](https://store.arduino.cc/products/arduino-mega-2560-rev3).

Add a 330–470 Ω resistor in the strip DIN path near the first pixel. The drawing's
R9 is a **transistor-base resistor on D13**, not a NeoPixel data resistor. The
annotated capacitor is not included as a connected component in `wiring.fzz`;
confirm its capacitance, voltage rating, placement, and polarity physically.
A 500–1000 µF capacitor across the strip supply and short, secure wiring follow
[Adafruit's guidance](https://learn.adafruit.com/adafruit-neopixel-uberguide/best-practices).

The diagram illustrates one rod-light driver: Q1 emitter to ground, collector
through R10 (47 Ω) to LED1, LED anode to +5 V, and base through R9 to D13. It does
not model eight complete rod-light circuits. A white LED with approximately
3.0 V forward voltage and a saturated transistor at 0.2 V would draw roughly
`(5 − 3 − 0.2) / 47 ≈ 38 mA`; this is an example calculation, not a measured
rating. Confirm whether each rod has one LED, parallel LEDs, or another load.
Use one current-limiting path per parallel LED branch. Choose transistor or
MOSFET drivers and resistor values for the measured load; a transistor alone
does not limit LED current.

For direct GPIO LEDs use the recommended operating current and chip/group
limits, rather than designing around absolute maximum ratings. Arduino's
[Mega pinout](https://docs.arduino.cc/resources/pinouts/A000067-full-pinout.pdf)
specifies 20 mA maximum per I/O pin. For LED groups, keep the load current in the
lighting supply/driver, not the GPIO. Provide a base/gate resistor and an
appropriate pull-down so each driver is off during reset.

### R4–R5: Build layout and door-close behavior

Arduino combines the `.ino` files in a sketch directory during preprocessing;
an older variant is not ignored because its name differs. Both present files
declare the same objects/constants, `setup()`, and `loop()`. The actual folder
build failed; the actual **isolated `choir.ino` build passed** with AVR core
1.8.8 and NeoPixel 1.15.5. Move the variant into its own properly named sketch
directory or archive it outside the source directory. See
[Arduino's build process](https://docs.arduino.cc/arduino-cli/sketch-build-process/).

The main sketch updates the rod LEDs and pixel buffer only inside `if (doorsOpen)`.
Once open, closing either door does not clear those outputs. The probe reproduced
eight PWM values of 255 and 50 lit pixels after closing either door. Explicitly
write zero PWM and clear/show the strip on the closed transition. Define whether
the strip should follow doors, song playback, or both, then test that policy.

### R6–R7: Make door decisions and playback transitions explicit

The ten-field unpack updates its outlets individually. The main patch combines
door values immediately, mixing a newly updated door with the previous value
of the other door. The headless test reproduced a false open/close pulse on
`(left=1,right=0)` → `(left=0,right=1)`. Decode and validate one complete frame,
store both door values, and calculate their conjunction once per frame before
emitting any door transition. Add hysteresis/filtering if sensor values wobble.

The firmware treats raw ADC 200 as open, while Pd uses `raw/1024 > 0.2`, making
205 the first open value. Readings 200–204 therefore disagree. Use the same
calibrated rule at both ends, or publish an explicit door-state field if the
controller is to own that decision.

On an open transition, `change` sends to `send doors` before its song-selection
branch. The sound players receive the opening event and start before the
filename cascade selects/loads the new song. The real Pd scheduler confirmed
this order with control probes. Actual audible consequences depend on array
replacement and completion scheduling, but the ordering fault is established.
Sequence **select song → load/preload assets → confirm ready → start all voices**.
Prefer preloading both songs or loading while stopped to avoid disk/memory work
on the audio transition. A regression test should assert this sequence.

### R8–R9: Serial is currently a telemetry link

`print len 68`, `print len 80`, and `print fin` feed `s $0-comctl`. No matching
receiver forwards them to `comport`, so the command-path test fails. The main
sketch never reads serial input anyway. The older variant extracts `len`'s
argument into a temporary value it never uses, then calls `toInt()` on the
whole command. `fin` only calls `show()` without clearing pixels. Its debug
text would also pollute the numeric telemetry stream if that version were used.
Either finish the command protocol with framing and acknowledgements, or
document/remove the dormant path so playback state is not assumed to control
the firmware.

`Serial.println()` emits **CRLF**, and the firmware prints a trailing space
after the tenth number. Pd's `sel 13 10` receives the serial bytes: CR flushes
the group, LF is discarded, and other bytes accumulate. These values mean
carriage return and line feed; they do not detect USB disconnects. LF-only
lines do not flush a frame in this parser. Add tests for partial and malformed
frames, maximum line length, exact field count, range, and a stale-data timeout.

Startup sends `open 1` from loadbang even though the object is `comport 2 9600`.
The macOS `devicename /dev/tty.usbmodem21201` message is a manual alternate,
not a Pi device path or the automatic startup selection. Resolve the real Pi
device, preferably via a persistent identity such as `/dev/serial/by-id/...`
where available, and pin the reconnect policy.

The worst telemetry line is 52 bytes: ten `1023 ` fields plus CRLF. At a desired
20 Hz that is **1040 bytes/s**. 9600 baud with 8N1 carries about **960 bytes/s**.
`delay(50)` also comes after reads, LED updates, and possible serial blocking,
so it does not establish a precise 20 Hz rate. Use a scheduled update period
and, for example, 115200 baud on a hardware UART with matching Pd settings;
verify USB CDC behavior separately on the S3.

### R10–R11: Audio, logging, and deployment follow-up

- `main.pd` has **eight shared choir players plus one rumble player**. The songs
  reuse the choir players; they do not each have eight dedicated instances.
- All inspected audio assets are stereo, but `soundfiler` loads into only
  `$0-array1`. `$0-array2` is declared but never loaded or played. Decide whether
  stems should deliberately use the first channel, be downmixed, or retain
  stereo. The unused array is approximately 19 MiB per player, roughly 171 MiB
  across nine instances before counting the working arrays.
- `tabplay~` uses the engine's sample clock; it does not automatically resample
  a table based on the source WAV's rate. Selected choir/rumble files are 44.1 kHz.
  Fix/document the Pd engine rate or implement resampling. The fixed
  `2.26757e-05` duration multiplier also assumes 44.1 kHz.
- Ohjad's selected files last roughly **67.93–68.36 s**; Tuljak's selected files
  last **77.30–81.68 s**. Rod 1's player supplies the current completion signal;
  it is shorter than some other voices in both songs. Equal-length stems are
  a proposed synchronization contract, so the asset test flags this difference.
  Alternatively define a master duration/end policy and test it explicitly.
  Check musical alignment by listening; duration alone cannot establish it.
- A completion bang also triggers `tabplay~ array1` in `main.pd`, although no
  global `array1` exists there. This is an old unbound playback branch.
- Rumble is inverted by `is_rumble` XOR, so it is intended for the closed-door
  phase. Check initial closed-door startup and end-of-file behavior; there is
  no explicit looping path. Its current gain is 1.5.
- Voice volumes go directly into `*~` without a ramp. Add a short `line~`/`vline~`
  fade for pot changes and stop/start transitions, then render/listen for clicks.
  Players sum directly at `dac~`; add a shared mix/master gain and check peaks
  with all rods active before choosing headroom or limiting.
- `cheering1.wav`, `cheering2.wav`, and `cheering3.wav` have ID3/MPEG-looking
  headers rather than RIFF/WAVE. They are not referenced by the current root
  patch; convert them to genuine PCM WAV before integrating those effects.
- `logs.pd` will only run if opened or instantiated; `main.pd` does neither.
  Its contents show an event logger, not evidence that deployment logs exist.
- Pin the Pd versus plugdata choice, externals/versions/search paths, sample
  rate, audio output device, DSP-on behavior, startup command/service, Git LFS
  retrieval, and serial permissions/device identity. [Pi setup history](Software/Pi%20setup%20history.md) is useful
  history, but includes failed attempts and is not a reproducible installer.

## Validation of ARCHITECTURE.md

The document captures the broad Pi/audio and Mega/I/O responsibilities and the
firmware's actual arrays. Treat its breadboard/shield description as a draft.
These specific claims need correction before it becomes a build reference:

| Architecture reference | Validation/correction |
| --- | --- |
| Lines 5, 193, 211–230, 295, 357–360, 382 | The drawn J1/J2 part is **Infrared Proximity Sensor**, with Vout/GND/Vcc. A magnetic-contact circuit and magnet polarity are unverified assumptions. Identify actual sensors before choosing bias or switching logic. |
| Lines 186, 293, 383 | The Fritzing pots specify **100 kΩ**, not 10 kΩ. Verify real markings. High source impedance can make multiplexed ADC settling/noise more important; filtering and an extra settling read may be needed. |
| Lines 106, 230 | Closing after opening leaves lights on; the missing closed-door output branch is reproducible. |
| Lines 117–123 | Actual parse order: `comport → sel 13 10 → zl group → iem_alisttosym → fromsymbol → unpack → /1024`. CR/LF selectors handle delimiters. |
| Lines 126, 129 | Automatic startup is `open 1`; macOS devicename is a manual message. `$0-comctl` has no receiver, so commands do not reach `comport`. |
| Lines 145, 154 | Eight choir instances are shared between songs, plus one rumble instance. |
| Lines 204, 251–256, 284 | R9 470 Ω feeds **Q1 base from D13**. NeoPixel DIN is wired directly to **D23**. |
| Lines 195, 258 | The bundled pixel part is **Quarter 60 NeoPixel Ring**, a segment model. The firmware's 50 is a configured count, not verification of the installed strip. |
| Lines 206–207, 218, 283, 285 | R11/R13 10 kΩ are **pull-ups to +5 V on sensor outputs**. R12/R14 150 Ω are **in series with sensor Vcc**. These are not generic rod LED resistors or sensor pull-downs. Validate the sensor supply drop against its datasheet. |
| Lines 244–246 | R5/R6 ground ends form an isolated net. Pots use A0–A7 in this drawing; sensors use A8/A9. Eight rod LED circuits are not shown. |
| Lines 260–262, 433 | LED1/Q1 are connected: D13 → R9 → base; emitter to ground; collector through R10 to LED1; LED anode to +5 V. D13 is used by the firmware. |
| Lines 269–271 | Terminal-row-to-ground and opposite-side rail jumpers do not bridge the middle breaks of the physical board. |
| Lines 379–384 | The high-current strip observation is sound. The capacitor buffers transients; it does not replace a sufficient supply. 40 mA absolute maximum is not a design target; current limits are chip/group specific. |
| Lines 416, 423 | Logging is not instantiated by `main.pd`. Worst-case 20 Hz telemetry is 1040 bytes/s, not approximately 200 bytes/s. |

R15–R18 are loose resistor examples in the drawing, with no electrical
connections. Do not count their presence as completed duplicate circuits.
The physical sensor model, LED groups, pixel count/type, shield model, and
ESP32-S3 board model remain unconfirmed.

## Proposed Mega shield revision

Use a Mega-compatible prototyping shield exposing A8/A9 as well as the needed
PWM pins. Some Uno-sized prototyping shields do not provide all those headers.
The shield should distribute signals and house drivers/connectors, while the
Pi continues audio playback over USB serial. Keep the Mega removable.

This is a **proposed map preserving the current code**, not a claim about the
existing physical harness:

| Shield connector / channel | ADC/input pin | PWM/output pin | Current Pd voice, Ohjad / Tuljak |
| --- | --- | --- | --- |
| Door left | A0 | — | Door field 1 |
| Door right | A1 | — | Door field 2 |
| Rod 1 | A2 | D5 | Baritone / bass |
| Rod 2 | A7 | D4 | Alto 1 / alto |
| Rod 3 | A4 | D3 | Bass / men 1 |
| Rod 4 | A8 | D11 | Alto 2 / soprano |
| Rod 5 | A9 | D2 | Soprano 2 / women 1 |
| Rod 6 | A6 | D10 | Tenor 1 / men 2 |
| Rod 7 | A5 | D12 | Soprano 1 / women 2 |
| Rod 8 | A3 | D13 | Tenor 2 / tenor |
| NeoPixel strip | — | D6, data | Confirm actual pixel count; firmware assumes 50 |

All eight listed LED pins support PWM on the Mega. Preserve this nonsequential
map for the first shield, or revise firmware, diagram, connector labels, and
tests together. Avoid soldering a sequential A2–A9 arrangement while retaining
the current `potPins` array. Ports D0/D1 remain available for USB serial.

Suggested physical layout:

1. Eight clearly numbered pot connectors, each labeled supply/wiper/ground,
   plus separate numbered rod-light connectors. Choose connector ratings and
   polarities for the actual loads and label both ends of every cable.
2. Two keyed sensor connectors labeled by the **actual datasheet pinout**.
   Reserve space for decoupling/filtering and configurable bias/level conversion;
   the drawing alone does not justify two-wire reed-switch wiring.
3. Eight suitable PWM driver channels if rods contain LED groups. Include
   current limiting for each LED branch, driver input pull-downs, and local
   supply decoupling. Verify transistor E/B/C or MOSFET G/D/S orientation from
   the exact part number, rather than the outline in Fritzing.
4. A dedicated lighting-supply connector, adequately sized conductors, and a
   strip connector. Route load current directly through distribution wiring
   sized for it; do not funnel amperes through narrow prototyping tracks.
5. A strip data resistor and the verified bulk capacitor close to the strip
   input. Keep signal and ground paired on longer sensor/data runs; provide
   strain relief and access for a meter.
6. Before power: check shorts and every connector/pin net. Bring up one pot,
   sensor, driver, and pixel first; then add channels and measure worst-case
   load. Re-export the final diagram and run the same pin/net tests.

Exact footprints, resistor values, wire sizes, and sensor-supply arrangements
depend on the unconfirmed parts. This proposal is sufficient to choose the
architecture and pin allocation, but is not a verified solder-by-hole drawing.

## ESP32-S3 feasibility

**Feasible as a replacement I/O controller.** Keep Pi/Pd, use USB serial first,
and prototype one sensor/pot/driver/pixel before moving the complete harness.
There is no need for Wi-Fi to reproduce the current interaction.

| Concern | Migration requirement |
| --- | --- |
| Voltages | S3 circuitry operates at 3.3 V; existing 5 V signals must be assessed and converted. Do not connect the current +5 V sensor pull-ups or pot wipers directly. |
| Pots/ADC | Use suitable analog GPIOs on the exact board. The default Arduino ADC result is 12-bit; explicitly preserve 0–1023 telemetry or change all Pd normalization/thresholds. |
| ADC input range | At maximum attenuation the S3 documented range is approximately 0–3.1 V, not a guarantee of accurate rail-to-rail 3.3 V measurement. Plan scaling/end-point calibration and filtering. |
| Door sensors | Confirm output type and maximum voltage. Powered IR sensors may need 5 V supply with an output divider/buffer; open-collector outputs may allow 3.3 V pull-ups if the datasheet permits. Digital sensors can avoid using two ADC channels. |
| Eight rod PWM channels | S3 has eight LEDC channels, sufficient for eight independent rods. Set frequency/resolution explicitly and check configuration success. |
| NeoPixels | Keep their separate appropriate supply. For 5 V pixels use an appropriate 3.3→5 V data buffer such as 74AHCT125, plus the strip data resistor. Verify the NeoPixel library/backend for the chosen core. |
| Pin budget | Full analog design needs ten suitable ADC inputs, eight PWM outputs, and one data output. Board header exposure, strapping pins, onboard devices, and flash/PSRAM reservations determine whether they are available. |
| USB | Native USB data uses GPIO19/20; reserve those if using that connection. Enable/configure USB CDC for the selected board, and update the Pi's device selection/reconnect behavior. |

The ADC and PWM points come from Espressif's
[Arduino ADC reference](https://docs.espressif.com/projects/arduino-esp32/en/latest/api/adc.html)
and [LEDC reference](https://docs.espressif.com/projects/arduino-esp32/en/latest/api/ledc.html).
Board-dependent USB, memory/strapping restrictions, and ADC decoupling are
covered by the [S3 hardware checklist](https://docs.espressif.com/projects/esp-hardware-design-guidelines/en/latest/esp32s3/schematic-checklist.html)
and [USB CDC guide](https://docs.espressif.com/projects/arduino-esp32/en/latest/tutorials/cdc_dfu_flash.html).
The 5 V pixel-buffer recommendation follows
[Adafruit's logic guidance](https://learn.adafruit.com/adafruit-neopixel-uberguide/best-practices).

Prefer ADC1 for the eight pots, especially if Wi-Fi might be added; the exact
board may not expose eight uncomplicated ADC1 pins. GPIO3 is a strapping pin,
so a pot can affect boot if assigned there without careful design. If all ten
inputs must stay analog and suitable pins are unavailable, use an external
ADC or analog multiplexer. Choose it after identifying the board and sensor
output ranges; do not guess a universal S3 pin map. Wi-Fi adds transport,
reconnection, and ADC-resource considerations without solving current wiring.

The extra processor performance is useful for future networking/animation, but
the present problem is integration reliability. The Mega shield is the smaller
change; make the harness modular so an S3 controller/carrier can be tried later
without resoldering the installation's pots and lights.

## Automated checks added and executed

See [tests/README.md](../tests/README.md) for commands, fixtures, limitations,
and extension ideas. The testbed uses free tools and has no paid simulator or
service dependency:

- Python standard-library checks for Pd graphs, Fritzing nets, and audio assets.
- C++ host execution of the real main sketch with small Arduino/NeoPixel doubles.
- Actual Mega AVR compilation with Arduino CLI, both folder and isolated sketch.
- Actual headless Pd execution of the main control graph with serial/audio
  boundary probes; tests preserve production object/connection ordering.
- A virtual serial-port fixture for manual integration testing of the real
  `arduino.pd`, installed externals, and audio playback on macOS/Linux.

The completed run executed **20 tests: 7 passed and 13 test methods failed**.
There are **19 failure records** because some methods check multiple doors,
pots, songs, or assets. No test was skipped in that full run. Current defects
are genuine failing assertions; the suite intentionally exits nonzero until
they are resolved. The tests are not a claim that physical wiring is validated.

The recorded baseline includes source hashes. Check those before applying its
line references to later edits by Vibe or another developer. Physical tests
remain necessary for sensor polarity, split rails, voltage/current, ADC noise,
driver temperature, cable interference, and the soldered assembly.
