Skip to main content

Introduction

Documentation Status Discord Build Status Code Style: Ruff

Detect selected Adafruit STEMMA QT sensors on a Raspberry Pi and optionally install their CircuitPython drivers. It recognizes only sensors with bundled probe modules; it is not a universal I²C device identifier.

Each sensor definition contains only:

  • ADDRESSES

  • DEFAULT_ADDRESSES (optional for configurable-address sensors)

  • PACKAGE

  • PROBE_CONFIDENCE

  • PROBE_RISK (optional for probes that send multi-byte addresses or commands)

  • probe(bus, address)

Definitions may optionally express a device signature with the helpers in stemma_detect.signature. A signature combines several safe, read-only characteristics, such as an exact chip ID, reserved-bit patterns, revision values, and nonblank factory calibration data. Checks carry weights so results can expose both a categorical confidence and an evidence score.

The scanner uses smbus2 for I²C access and Adafruit Python Shell for prompts and streaming installation commands. Individual CircuitPython drivers are imported neither by the scanner nor by chip definitions.

Dependencies

This driver depends on:

Installing from PyPI

On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally from PyPI. To install for current user:

pip3 install adafruit-stemma-detect

To install system-wide (this may be required in some cases):

sudo pip3 install adafruit-stemma-detect

To install in a virtual environment in your current project:

mkdir project-name && cd project-name
python3 -m venv .env
source .env/bin/activate
pip3 install adafruit-stemma-detect

Running from a checkout

python3 -m venv --system-site-packages .venv
.venv/bin/python -m pip install -e .
.venv/bin/stemma-scan --bus 1

Use --install to install drivers for definitive matches:

.venv/bin/stemma-scan --bus 1 --install

Address-only or otherwise ambiguous results are reported but not installed.

To be prompted before installing a driver for each possible match, use:

.venv/bin/stemma-scan --bus 1 --install --prompt-possible-matches

Possible matches default to “no.” This is important because several unrelated devices can share the same I²C address.

The complete scan finishes before any prompts are shown. Definitive-capable probes run before possible-only probes at each address. A definitive match claims its I²C address immediately, so possible-only candidates are neither probed nor presented. If no definitive probe matches, the possible candidates are collected. A declined candidate is removed from the results; once one is confirmed, it is retained and all remaining candidates at that address are removed without further prompts.

Possible candidates using their default address are prompted before candidates using an alternate address. The prompt labels the address as default or alternate when that information is known.

Multiplexers

The scanner automatically looks for PCA9546-compatible four-channel and PCA9548/TCA9548A-compatible eight-channel multiplexers at 0x70 through 0x77. Each channel is scanned separately, and the route is included in every result:

MUX: PCA9546 at 0x70 (4 channels)
MUX: PCA9546 at 0x71 via mux 0x70 channel 1 (4 channels)
MATCH: VL53L4CD at 0x29 via mux 0x70 channel 2
MATCH: VL6180X at 0x29 via mux 0x70 channel 1 via mux 0x71 channel 3

No option or CircuitPython multiplexer driver is required. Detection and channel selection use the project’s small I²C bus interface, so this feature does not add Blinka as a dependency.

These multiplexers have no identity register. To reduce false identification, the scanner first requires a plausible one-byte control value, then verifies that channel-selection writes read back with the expected four- or eight-channel mask. The original control value is restored after probing and again after the scan. This is still an active probe: an unrelated device at 0x70 through 0x77 with mux-like behavior could be changed.

Muxes are discovered recursively to a maximum of eight channel hops. Each nested mux must have an address different from every mux upstream of it. Muxes chained with the same address cannot be controlled independently because a channel-selection write reaches both devices; change one mux’s address jumpers before scanning that topology.

Using as a library

The high-level detect function opens a Raspberry Pi I²C bus, scans it and any compatible multiplexers, then closes the bus. It returns structured data without printing, prompting, or installing drivers:

from stemma_detect import detect

report = detect(bus_number=1)

for detection in report.matches:
    print(detection.name, detection.address_hex, detection.driver_package)

for detection in report.possible_matches:
    print("Needs confirmation:", detection.name)

Applications that already own an I²C connection can pass any object implementing I2CBusProtocol. The built-in catalog is used automatically:

from stemma_detect import scan_all

report = scan_all(my_i2c_bus)

Pass chips=discover_chips() explicitly only when filtering or extending the catalog. Both detect and scan_all accept diagnostic=callback for applications that need every probe outcome.

Installing drivers from a library

create_install_plan automatically includes definitive matches. Possible matches are excluded unless the application confirms them with a callback. A script that knows its expected hardware can match the sensor name, address and mux path:

from stemma_detect import create_install_plan, detect, install_drivers

report = detect(1)
expected = {
    ((), 0x48): "pcf8591",
}

def confirm_possible(sensor):
    return expected.get((sensor.path, sensor.address)) == sensor.name

plan = create_install_plan(report, confirm_possible=confirm_possible)

for item in plan:
    state = "installed" if item.installed_version else "not installed"
    print(item.package, state)

results = install_drivers(plan)

The callback is called only for possible matches. If it confirms two different candidates at the same address and mux path, planning raises ValueError instead of silently selecting one. Packages shared by multiple detected sensors are deduplicated. install_drivers does not print or prompt; it returns an InstallResult for each package with an InstallOutcome of INSTALLED, ALREADY_INSTALLED or FAILED.

JSON output

Use --json to write one machine-readable document to standard output. It contains the bus, multiplexer topology, detections, confidence, signature evidence and scores, and CircuitPython driver installation status. Both integer and hexadecimal forms of each I²C address are included:

.venv/bin/stemma-scan --bus 1 --json
.venv/bin/stemma-scan --bus 1 --json > stemma-scan.json

The top-level schema_version is incremented if a future release makes an incompatible output change. JSON mode cannot be combined with --install or --diagnostics because prompts, installation progress, and transaction traces would make standard output invalid JSON.

Library users can serialize an existing report without running another scan:

data = report.to_dict(bus=1)
text = report.to_json(bus=1)

The existing report_to_dict and report_to_json functions remain available for functional style code.

Diagnostics

Use --diagnostics to show every probe attempted, including its safety category, non-matches, and I²C errors that are hidden during a normal scan:

.venv/bin/stemma-scan --bus 1 --diagnostics

An address that does not acknowledge an I²C transaction is reported as NOT DETECTED. The ERROR label is reserved for unexpected failures. Successful transactions include their raw write and read bytes so new or mismatched identity probes can be investigated without changing the chip module.

Definitive probes run before possible-only probes at each address. Within each category, probes are ordered from lowest to highest risk: passive reads, ordinary one-byte register reads, then commands or multi-byte register addressing. A definitive match prevents all remaining probes from touching that address. When only possible matches remain, candidates with more weighted signature evidence are reported and prompted first; a factory-default address adds a small score bonus.

Known limitations and planned work

The CLI is a consumer of the same detection API available to other programs. Library imports do not scan hardware, print, prompt, install packages, or exit the process as a side effect. Diagnostics and driver installation remain explicit opt-in operations.

The scanner cannot resolve two devices responding at the same address. They may corrupt each other’s identity responses and produce only ambiguous possible matches. Conflicting devices must be readdressed or placed on separate multiplexer channels. Automatic mux scanning resolves conflicts between different channels, but it cannot resolve a conflict between a root-bus device and a device behind a currently selected mux channel.

Adafruit’s Troublesome Chips guide identifies devices with unusual I²C behavior that can cause missed detections or communication failures:

  • AGS02MA requires a 20–30 kHz bus.

  • AM2320 automatically sleeps, making scans unreliable.

  • ATECCx08 requires slow I²C communication when waking from sleep.

  • BNO055 and BNO085 use clock stretching, can violate timing requirements, and may need resets.

  • CCS811 uses clock stretching.

  • LC709203F uses repeated starts, clock stretching, and sleep mode.

  • Older MCP9600 devices can duplicate register data; MCP9600 and MCP9601 also use repeated starts and clock stretching and may ignore zero-length scan writes.

  • PN532 uses clock stretching.

The current catalog includes AGS02MA, AM2320, BNO055, BNO08x/BNO085, CCS811, LC709203F, and MCP9600. A failed probe for one of these chips does not necessarily mean the device is absent. Raspberry Pi users should also consult Adafruit’s I²C clock stretching guide.

Supported sensors so far

The catalog currently contains 122 device definitions: 93 with definitive-capable probes and 29 that produce possible matches. Possible matches are never installed without --prompt-possible-matches and user confirmation.

Devices with definitive probes include ADT7410, AGS02MA, AM2320, APDS9960, APDS9999, AS726x, AS7331, AS7341, AS7343, AW9523, BME280, BME680, BMP280, BMP3xx, BMP5xx, BNO055, CAP1188, CCS811, CST8xx, DPS310, DRV2605/DRV2605L, ENS160, FXAS21002C, FXOS8700, GUVX I2C, HDC302x, HTS221, HTU21D, ICM20x, INA228, INA237/INA238, INA260, INA3221, LC709203F, LIS2MDL, LIS331, LIS3DH, LIS3MDL, LPS2x, LPS28, L3GD20, LSM303DLH magnetometer, LSM6DS, LSM9DS0, LSM9DS1, LTR329/LTR303, LTR390, MAX1704x, MCP9600, MCP9808, MLX90614, MLX90632, MMA8451, MMC5603, MPL3115A2, MPU6050, MS8607, MSA301, OPT4048, PA1010D, PMSA003I, QMC5883P, SCD30, SCD4x, SEN6x, Seesaw, SGP30, SGP40, SGP41, SHT31D, SHT4x, SHTC3, SI1145, Si7021, SPA06-003, STCC4, STHS34PF80, TCS3430, TCS34725, TMAG5273, TMP006, TMP007, TMP117/TMP119, TSL2561, TSL2591, VCNL4030, VCNL4040, VCNL4200, VEML6075, VL53L0X, VL53L1X, VL53L4CD, and VL6180X.

Possible-match definitions include ADXL34x, ADXL37x, AHTx0, AS5600, BH1750, BNO08x, DS3502, HTU31D, INA219, LIDAR-Lite, LPS35HW, LSM303 accelerometer, MAX44009, MCP3421, MLX90393, MLX90395, MLX90640, MPL115A2, MPR121, MPRLS, PCF8591, PCT2075, TC74, TLV493D, TSC2007, VCNL4010, VCNL4020, VEML6070, and VEML7700.

Adding a sensor

Add one module under stemma_detect/chips/. Modules are discovered automatically, so no registry edit is needed. Use one module per installable driver family: chips that cannot be distinguished but use the same CircuitPython package should share a family definition and produce one detection. The catalog requires package names to be unique. Keep separate definitions when ambiguity changes which driver would be installed.

from stemma_detect.result import Confidence, ProbeResult, ProbeRisk

ADDRESSES = (0x44,)
DEFAULT_ADDRESSES = (0x44,)  # Optional; single addresses are defaults automatically.
PACKAGE = "adafruit-circuitpython-example"
PROBE_CONFIDENCE = Confidence.MATCH
PROBE_RISK = ProbeRisk.COMMAND  # Only for commands or multi-byte addresses.

def probe(bus, address):
    value = bus.read_register(address, 0x00, 1)
    if value == b"\x12":
        return ProbeResult.match({"id": value.hex()})
    return ProbeResult.no_match()

Keep probes short, non-destructive, and independent of the package they are intended to install. Most identity-register probes should omit PROBE_RISK and use the default register category. Address-only definitions automatically use the passive category. Set ProbeRisk.COMMAND when a probe transmits a command or a multi-byte register address that another chip could interpret as a write. Family probes may pass name to ProbeResult.match() when an identity register distinguishes a specific chip. Ambiguous IDs should remain at the family level.

When a sensor has several useful read-only registers or safe identity commands, prefer a device signature over custom probe logic. command_response supports CRC-protected serial-number and feature-set commands while using the same weights and result handling as register checks.

from stemma_detect.result import Confidence
from stemma_detect.signature import DeviceSignature, exact, not_blank

ADDRESSES = (0x76, 0x77)
PACKAGE = "adafruit-circuitpython-example"
PROBE_CONFIDENCE = Confidence.MATCH

SIGNATURE = DeviceSignature(
    (
        exact("chip_id", 0xD0, b"\x60", show_value=True, weight=10),
        exact(
            "status_reserved",
            0xF3,
            b"\x00",
            mask=b"\xF6",
            required=False,
            weight=2,
        ),
        not_blank("calibration", 0x88, 24, required=False, weight=3),
    ),
    match_threshold=15,
)

def probe(bus, address):
    return SIGNATURE.probe(bus, address)

Failure of a required check produces NO_MATCH. Supporting checks contribute weight; missing supporting evidence lowers the score and may reduce the result to POSSIBLE without rejecting it. The scanner adds one weak point for a known default address but never lets that address bonus turn a possible result into a definitive match. Only definitive MATCH results are installed automatically.

Scores represent accumulated evidence, not a statistical probability. Weights should be kept consistent across definitions: exact identity registers should dominate, while address responses and default-address bonuses should remain weak evidence.

Only use documented, safe reads: avoid FIFO, read-to-clear, write-only, initialization, reset, and measurement commands. Identity and serial-number commands are suitable when their datasheet says they do not alter sensor state. Mark command-based probes as ProbeRisk.COMMAND. not_blank is intended for factory-programmed blocks where all-zero and all-0xFF data are invalid; it should not be used for ordinary configuration or measurement registers.

Development

python3 -m unittest discover -s tests -v
ruff check .
ruff format --check .

Contributing

Contributions are welcome! Please read our Code of Conduct before contributing to help this project stay welcoming

License

MIT, see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

adafruit_stemma_detect-1.0.0.tar.gz (57.6 kB view details)

Uploaded Source

Built Distribution

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

adafruit_stemma_detect-1.0.0-py3-none-any.whl (91.9 kB view details)

Uploaded Python 3

File details

Details for the file adafruit_stemma_detect-1.0.0.tar.gz.

File metadata

  • Download URL: adafruit_stemma_detect-1.0.0.tar.gz
  • Upload date:
  • Size: 57.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.15

File hashes

Hashes for adafruit_stemma_detect-1.0.0.tar.gz
Algorithm Hash digest
SHA256 a90ff7365f99267334c3fcce7faf338bcad546483d364722fe3feceeb1c6e68a
MD5 3000679d7335ed21796c791acbe43fbb
BLAKE2b-256 85800d189ba810ccab9f128361762b2ca89bcfcea621553b858fe3d30a4c3c17

See more details on using hashes here.

File details

Details for the file adafruit_stemma_detect-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for adafruit_stemma_detect-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 948fdd7b6970616eef25a85d69b55a04c2a365e76b8330f308ab3cebcf9a2128
MD5 4b5b4b9c307a37f7c5e24f3b51db359c
BLAKE2b-256 17744875de29b45fc50951e00994692ecfa1addff0e6197f4c5de17f810fa1a1

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.0 This release

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