Files
brewpi/components/sensor/tempSensor_max31865.py
jens a1864a5257 log: route task/component status output through logging, not print()
AttributeChange (the common base of every ATask and component ABC) now
sets self.log = logging.getLogger(type(self).__name__), so components
no longer need to hand-type their own name into each message. Wired
logging.basicConfig() in server/brewpi.py with a bare "%(name)s:
%(message)s" formatter - Tee (see prior commit) still supplies the
"<date>T<time>:" prefix, so lines read "<date>T<time>:<component>:
<message>" without double-stamping, and third-party loggers
(websockets, asyncio) now get the same formatting for free.

Converted the print() call sites that were standing in for this in
tasks/ and components/ (leaving __main__ demo blocks and explicit
debug-dump helpers alone).
2026-07-10 22:00:04 +02:00

182 lines
4.6 KiB
Python
Executable File

#!/usr/bin/env python3
import time
from components import ATemperatureSensor, TemperatureSensorException
import spidev
class TempSensor_max31865(ATemperatureSensor):
def name(self):
return "Max31865"
def __init__(self, temp_offset=0, **kwargs):
ATemperatureSensor.__init__(self)
self.temp_offset = temp_offset
# Set before the first read attempt so a read failure even on the
# very first call (temperature() below, or TempSensorTask's own
# priming call) has a well-defined last-known-good value to fall
# back to rather than raising AttributeError.
self.temp = None
self.spi = spidev.SpiDev()
try:
self._open_spi()
except Exception as e:
# Don't crash the whole server if the SPI bus is momentarily
# busy/unavailable at startup (e.g. right after a Pi reboot) -
# temperature()'s own except handler already retries via
# reopen() on the next tick, same recovery path as a later
# runtime failure.
self.log.error(f"could not open SPI at startup: {e}")
def _open_spi(self):
self.spi.open(0, 0)
self.spi.max_speed_hz = 50000
self.spi.mode = 0b01
self.write_reg(0x00, 0xA3)
def reopen(self):
"""Closes and reopens the SPI handle - recovers from a wedged bus/
chip-select state (e.g. left mid-transaction by some other fault)
without needing a full process or Pi reboot. There's no serial-
style connect/disconnect concept for a bus peripheral like this
one, so this is the closest equivalent - called automatically by
temperature() below on a read failure.
Never raises - it's itself the recovery path called from
temperature()'s except handler, and if it raised too that would
escape temperature() entirely (same class of bug as HendiCtrl.
disconnect() previously letting a comm failure escape and
permanently kill HeaterTask's coroutine - see tasks/heater.py's
on_process()). A genuinely still-broken bus just leaves self.temp
at its last-known-good value, logged, and gets retried again next
tick."""
try:
self.spi.close()
except Exception:
pass
try:
self._open_spi()
except Exception as e:
self.log.error(f"reopen failed: {e}")
def read_reg(self, addr):
reg = self.spi.xfer([addr, 0xFF])
return reg[1]
def write_reg(self, addr, data):
self.spi.xfer([addr+0x80, data])
def read_digits(self):
for i in range(0, 10):
reg = self.read_reg(0x00)
if (0x20 & reg) == 0:
break
time.sleep(0.01)
digits = 0
msb = self.read_reg(0x01)
lsb = self.read_reg(0x02)
self.write_reg(0x00, 0xA3)
if lsb & 0x01 == 0x00:
digits = (256*msb + lsb) >> 1
else:
raise TemperatureSensorException("Could not read sensor!")
return digits
def temperature(self):
try:
digits = self.read_digits()
self.temp = self.temp_offset + self.to_temperature(digits)
except Exception as e:
# Holds the last-known-good self.temp rather than raising -
# TempSensorTask.on_process() calls this every tick with no
# guard of its own (mirrors HeaterHendi.process()'s pattern of
# self-healing at the actor level rather than letting a comm
# failure escape and permanently kill the task's coroutine -
# nothing restarts a dead ATask). reopen() gives the SPI bus a
# chance to recover from a wedged state without a full process/
# Pi reboot - see docs/pi_deployment_notes.md.
self.log.error(f"comm error reading sensor, reopening SPI: {e}")
self.reopen()
return self.temp
@staticmethod
def to_temperature(digits):
T = 0
R = __class__.calc_R(430, digits)
T = __class__.vanDusen_temp(R)
return T
@staticmethod
def calc_R(Rref, digits):
k = float(digits) / 32768
R = k*Rref
return R
@staticmethod
def vanDusen_temp(Rmeas):
RLut, TLut = __class__.vanDusenLut(100, -150, +500, 1.0)
# Find candidate
i = 0
for R in RLut:
if R > Rmeas:
break
i += 1
# Linear interpolation
R0 = RLut[i-1]
R1 = RLut[i]
T0 = TLut[i-1]
T1 = TLut[i]
dR = R1 - R0
dT = T1 - T0
k = (Rmeas-R0)/dR
T = T0 + dT*k
return T
@staticmethod
def vanDusenLut (R0, Tmin, Tmax, dT):
a = +3.90830e-03
b = -5.77500e-07
c = -4.18301e-12
Rv = []
Tv = []
T = Tmin
while T <= Tmax:
R = R0*(1 + a*T + b*T**2)
if T < 0.0:
R += R0*c*(T - 100)*T**3
Rv.append(R)
Tv.append(T)
T += dT
return Rv, Tv
# Main
if __name__ == '__main__':
sensor = TempSensor_max31865()
for k in range (0, 100):
for i in range (0, 8):
print("Reg[0x{}]: 0x{:02X}".format(i, sensor.read_reg(i)))
digits = sensor.read_digits()
print("Digits = {:04X}".format(digits))
print("Temperature = {:0.2f} °C".format(TempSensor_max31865.to_temperature(digits)))
time.sleep(0.5)
print("End of program")