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
i2cdetectbetween 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,DecodeExceptionandcrc16_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 importsmbus2, so it can be used off-machine and by other transports. Seedocs/protocol.rstfor 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()raisesDecodeExceptioninstead ofIndexErroron 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
0x70pass for a0x50-based board. - Fix:
fw_versionrendered any component above 9 as punctuation; v2.10.0 came out asv2:0. - Fix:
set_i2c_port()importedsmbus, which is not a declared dependency, and did not take effect; it now usessmbus2and replaces the open bus. - Fix:
cwdt.periodaccepted values that overflow the u32 wire field. - Fix:
Frame's payload default was a shared mutable list, and an invalid channel index raised aTypeErrorwhile 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._frameis now a compatibility re-export;StatusWord.BitsandIrq.RegNameare 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 boardconfig_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 readdq.write_mask(CiA 401 0x6208): gates bulk output writes only — single-channel writes bypass it and the CWDT safety behavior stays governed bydq.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_edgeloop 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
- Attributes are now used for accessing board parameters, rather then methods
- Added support for new boards:
- DI16ac I2C-HAT (replacement for Di16 I2C-HAT)
- DQ10rly I2C-HAT (replacement for Rly10 I2C-HAT)
- DQ16oc I2C-HAT
- DI6acDQ6rly I2C-HAT (replacement for Di6Rly6 I2C-HAT)
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)
| File | Size | Uploaded | |
|---|---|---|---|
| raspihats-3.2.0.tar.gz | 39.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|