Skip to main content

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 at 0x41, 12V rail at 0x40
  • 2× CAT24C32 I2C EEPROMs on I2C bus 0: chassis identity at 0x50, Test Module identity at 0x51
  • See testomatic_io/pinout.py for the full GPIO pin map (power rail relays, button, beeper, IOMOD interrupt line)

Enabling I2C bus 0

I2C bus 0 (the ID_SD/ID_SC pins, GPIO0/GPIO1) is disabled by default — it's reserved for HAT ID EEPROM detection and isn't brought up by the normal raspi-config "Enable I2C" option, which only enables bus 1. Without it, open_i2c_bus0() (used by Chassis.hat_eeprom and TestModule.eeprom) fails with a misleading error like:

ValueError: No Hardware I2C on (scl,sda)=(1, 0)
Valid I2C ports: ((1, 3, 2), (0, 1, 0), (10, 45, 44)).
Make sure I2C is enabled.

(0, 1, 0) in that list looks like a match for the requested (1, 0), which is what makes this misleading — the pins are the right ones for bus 0. Blinka's busio.I2C matches the pin pair first, then tries to actually open that bus; if the bus isn't enabled at the OS level, opening it raises internally and Blinka silently falls through to try the next candidate before eventually raising this "no hardware I2C" error, without indicating that a pin match was in fact found.

To fix it, enable the i2c_vc device tree overlay and reboot:

echo "dtparam=i2c_vc=on" | sudo tee -a /boot/firmware/config.txt
sudo reboot

(On older Raspberry Pi OS releases the file is /boot/config.txt instead of /boot/firmware/config.txt.) After rebooting, ls /dev/i2c-* should list the new bus.

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.

rpi-lgpio (declared above, alongside gpiod) is what lets import board succeed at all on a Raspberry Pi: Blinka's own board/pin detection needs RPi.GPIO importable even though this package only asks Blinka for I2C. Before this was a declared dependency, installing on a fresh Raspberry Pi OS (Bookworm or later) failed several layers deep in adafruit_blinka with ModuleNotFoundError: No module named 'RPi' — a real GPIO backend was missing, but nothing in the error said so. rpi-lgpio fixes this by providing the same RPi.GPIO import name on top of the modern gpiochip character-device API, so it works unmodified across old and new Raspberry Pi OS releases. If you ever see that error anyway (e.g. an editable install from before this was added), install it directly:

pip install rpi-lgpio

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:

  1. Create a driver module in testomatic_io/iomod/drivers/ that subclasses ExpanderDriver (see testomatic_io/iomod/drivers/base.py for the interface and testomatic_io/iomod/drivers/ad5593r.py for a reference implementation).
  2. Implement probe(i2c_adapter, address) as a classmethod that returns True when 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.
  3. Implement the operations the chip actually supports (pin_mode, digital_write, digital_read, toggle, and where applicable analog_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 raises NotImplementedError for them.
  4. Register the class in DRIVERS in testomatic_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

Examples live in examples/ in the repo and are included in the source distribution (sdist), but are not installed into a venv's site-packages — they're demo scripts to read and run from a checkout, not importable library code. Since this package normally needs a real Testomatic chassis and is installed with pip install -e . from a git clone (see Dependencies), the examples/ directory is already right there alongside the code; there's nothing extra to install.

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 (see testomatic_io/i2c_probe.py) — check .present / present_3v3 etc. (or chassis.power.read_3v3_available()) before reading, which otherwise raise RuntimeError if 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 NotImplementedError for analog/voltage-reference calls
  • Chassis and TestModule are 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

testomatic_io-0.1.1.tar.gz (34.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

testomatic_io-0.1.1-py3-none-any.whl (27.7 kB view details)

Uploaded Python 3

File details

Details for the file testomatic_io-0.1.1.tar.gz.

File metadata

  • Download URL: testomatic_io-0.1.1.tar.gz
  • Upload date:
  • Size: 34.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.4

File hashes

Hashes for testomatic_io-0.1.1.tar.gz
Algorithm Hash digest
SHA256 388a38480adcc32c257a93dbc90343fabd999340c284d1e74f5ca60a6e4b994b
MD5 e178e633bedb35b86fdfd82473c5d069
BLAKE2b-256 737f4caea638f1ca0c6e1d548c98137ddf8e672249f4c94e0be76a010f1112eb

See more details on using hashes here.

File details

Details for the file testomatic_io-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: testomatic_io-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 27.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.4

File hashes

Hashes for testomatic_io-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8d6448d6592ced97b7d8708d5f89f7cb56d31d63cad010ff22292c9a96580c81
MD5 f4f5ad36ac3ce87add3a4c14e6f6da0d
BLAKE2b-256 0d202c42abab4c14e8fe6f37f11ca135cdf4354c235423beb3f7d628aa10b521

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page