Skip to content

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_VERSION in examples/cow-board-test.py (currently 2.5.0), firmware_version.h (1.1.0, 2026-08-01), and each fixture's ProductionFixtureProfile. SemVer, never decremented or reused, and when unsure pick the larger bump. See semver-discipline.
  • Every new fixture defines and validates a ProductionFixtureProfile before discovery or power-on. Shared behavior goes in universal_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.md is 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.