Source code for herosdevices.hardware.raspberrypi.max318xx.max318xx
"""
Implements the MAX31865 resistance-to-digital converter interface.
The MAX31865 chip communicates via SPI, and in this implementation, SPI communication
is carried out through bit-banging using the RPi.GPIO library. For better performance,
using an SPI library such as spidev is recommended, but this code is a first approach.
Note: numpy is only required if implementing the full Calendar-Van Dusen equations.
This code is adapted from steve71's work: https://github.com/steve71/MAX31865
"""
# system packages
from abc import abstractmethod
from herosdevices.helper import log
# rpi specific imports
try:
from RPi import GPIO
except ImportError:
log.warning("could not import RPi.GPIO")
from herosdevices.helper import log
[docs]
class FaultError(Exception):
"""Exception class for handling faults detected in the RTD or wiring."""
[docs]
class MAX318xx:
"""Base class for MAX318xx series resistance-to-digital converters.
Handles SPI communication via bit-banging using GPIO pins.
Attributes:
cs_pin (dict): {"description": "Chip select GPIO pin number"}.
miso_pin (int): Master In Slave Out GPIO pin number.
mosi_pin (int): Master Out Slave In GPIO pin number.
clk_pin (int): Clock GPIO pin number.
"""
def __init__(self, cs_pins: dict, miso_pin: int = 9, mosi_pin: int | None = 10, clk_pin: int = 11) -> None:
"""Initialize MAX318xx object with specified GPIO pins and sets up GPIO.
Args:
cs_pin (dict): {"description": "Chip select GPIO pin number"}.
miso_pin (int): GPIO pin for MISO.
mosi_pin (int): GPIO pin for MOSI.
clk_pin (int): GPIO pin for clock.
"""
# SPI specific
self.cs_pins = cs_pins
self.miso_pin = miso_pin
self.mosi_pin = mosi_pin
self.clk_pin = clk_pin
log.debug(f"CS: {self.cs_pins}\tMISO: {self.miso_pin}\tMOSI: {self.mosi_pin}\tCLK: {self.clk_pin}")
log.debug(f"CS: {self.cs_pins}\tMISO: {self.miso_pin}\tMOSI: {self.mosi_pin}\tCLK: {self.clk_pin}")
self._setup_gpio()
def _setup_gpio(self) -> None:
"""Configure GPIO pins for SPI communication."""
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
for cs_pin in self.cs_pins.values():
GPIO.setup(cs_pin, GPIO.OUT)
GPIO.output(cs_pin, GPIO.HIGH)
GPIO.setup(self.miso_pin, GPIO.IN)
GPIO.setup(self.clk_pin, GPIO.OUT)
GPIO.output(self.clk_pin, GPIO.LOW)
if self.mosi_pin is not None:
GPIO.setup(self.mosi_pin, GPIO.OUT)
GPIO.output(self.mosi_pin, GPIO.LOW)
[docs]
def write_register(self, reg_num: int, data_byte: int) -> None:
"""Write a byte to a specified register.
Args:
regNum (int): Register number to write to.
dataByte (int): Data byte to write.
"""
GPIO.output(self.cs_pins, GPIO.LOW) # TODO: self.cs_pins is a dict but this needs a single int...
GPIO.output(self.cs_pins, GPIO.LOW)
# 0x8x to specify 'write register value'
address_byte = 0x80 | reg_num
# first byte is address byte
self.send_byte(address_byte)
# the rest are data bytes
self.send_byte(data_byte)
GPIO.output(self.cs_pins, GPIO.HIGH)
GPIO.output(self.cs_pins, GPIO.HIGH)
[docs]
def read_register(self, cs_pin: int, num_registers: int, reg_num_start: int | None = None) -> list[int]:
"""Read one or more bytes starting from a specified register.
Args:
cs_pin (int): Chip select GPIO pin number.
numRegisters (int): Number of bytes to read.
regNumStart (int, optional): Starting register number for the read operation.
Returns:
list: List of bytes read from the device.
"""
out = []
GPIO.output(cs_pin, GPIO.LOW)
GPIO.output(cs_pin, GPIO.LOW)
# 0x to specify 'read register value'
if self.mosi_pin is not None:
# TODO: this can be none, add doc that required if mosi_pin is not None
self.send_byte(reg_num_start)
for _ in range(num_registers):
data = self.recv_byte()
out.append(data)
GPIO.output(cs_pin, GPIO.HIGH)
GPIO.output(cs_pin, GPIO.HIGH)
return out
[docs]
def send_byte(self, byte: int) -> None:
"""Send a byte via SPI by bit-banging.
Args:
byte (int): Byte value to send.
"""
for _ in range(8):
GPIO.output(self.clk_pin, GPIO.HIGH)
if byte & 0x80:
GPIO.output(self.mosi_pin, GPIO.HIGH)
else:
GPIO.output(self.mosi_pin, GPIO.LOW)
byte <<= 1
GPIO.output(self.clk_pin, GPIO.LOW)
[docs]
def recv_byte(self) -> int:
"""Receive a byte via SPI by bit-banging.
Returns:
int: Byte received.
"""
byte = 0x00
for _ in range(8):
GPIO.output(self.clk_pin, GPIO.HIGH)
byte <<= 1
if GPIO.input(self.miso_pin):
byte |= 0x1
GPIO.output(self.clk_pin, GPIO.LOW)
return byte
[docs]
def create_dict(self, values: list[float], observable: str, unit: str) -> dict:
"""Build dictionary of observables to be returned by the sensor.""" # TODO: better docstring
if len(values) != len(self.cs_pins):
raise RuntimeWarning("Sensorlist length does not match number of measured values")
description = [key + f"_{observable}" for key in self.cs_pins]
return {f"{key}": (val, unit) for key, val in zip(description, values, strict=True)}
@abstractmethod
def _observable_data(self) -> dict:
"""Abstract method to check if new data is available.
This method should be implemented in subclasses.
Raises:
NotImplementedError: If not implemented in subclass.
returnstyle: {"data_name": (data, "Unit")}
"""