universal-tester¶
Reusable "brain box" + swappable fixture boards for automated PCB test.
Repo: ~/Documents/GitHub/Universal_Tester. Board layout work for the next
revision lives in universal-tester-003.
Productization, pricing, and the build plan live in
universal-tester-product.
Host PC --USB--> UT002 brain box --40-pin 2x20--> Fixture board --> UUT
Three layers, three owners. Brain box owns rails (3V3/5V/0-30V var), measurement (ADS1120 precision path), MCU buses, USB hub, fixture-ID EEPROM read. Fixture owns ID EEPROM, UUT pin mapping, level shifting, golden peripherals, and the test sequence. Host owns discovery, orchestration, bench instruments, serial-number policy, append-only logs, labels, and failure receipts. No layer duplicates another's contract.
Operating it¶
Production run: double-click the launcher, never a hand-picked serial port.
Each fixture ships four with the same display name — .command (macOS),
.bat (Windows), .desktop (Linux), .sh (shared POSIX runner). First launch
builds a private .cow-test-venv; later launches reuse it. Startup validates
all four exist via ProductionLauncherSet.validate() before discovery, so a
fixture can't silently lose an OS.
Current stations: Cow Robot Test Station, Cow Remote Test Station,
plus Cow Remote Diagnostic and Cow Remote Button Diagnostic.
Prereqs: Python 3.10+, libusb, dfu-util on PATH. macOS: brew install
libusb dfu-util (launcher checks both /opt/homebrew/lib and /usr/local/lib).
Linux: site-approved udev rules, never run the station as root.
Auto-discovery runs at the start of every attempt and names every missing
device instead of dying on a VISA timeout:
- Siglent SPD3303X-E — USB 62700:5168
- Siglent SDM3055 — USB 62700:4609
- UniversalTester-002 — USB 0483:5740 + product string
- dfu-util on PATH
The DUT is discovered after the fixture enables USB1 — it is intentionally
absent at startup. --resource, --dmm-resource, --tester-port exist for
diagnostics only.
Control panel (bench/bring-up, not production): double-click
Universal Tester Control Panel.command → localhost UI on
http://127.0.0.1:8765/. Default ceilings 5.0 V / 0.5 A on SPD CH1+CH2; raise
them only by running the backend directly with --max-voltage/--max-current.
CH3 is a manual fixed output the panel does not touch. Every button fires
immediately, no confirm dialog. A 3 s watchdog reopens dropped USB/VISA sessions
and deliberately brings USB, DMM insertion, CH1 and CH2 back off — it never
silently re-powers. Raw SCPI can desync the UI; hit ALL OUTPUTS OFF after.
Dev usage:
cd host-api && python3 -m venv .venv && . .venv/bin/activate
pip install -e . && python examples/basic_control.py
Where things live¶
host-api/universal_tester/—client.py(discovery + thread-safe transport),pins.py(restricted-pin checks),instruments/,station.py,production/(profile, terminal, launchers, logging, dfu, rework),printers/(Brother label, Zebra bag label, Citizen CT-S801 receipt),optical_*(camera calibration, ROI, color, registration).host-api/examples/cow-board-test.py— the reference production program.host-api/examples/fixture_profile_template.py— start every new fixture here.universal-tester-002-firmware/— STM32G0B1, HAL + ST USB device lib, CDC.universal-tester-002/,fixture-board-cow-*/,power-analog-board/,digital-control-board/— KiCad.portable-universal-tester/— separate ESP32-S3/DM40C/DP100 battery prototype. Different product, different contract; don't leak UT002 assumptions into it.- Logs:
host-api/examples/log-cow-remote/,log-cow-robot/—all-board-tests.jsonl(append-only, source of truth),equipment-usage-summary.json(rebuilt atomically from it, never hand-edited),serial-number-registry.jsonl, printed-label registries, rework actions.
Rules that bite¶
- Three versions, never conflated:
COW_TESTER_VERSIONinexamples/cow-board-test.py(currently 2.5.0),firmware_version.h(1.1.0, 2026-08-01), and each fixture'sProductionFixtureProfile. SemVer, never decremented or reused, and when unsure pick the larger bump. See semver-discipline. - Every new fixture defines and validates a
ProductionFixtureProfilebefore discovery or power-on. Shared behavior goes inuniversal_tester/production; only limits, identity, firmware rules, and board tests go in the fixture program. Do not fork the Cow Robot code. - Every result records software + firmware + fixture + UUT versions.
- Never infer a pin from a similarly-named fixture. Section 4 of
Universal_Tester_Requirements.mdis the locked pinout; cross-check against the current schematic. - Keep logs and the serial registry outside any replaceable app bundle (PyInstaller deployment is the planned next step).
Safety¶
Outputs default off on reset, disconnect, invalid config, and watchdog failure. No UUT power until fixture identity, wiring, limits, and instruments are all verified. Hardware interlocks outrank software intent, always. Use the documented discharge/safe-state sequence before changing routing. Don't hang arbitrary grounded equipment off an energized setup without walking the ground path — the portable tester in particular has no galvanic isolation and requires the Mac on internal battery, charger and unreviewed peripherals disconnected.
Fact precedence when docs disagree¶
safety + explicit user instruction → AGENTS.md → locked requirements/ADRs →
current source and tests → examples, logs, backups, history. Historical files are
evidence, not authority. A design file existing does not mean the circuit passed
measured acceptance test.
Log¶
- 2026-09-08: wrote this note from
PROJECT_TRAINING_GUIDE.md(reviewed 2026-08-17),AGENTS.md,host-api/docs, and the production sources. Cow Tester at 2.5.0, UT002 firmware at 1.1.0.