testomatic-io
A Python hardware abstraction layer for the I/O features of the Testomatic PCB test system chassis and its test modules.
Overview
The Testomatic hardware is two distinct physical devices:
- Chassis — the fixed enclosure: IOMOD I/O expander modules (behind a TCA9548A I2C multiplexer), the 3.3V/5V/12V power rail relays and INA260 sensors, the IOMOD interrupt line, the external "test start" button, the piezo beeper, and the chassis's own identity EEPROM.
- Test Module — a swappable shim that plugs into the chassis to interface with a specific Device Under Test. Today that's just its identity EEPROM; more Test-Module-specific hardware and drivers will be added over time, varying per Test Module type.
testomatic_io.Chassis and testomatic_io.TestModule are the two top-level
entry points, each grouping its subsystems into sub-namespaces:
from testomatic_io import Chassis, TestModule
chassis = Chassis()
chassis.init()
test_module = TestModule()
test_module.init()
chassis.iomod.digital_write('C', 4, True)
chassis.power.rail_3v3(True)
chassis.power.read_3v3() # PowerReading(voltage, current, power)
chassis.interrupts.is_asserted()
chassis.button.pressed()
chassis.beeper.beep(0.1)
chassis.hat_eeprom.read(0, 32)
test_module.eeprom.read(0, 32)
Features
- IOMODs: multiple ADC/DAC/GPIO expander modules behind a TCA9548A multiplexer, with the expander chip on each module identified automatically by its I2C address — no manual configuration needed
- Power rails: turn the 3.3V/5V/12V rails to the Device Under Test on or off, and measure voltage/current/power on each via INA260 sensors
- IOMOD interrupts: read the shared interrupt line that all IOMODs OR onto
- Button: read the external "test start" button
- Beeper: drive the piezo beeper
- EEPROMs: read/write the chassis identity EEPROM and the Test Module identity EEPROM (both CAT24C32, on I2C bus 0)
Not yet implemented: thermal camera capture (planned, MLX90640 over serial), and Test-Module-specific drivers beyond the identity EEPROM.
Supported IOMOD Expander Chips
- AD5593R — 8-channel ADC/DAC/GPIO, I2C address
0x10 - MCP23008 — 8-channel digital GPIO only (no ADC/DAC), I2C address
0x20
Support for additional chips (e.g. Serial Wombat) can be added by writing a driver — see testomatic_io/iomod/drivers/ for the driver interface and the AD5593R driver as a reference implementation.
Hardware Requirements
- Raspberry Pi (or other Blinka-supported board)
- TCA9548A I2C multiplexer, on I2C bus 1
- One or more IOMOD modules (connected via the multiplexer), each fitted with a supported expander chip
- 3× INA260 power monitors on I2C bus 1 (not behind the multiplexer):
3.3V rail at
0x42, 5V rail at0x41, 12V rail at0x40 - 2× CAT24C32 I2C EEPROMs on I2C bus 0: chassis identity at
0x50, Test Module identity at0x51 - See testomatic_io/pinout.py for the full GPIO pin map (power rail relays, button, beeper, IOMOD interrupt line)
Dependencies
pip install -r requirements.txt
or for local development:
pip install -e .
import testomatic_io requires actual Raspberry Pi (or other
Blinka-supported) hardware — board/Adafruit-Blinka does platform detection
at import time and raises NotImplementedError on unsupported platforms
(e.g. a Mac dev machine). When working on this repo without hardware, verify
logic by stubbing board, tca9548a, gpiod, and busio with
fakes before importing testomatic_io, rather than trying to run it
directly.
Tests
pip install -e ".[dev]"
pytest
The test suite stubs the hardware packages above (see tests/conftest.py),
so it runs on any machine — no Raspberry Pi required. It covers driver
logic, GPIO active-high/low behaviour, and present/absent handling for the
EEPROMs and power sensors; it does not verify real electrical behaviour,
which still needs a manual pass against actual hardware.
Quick Start
from testomatic_io import Chassis
chassis = Chassis()
chassis.init()
# Scan for available IOMODs
modules = chassis.iomod.scan_modules()
print(f"Available modules: {modules}")
# Write HIGH to pin 4 on module D
chassis.iomod.digital_write('D', 4, True)
# Read from pin 1 on module C
value = chassis.iomod.digital_read('C', 1)
# Read analog voltage from pin 3 on module B
voltage = chassis.iomod.read_voltage('B', 3)
# Turn on the 3.3V rail and read it back
chassis.power.rail_3v3(True)
reading = chassis.power.read_3v3()
print(f"3.3V rail: {reading.voltage:.3f}V, {reading.current:.1f}mA, {reading.power:.0f}mW")
Modules can be referenced either by letter ('A'-'H', preferred) or by their
underlying numeric channel (0-7) on the TCA9548A multiplexer.
API Reference
Chassis
chassis.init(i2c_bus=None, i2c_bus0=None)
Initialize every chassis subsystem. i2c_bus is bus 1 (IOMODs + power
sensors), defaulting to board.I2C(). i2c_bus0 is bus 0 (the identity
EEPROM bus) — pass the same object to TestModule.init() if using both
together, so they share one bus 0 connection.
chassis.iomod — IOMODs
chassis.iomod.scan_modules()
Scan for available modules and return a list of working module letters
('A'-'H'). Each module's expander chip is identified automatically by
its I2C address.
chassis.iomod.select_module(module_id)
Validate that a module is present and ready for use.
chassis.iomod.digital_write(module_id, pin, value)
chassis.iomod.digital_read(module_id, pin)
chassis.iomod.toggle(module_id, pin)
Digital I/O: write/read HIGH/LOW, or toggle an output pin.
chassis.iomod.analog_read(module_id, pin, average=1)
chassis.iomod.analog_write(module_id, pin, value)
chassis.iomod.read_voltage(module_id, pin, average=1)
Analog I/O: raw ADC value (0-4095), raw DAC value (0-4095), or voltage in volts (chip-dependent).
chassis.iomod.pin_mode(module_id, pin, mode)
Configure pin mode: testomatic_io.INPUT, OUTPUT, ADC, or DAC.
chassis.iomod.set_vref(module_id, activate=True)
chassis.iomod.get_vref(module_id)
chassis.iomod.get_dac_range(module_id)
chassis.iomod.set_dac_range(module_id, range=2)
chassis.iomod.set_ldac_mode(module_id, mode)
Voltage reference and DAC range management (chip-dependent).
chassis.iomod.reset(module_id)
Reset the specified module.
chassis.power — power rails
chassis.power.rail_3v3(on)
chassis.power.rail_5v(on)
chassis.power.rail_12v(on)
Turn a power rail relay on or off.
chassis.power.rail_3v3_enabled()
chassis.power.rail_5v_enabled()
chassis.power.rail_12v_enabled()
Read back whether a rail relay is currently on.
chassis.power.read_3v3()
chassis.power.read_5v()
chassis.power.read_12v()
Read a rail's INA260 sensor. Returns a PowerReading(voltage, current, power)
namedtuple — voltage in V, current in mA, power in mW. Raises RuntimeError
if that rail's sensor wasn't detected on the bus.
chassis.power.read_3v3_available()
chassis.power.read_5v_available()
chassis.power.read_12v_available()
Check whether a rail's INA260 sensor was detected on the bus, before calling
read_*.
chassis.interrupts
chassis.interrupts.is_asserted()
True if any IOMOD has a pending interrupt (the shared line is driven low).
Doesn't identify which module — poll chassis.iomod's modules to find it.
chassis.button
chassis.button.pressed()
True while the external button is held down.
chassis.beeper
chassis.beeper.on()
chassis.beeper.off()
chassis.beeper.beep(duration_s=0.1)
Drive the piezo beeper.
chassis.hat_eeprom
chassis.hat_eeprom.present # False if no EEPROM responded on the bus
chassis.hat_eeprom.read(address, length)
chassis.hat_eeprom.write(address, data)
Read/write the chassis identity EEPROM (CAT24C32 at 0x50, I2C bus 0). If
no chip responds at that address, present is False and read/write
raise RuntimeError instead of the underlying I2C probe crashing
Chassis.init().
TestModule
test_module.init(i2c_bus0=None)
Initialize the Test Module. i2c_bus0 is bus 0 — pass the same object as
Chassis.init()'s if using both together.
test_module.eeprom.present # False if no Test Module (or no EEPROM) is present
test_module.eeprom.read(address, length)
test_module.eeprom.write(address, data)
Read/write the Test Module identity EEPROM (CAT24C32 at 0x51, I2C bus 0).
Constants
testomatic_io.HIGH/LOW— digital states (1 / 0)testomatic_io.INPUT/OUTPUT/ADC/DAC— IOMOD pin modes
Adding a New IOMOD Expander Driver
To support a new I/O expander chip:
- Create a driver module in
testomatic_io/iomod/drivers/that subclassesExpanderDriver(seetestomatic_io/iomod/drivers/base.pyfor the interface andtestomatic_io/iomod/drivers/ad5593r.pyfor a reference implementation). - Implement
probe(i2c_adapter, address)as a classmethod that returnsTruewhen the driver recognises a chip at that address — matching on address alone is fine for chips with a fixed or narrow address range; chips that could share an address with another supported device should read an identifying register instead. - Implement the operations the chip actually supports (
pin_mode,digital_write,digital_read,toggle, and where applicableanalog_read,analog_write,read_voltage,set_vref,get_vref,get_dac_range,set_dac_range,set_ldac_mode,reset). Operations a chip doesn't support can be left unimplemented — the base class raisesNotImplementedErrorfor them. - Register the class in
DRIVERSintestomatic_io/iomod/drivers/__init__.py.
Once registered, modules using that chip are detected and used automatically
— no changes to IOModManager itself are needed.
Running Examples
Basic Example
python examples/testomatic_io_example.py
Interactive Mode
python examples/testomatic_io_example.py interactive
The interactive mode provides a command-line interface for testing individual functions.
Error Handling
- Invalid module: Must be a letter A-H or number 0-7
- Invalid pin number: Must be 0-7
- Invalid pin mode: Must be INPUT, OUTPUT, ADC, or DAC
- No supported expander found: No registered driver recognised a chip on that module's I2C channel
- Module not found: Module not responding on I2C bus
- Fixed I2C device not found: EEPROMs (
Cat24C32) and power sensors (PowerSensors, INA260) trap the construction-time probe failure instead of raising (seetestomatic_io/i2c_probe.py) — check.present/present_3v3etc. (orchassis.power.read_3v3_available()) before reading, which otherwise raiseRuntimeErrorif called while absent - I2C errors: Communication failures
- Unsupported operation: A chip-dependent operation (e.g. analog I/O) not supported by the expander on that module
Notes
- IOMOD modules are referenced by letter A-H (preferred) or by their underlying numeric channel 0-7 (TCA9548A has 8 channels)
- IOMOD pin numbers range from 0-7 (matching the AD5593R's 8 I/O pins; other supported chips may differ)
- ADC/DAC values are 12-bit (0-4095) on the AD5593R
- Voltage reference is typically 2.5V or 5V depending on configuration
- Not every expander chip supports every operation — GPIO-only chips will
raise
NotImplementedErrorfor analog/voltage-reference calls ChassisandTestModuleare independent top-level classes, not nested, since they represent two separate physical devices with independent lifecycles (a Test Module can be swapped without reinitializing the chassis)
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file testomatic_io-0.1.0.tar.gz.
File metadata
- Download URL: testomatic_io-0.1.0.tar.gz
- Upload date:
- Size: 27.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4949d3f8c6910258873102cbdafac2214a08eff24218537b1787eb098984379e
|
|
| MD5 |
cd03f36e70977248d5de1d0a0c08d3be
|
|
| BLAKE2b-256 |
d42ead31e160d87625f76cd18edae2eb84dd4611e5c3d3ce2500b4a9fe7f0357
|
File details
Details for the file testomatic_io-0.1.0-py3-none-any.whl.
File metadata
- Download URL: testomatic_io-0.1.0-py3-none-any.whl
- Upload date:
- Size: 25.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41976e09cc6358ca356f9c95c5a180636ad10df779693b6ba31414f1ebf9e26a
|
|
| MD5 |
93b9ce86cc5202bfe1758fe49a7207b9
|
|
| BLAKE2b-256 |
8c1debf6812ea5061109bc2f21bd63cd403e1dfc27b2b4767b52350eda8e6369
|