Skip to main content

raspihats package

This python package provides the necessary code to interface the Raspberry Pi add-on boards from raspihats.com:

Typical usage often looks like this:

#!/usr/bin/env python
# In this setup there are two I2C-HATs stacked, one DI16ac and one DQ10rly.
from raspihats.i2c_hats import DI16ac, DQ10rly

di16ac = DI16ac(0x40)   # 0x40 is the I2C bus address
dq10rly = DQ10rly(0x50) # 0x50 is the I2C bus address

while True:
    state = di16ac.di.channels[0]           # get digital input channel 0
    dq10rly.dq.channels[0] = state          # set digital output channel 0
    dq10rly.dq.channels[1] = not state      # set digital output channel 1

IRQ feature(from library v2.3.0)

Starting from hardware revision 2.0, DI16ac I2C-HAT and DI6acDQ6rly I2C-HAT can trigger an IRQ line that's connected to GPIO21 of the Raspberry Pi.

Firmware 3.0.0 completed the block along CiA 401: the edge masks (0x6007/0x6008) persist in the board's EEPROM, and the volatile global enable (0x6005) gates capture — arm it after every board reset, write 0 to disarm in one transaction (capture queue dumped, IRQ line released, masks untouched). A communication-watchdog timeout disarms the block the same way, so a dead controller is never held on the line.

Firmware 3.0.0 and newer

The IRQ line is a level — low exactly while captures pend — so a single-threaded loop with no callbacks or queues is all a host needs:

import RPi.GPIO as GPIO
from raspihats.i2c_hats import DI6acDQ6rly

IRQ_PIN = 21
GPIO.setmode(GPIO.BCM)
GPIO.setup(IRQ_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

board = DI6acDQ6rly(0x60)   # 0x60 is the I2C bus address
print(str(board.name) + ' ' + str(board.fw_version))
print('Use Ctrl+C to stop program.')

# commission once - the edge masks persist in the board's EEPROM
board.di.irq_reg.rising_edge_control = 0x3F     # rising edges, all 6 channels
board.di.irq_reg.falling_edge_control = 0x3F    # falling edges, all 6 channels

# arm - the global enable is volatile, a board reset always starts disarmed
board.di.irq_reg.capture = 0
board.di.irq_reg.global_enable = 1

try:
    while True:
        # the IRQ line is a level: low exactly while captures pend. Wait for
        # a falling edge only while it is high; the timeout re-checks the
        # level (and keeps Ctrl+C responsive), so a capture stored while the
        # line was already low is never missed.
        if GPIO.input(IRQ_PIN):
            GPIO.wait_for_edge(IRQ_PIN, GPIO.FALLING, timeout=200)
            continue
        # line is low: read one capture - (states << 16) | edge_status, 0
        # means empty - the board releases the line once the queue is drained
        capture = board.di.irq_reg.capture
        if capture == 0:
            continue
        status = capture & 0xFFFF
        states = capture >> 16
        for channel in range(len(board.di.channels)):
            if status & (0x01 << channel):
                print('IRQ on channel: %d, state: %d' % (channel, (states >> channel) & 0x01))
except KeyboardInterrupt:
    pass
finally:
    # one write disarms: queue dumped, line released, masks keep the commissioning
    board.di.irq_reg.global_enable = 0
    GPIO.cleanup()

Older firmware (before 3.0.0)

On older firmware there is no global enable — the block is live as soon as an edge mask is non-zero — and the masks are volatile, so arm them at every start. Disarm by zeroing the masks and clearing the capture queue: leftover captures keep the IRQ line asserted. The level check on the queue timeout is essential here too — a capture stored while the line is already low makes no new falling edge, so an edge-only sleep can strand events forever:

import queue
import RPi.GPIO as GPIO
from raspihats.i2c_hats import DI6acDQ6rly

IRQ_PIN = 21
GPIO.setmode(GPIO.BCM)
GPIO.setup(IRQ_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

# the ISR just hands the event to the main thread
event_queue = queue.Queue(maxsize = 20)
GPIO.add_event_detect(IRQ_PIN, GPIO.FALLING, callback=event_queue.put)

board = DI6acDQ6rly(0x60)   # 0x60 is the I2C bus address
print(str(board.name) + ' ' + str(board.fw_version))
print('Use Ctrl+C to stop program.')

# volatile on this firmware: arm the masks at every start
board.di.irq_reg.rising_edge_control = 0x3F     # rising edges, all 6 channels
board.di.irq_reg.falling_edge_control = 0x3F    # falling edges, all 6 channels
board.di.irq_reg.capture = 0

try:
    while True:
        try:
            # the timeout keeps Ctrl+C responsive and doubles as a line check
            event_queue.get(block=True, timeout=0.2)
        except queue.Empty:
            # captures stored while the line was already low make no new
            # falling edge - look at the level on every timeout
            if GPIO.input(IRQ_PIN):
                continue
        # drain the capture queue; each entry is (states << 16) | edge_status,
        # 0 means empty - the board releases the line once drained
        while True:
            capture = board.di.irq_reg.capture
            if capture == 0:
                break
            status = capture & 0xFFFF
            states = capture >> 16
            for channel in range(len(board.di.channels)):
                if status & (0x01 << channel):
                    print('IRQ on channel: %d, state: %d' % (channel, (states >> channel) & 0x01))
except KeyboardInterrupt:
    pass
finally:
    # no global enable on this firmware: disarm by zeroing the masks, then
    # clear the queue - leftover captures would keep the IRQ line asserted
    board.di.irq_reg.rising_edge_control = 0
    board.di.irq_reg.falling_edge_control = 0
    board.di.irq_reg.capture = 0
    GPIO.remove_event_detect(IRQ_PIN)
    GPIO.cleanup()

Firmware update without a jumper(from library v3.1.0)

Requires firmware from the 3.1.0 series. Before that, updating meant fitting the board's BOOT jumper by hand — which means opening the enclosure and, for a board already wired into a panel, taking it out of service.

enter_bootloader() resets the board into its ROM system bootloader over I2C, so a HAT can be updated in place — no jumper, no disassembly, nothing to touch. The board leaves its application address and re-enumerates at the ROM's address (0x3E on the STM32F0 boards), where stm32flash writes the new firmware and -g 0 starts it again:

from raspihats.i2c_hats import DI6acDQ6rly

board = DI6acDQ6rly(0x60)
print(board.name, board.fw_version)
board.enter_bootloader()      # 'boot'-guarded, no response is sent
# the board is now the ROM bootloader at 0x3E
stm32flash -w di6acdq6rly-3.1.0.bin -v -g 0 -a 0x3E /dev/i2c-1

Minimum firmware version per board, since the command is not in older firmware:

Board ENTER_BOOTLOADER from
DI16ac 3.1.1 (3.1.0 answered the command but did not enter the bootloader)
DI6acDQ6rly 3.1.0
DQ5rly 1.3.0
DQ10rly 2.4.0

Notes:

  • A board sitting in the bootloader returns to its application on stm32flash -g 0, or after a full I2C bus scan.
  • Do not run i2cdetect between entering the bootloader and flashing — a scan can return the board to the application mid-procedure.
  • Disconnect whatever is wired to the inputs and outputs first: the outputs are reset while the new firmware starts.

Configuration signature(from library v3.1.0)

config_signature (CiA 301 0x1020) turns "is this board still configured the way I left it?" into a single read. The controller writes any token it likes after a successful commissioning pass; the firmware voids it to 0 the moment any other persistent value really changes, so a surviving token proves the whole stored configuration is intact. Re-writing identical values keeps it.

from raspihats.i2c_hats import DI6acDQ6rly

PROJECT_REVISION = 0x00010004
board = DI6acDQ6rly(0x60)

if board.config_signature != PROJECT_REVISION:
    # full reconcile - every persistent register read, compared, written if different
    board.di.polarity = 0x00
    board.di.filters[0] = 5
    board.dq.safety_mask = 0x3F
    board.dq.safety_value = 0x00
    board.cwdt.period = 2
    board.config_signature = PROJECT_REVISION   # claim it, last

Steady state then costs one read instead of a dozen. restore_factory_defaults() wipes the signature along with everything else, and 0 means "no claim" — a board that was never commissioned and one whose configuration moved read the same.

Listing attributes and methods(from v2.0.0)

from raspihats.i2c_hats import DI6acDQ6rly

board = DI6acDQ6rly(0x60)     # 0x60 is the I2C bus address

board.name                    # get board name, in this case 'DI6acDQ6rly'
board.status.value            # get status word
board.reset()                 # reset board
board.restore_factory_defaults()  # restore factory defaults (CiA 301 0x1011): formats the EEPROM and resets, every persistent register falls back to its default
board.enter_bootloader()      # reset into the ROM bootloader for jumperless flashing (firmware >= the 3.1.0 series); board re-enumerates at 0x3E (F0) / 0x56 (G0)
# ConfigSignature (CiA 301 0x1020) -- controller-owned token, persistent; the firmware voids it to 0 when any OTHER persistent value really changes
board.config_signature        # get the stored signature, 0 = no claim
board.config_signature = 1    # set it after a successful reconcile; identical re-writes of other values keep it

# cwdt - Communication WatchDog Timer
board.cwdt.period             # get CommunicationWatchDogTimer(CWDT) period
board.cwdt.period = 1         # set CWDT period, any value greather than 0 enables the CWDT
board.cwdt.period = 0         # 0 disables the CWDT

# di - Digital Inputs
board.di.value                # get all digital input channel states, bit 0 represents channel 0 state and so on ..
board.di.channels[0]          # get digital input channel 0 state, access using channel index
board.di.channels['I0']       # get digital input channel 0 state, access using channel label
board.di.r_counters[0]        # get digital input channel 0 rising edge counter
board.di.r_counters['I0']     # get digital input channel 0 rising edge counter
board.di.r_counters[0] = 0    # reset digital input channel 0 rising edge counter
board.di.r_counters['I0'] = 0 # reset digital input channel 0 rising edge counter
board.di.f_counters[0]        # get digital input channel 0 falling edge counter
board.di.f_counters['I0']     # get digital input channel 0 falling edge counter
board.di.f_counters[0] = 0    # reset digital input channel 0 falling edge counter
board.di.f_counters['I0'] = 0 # reset digital input channel 0 falling edge counter
board.di.reset_counters()     # reset all counters(rising and falling edge) for all channels
board.di.labels               # get digital input labels
# Polarity (CiA 401 0x6002) -- per-bit invert applied before the filter, persistent
board.di.polarity             # get digital input polarity, bit 0 represents channel 0 and so on ..
board.di.polarity = 0x01      # set digital input polarity, channel 0 inverted
# Filters (CiA 401 0x6003) -- per-channel filter time in milliseconds, persistent
board.di.filters[0]           # get digital input channel 0 filter time in ms
board.di.filters[0] = 2       # set digital input channel 0 filter time in ms, 1..65535
# IRQ block (firmware >= 3.0.0) -- see the IRQ feature section above
board.di.irq_reg.rising_edge_control     # get/set rising edge mask (CiA 401 0x6007), persistent
board.di.irq_reg.falling_edge_control    # get/set falling edge mask (CiA 401 0x6008), persistent
board.di.irq_reg.capture                 # read one capture entry, 0 = queue empty; write 0 to clear the queue
board.di.irq_reg.global_enable           # volatile arming bit (CiA 401 0x6005), 0 after reset; write 0 to disarm

# dq - Digital Outputs
board.dq.value                # get all digital output channel states, bit 0 represents channel 0 and so on ..
board.dq.value = 0            # set all digital output channel states
board.dq.channels[0]          # get digital output channel 0 state, access using channel index
board.dq.channels[0] = 0      # set digital output channel 0 state
board.dq.channels['Q0']       # get digital output channel 0 state, access using channel label
board.dq.channels['Q0'] = 0   # set digital output channel 0 state
# PowerOnValue -- loaded to Digital Outputs at board power on
board.dq.power_on_value       # get digital output channels PowerOnValue, bit 0 represents channel 0 and so on ..
board.dq.power_on_value = 0   # set digital output channels PowerOnValue
# SafetyValue -- loaded to Digital Outputs at CWDT timeout
board.dq.safety_value         # get digital output channels SafetyValue, bit 0 represents channel 0 and so on ..
board.dq.safety_value = 0     # set digital output channels SafetyValue
# SafetyMask (CiA 401 0x6206) -- per bit: 1 = load the SafetyValue at CWDT timeout, 0 = hold last state, persistent
board.dq.safety_mask          # get digital output channels SafetyMask
board.dq.safety_mask = 0x3F   # set digital output channels SafetyMask, all channels apply the SafetyValue
# Polarity (CiA 401 0x6202) -- per-bit invert applied at the pin, the bus value stays logical, persistent
board.dq.polarity             # get digital output polarity, bit 0 represents channel 0 and so on ..
board.dq.polarity = 0         # set digital output polarity, careful: writing this flips live pins instantly
# WriteMask (CiA 401 0x6208, firmware >= the 3.1.0 series) -- gates BULK writes only; single-channel writes bypass it; VOLATILE: all-ones after every reset
board.dq.write_mask           # get the write mask
board.dq.write_mask = 0x3E    # bulk writes no longer touch channel 0 - it belongs to whoever drives dq.channels[0]
board.dq.labels               # get digital output labels

Change Log

v3.2.0

  • raspihats.protocol: a public, semver-stable wire-protocol surface. Command, Frame, DecodeException and crc16_modbus, plus the protocol semantics that were previously buried in private modules — StatusWordBits, IrqRegister, CounterType, the CWDT wire unit and limits, and the 'load' / 'boot' guard payloads. It performs no I/O and does not import smbus2, so it can be used off-machine and by other transports. See docs/protocol.rst for the transport contract and the stability commitment.
  • RESPONSE_DATA_SIZE: the per-command response payload size, previously scattered as literals through the code. A transport has to know the size before it reads, so this is part of the contract.
  • BOARDS / BoardInfo: inert per-model capability data — reported name, base address, address range, per-section channel labels, has_cwdt, has_irq. Usable without a bus, which is what validating a configuration before touching hardware needs. The board classes now take their address, name and labels from this table, so the two cannot drift apart.
  • Fix: Frame.decode() raises DecodeException instead of IndexError on a short read, so a truncated response is retried like any other bad frame rather than escaping the retry loop as a crash.
  • Fix: the I2C address range check accepted addresses outside the family window — a bitwise test let 0x70 pass for a 0x50-based board.
  • Fix: fw_version rendered any component above 9 as punctuation; v2.10.0 came out as v2:0.
  • Fix: set_i2c_port() imported smbus, which is not a declared dependency, and did not take effect; it now uses smbus2 and replaces the open bus.
  • Fix: cwdt.period accepted values that overflow the u32 wire field.
  • Fix: Frame's payload default was a shared mutable list, and an invalid channel index raised a TypeError while building its own error message.
  • tests/: a unit suite for the protocol surface — golden frame vectors cross-checked against an independent CRC implementation, the decode failure modes, and the board data. No hardware needed.
  • raspihats.i2c_hats._frame is now a compatibility re-export; StatusWord.Bits and Irq.RegName are aliases of the public enums. Nothing existing breaks.

v3.1.0

  • enter_bootloader() (0x19): resets the board into its ROM system bootloader over I2C, so firmware can be updated in place without fitting the BOOT jumper — see the section above for the procedure and the minimum firmware version per board
  • config_signature (CiA 301 0x1020): controller-owned persistent token, voided by the firmware whenever another persistent value really changes, so a steady-state configuration check is one read
  • dq.write_mask (CiA 401 0x6208): gates bulk output writes only — single-channel writes bypass it and the CWDT safety behavior stays governed by dq.safety_mask; volatile, all-ones after every board reset
  • robotframework keywords for all three
  • README: worked examples for jumperless firmware update and for the configuration-signature reconcile

v3.0.0

  • CiA 401/301 alignment attributes: di.polarity (0x6002), di.filters (0x6003), dq.polarity (0x6202), dq.safety_mask (0x6206), restore_factory_defaults() (0x1011)
  • IRQ block support for DI-board firmware 3.0.0: di.irq_reg.global_enable (CiA 401 0x6005) plus the matching robotframework keywords
  • README: IRQ examples for both firmware generations (single-threaded wait_for_edge loop for firmware >= 3.0.0, callback + queue pattern for older firmware), attribute listing extended with the CiA registers

v2.5.0

  • Added support for new board, DQ5rly I2C-HAT

v2.4.0

  • Switched to smbus2 to communicate over I2C
  • Removed I2C clock stretching timeout script, RaspberryPi OS sets by default an acceptable value for the I2C clock stretching timeout.
  • Added support for new boards:
    • DI6acDQ6ssr I2C-HAT
    • DI6dwDQ6ssr I2C-HAT

v2.3.0

  • Added IRQ support

v2.2.3

  • enum34 is loaded for python<3.4
  • Setup script warning if it's not run with sudo(used to setup I2C ClockStretchTimeout)

v2.2.2

  • Bug fix in setup script, BCM2835 platform hardware is now recognized.
  • Bug fix in robotframework interface, status.value is now returned by get_status()

v2.2.1

  • Added StatusWord class. To get raw int value use board.status.value, to get beautiful string representation use str(board.status).

v2.1.1

  • String representation of I2CHat object doesn't use an I2C bus transfer any more.
  • Improved exception messages

v2.1.0

  • Improved exception handling

v2.0.1

  • Fixed I2C clock stretch timeout setup script

v2.0.0

v1.1.1

  • Added support for new boards:
    • Di16
    • Rly10
    • Di6Rly6

Installation

$ pip install raspihats

Checkout raspihats.com!

Metadata

Release files for raspihats 3.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for raspihats 3.2.0
File Size Uploaded
raspihats-3.2.0.tar.gz 39.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for raspihats 3.2.0
File Interpreter ABI Platform
raspihats-3.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 67.5 kB

Release files / raspihats-3.2.0.tar.gz

Download URL raspihats-3.2.0.tar.gz
Size 39.2 kB
Tags Source
SHA-256 checksum
How to use checksums
95318c89637b8ce28ba9cf908c45258d3617cb192c23a48194dc2c5483364251
BLAKE2b-256 checksum
How to use checksums
73df48354831a3055fe63346a1608641726ccdb330ff81c2e495e84cfe49255a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.12

Release files / raspihats-3.2.0-py3-none-any.whl

Download URL raspihats-3.2.0-py3-none-any.whl
Size 28.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
07bcca43d829cd0803dedf1c662b66ca16c6944b651e20e8df68c3e35059bf84
BLAKE2b-256 checksum
How to use checksums
608fb31fec7a119f6c4177b4d1c000168f366109d9b2eec4da548973d5d31ffe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.12

Release history Release notifications | RSS feed

This release

3.2.0 This release

2 release files

3.1.0

2 release files

3.0.0

2 release files

2.5.0

2 release files

2.4.0

2 release files

2.3.0

1 release file

2.2.3

1 release file

2.2.2

1 release file

2.2.1

1 release file

2.1.1

1 release file

2.1.0

1 release file

2.0.1

1 release file

2.0.0

1 release file

1.1.1

1 release file

1.1.0

1 release file

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