""" Temperatue gun sensor module using the MLX90640-D55/D110 sensor. This module provides a function to read the temperature from the gun sensor. Resolution of 32x24 pixels, I2C interface Noise Equivalent Temperature difference (NETD) is 0.1K RMS @ 1Hz refresh rate """ import machine # type: ignore import math import struct import time from micropython import const# Some libraries that we will use import time class RefreshRate: # pylint: disable=too-few-public-methods """ Enum-like class for MLX90640's refresh rate """ REFRESH_0_5_HZ = const(0b000) # 0.5Hz REFRESH_1_HZ = const(0b001) # 1Hz REFRESH_2_HZ = const(0b010) # 2Hz REFRESH_4_HZ = const(0b011) # 4Hz REFRESH_8_HZ = const(0b100) # 8Hz REFRESH_16_HZ = const(0b101) # 16Hz REFRESH_32_HZ = const(0b110) # 32Hz REFRESH_64_HZ = const(0b111) # 64Hz class ContextManaged: """An object that automatically deinitializes hardware with a context manager.""" def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.deinit() # pylint: disable=no-self-use def deinit(self): """Free any hardware used by the object.""" return class Lockable(ContextManaged): """An object that must be locked to prevent collisions on a microcontroller resource.""" _locked = False def try_lock(self): """Attempt to grab the lock. Return True on success, False if the lock is already taken.""" if self._locked: return False self._locked = True return True def unlock(self): """Release the lock so others may use the resource.""" if self._locked: self._locked = False else: raise ValueError("Not locked") class I2C(Lockable): def __init__(self, pins=(21, 22), frequency=100000): self.init(pins, frequency) def init(self, pins, frequency): self.deinit() # 1. Force the ESP32 to activate its internal pull-up resistors on these pins self._pins = ( machine.Pin(int(pins[0]), machine.Pin.IN, machine.Pin.PULL_UP), machine.Pin(int(pins[1]), machine.Pin.IN, machine.Pin.PULL_UP) ) try: # 2. Bypasses the glitchy ESP32 hardware block using SoftI2C # (Note: SoftI2C does not take a bus ID number like '0') self._i2c = machine.SoftI2C(scl=self._pins[0], sda=self._pins[1], freq=frequency) except RuntimeError: raise print(f"Created resilient SoftI2C: {self._i2c}") def deinit(self): try: del self._i2c except AttributeError: pass def scan(self): return self._i2c.scan() def readfrom_into(self, address, buffer, *, start=0, end=None): if start is not 0 or end is not None: if end is None: end = len(buffer) buffer = memoryview(buffer)[start:end] stop = True # remove for efficiency later return self._i2c.readfrom_into(address, buffer) def writeto(self, address, buffer, *, start=0, end=None, stop=True): if isinstance(buffer, str): buffer = bytes([ord(x) for x in buffer]) if start is not 0 or end is not None: if end is None: return self._i2c.writeto(address, memoryview(buffer)[start:], stop) else: return self._i2c.writeto(address, memoryview(buffer)[start:end], stop) return self._i2c.writeto(address, buffer, stop) class I2CDevice: def __init__(self, i2c, device_address, probe=True): self.i2c = i2c self._has_write_read = False # hasattr(self.i2c, "writeto_then_readfrom") --> has been turned to False self.device_address = device_address if probe: self.__probe_for_device() def readinto(self, buf, *, start=0, end=None): if end is None: end = len(buf) self.i2c.readfrom_into(self.device_address, buf, start=start, end=end) def write(self, buf, *, start=0, end=None, stop=True): if end is None: end = len(buf) self.i2c.writeto(self.device_address, buf, start=start, end=end, stop=stop) # pylint: disable-msg=too-many-arguments def write_then_readinto( self, out_buffer, in_buffer, *, out_start=0, out_end=None, in_start=0, in_end=None, stop=False ): if out_end is None: out_end = len(out_buffer) if in_end is None: in_end = len(in_buffer) if stop: raise ValueError("Stop must be False. Use writeto instead.") if self._has_write_read: #print("c",dir(self.i2c)) # In linux, at least, this is a special kernel function call self.i2c.writeto_then_readfrom( self.device_address, out_buffer, in_buffer, out_start=out_start, out_end=out_end, in_start=in_start, in_end=in_end, ) else: # If we don't have a special implementation, we can fake it with two calls self.i2c.writeto(self.device_address, out_buffer, stop=False) # These lines have been changed to make it work with wipy micropython I2C module #self.write(out_buffer, start=out_start, end=out_end, stop=False) #self.readinto(in_buffer, start=in_start, end=in_end) self.i2c.readfrom_into(self.device_address, in_buffer) # These lines have been changed to make it work with wipy micropython I2C module # pylint: enable-msg=too-many-arguments def __enter__(self): while not self.i2c.try_lock(): pass return self def __exit__(self, exc_type, exc_val, exc_tb): self.i2c.unlock() return False def __probe_for_device(self): """ Try to read a byte from an address, if you get an OSError it means the device is not there or that the device does not support these means of probing """ while not self.i2c.try_lock(): pass try: self.i2c.writeto(self.device_address, b"") except OSError: # some OS's dont like writing an empty bytesting... # Retry by reading a byte try: result = bytearray(1) self.i2c.readfrom_into(self.device_address, result) except OSError: raise ValueError("No I2C device at address: %x" % self.device_address) finally: self.i2c.unlock() eeData = [0] * const(832) I2C_READ_LEN = const(2048) SCALEALPHA = const(0.000001) MLX90640_DEVICEID1 = const(0x2407) OPENAIR_TA_SHIFT = const(8) class MLX90640: # pylint: disable=too-many-instance-attributes """Interface to the MLX90640 temperature sensor.""" kVdd = 0 vdd25 = 0 KvPTAT = 0 KtPTAT = 0 vPTAT25 = 0 alphaPTAT = 0 gainEE = 0 tgc = 0 KsTa = 0 resolutionEE = 0 calibrationModeEE = 0 ksTo = [0] * 5 ct = [0] * 5 alpha = [0] * 768 alphaScale = 0 offset = [0] * 768 kta = [0] * 768 ktaScale = 0 kv = [0] * 768 kvScale = 0 cpAlpha = [0] * 2 cpOffset = [0] * 2 ilChessC = [0] * 3 brokenPixels = [0xFFFF] * 5 outlierPixels = [0xFFFF] * 5 cpKta = 0 cpKv = 0 def __init__(self, i2c_bus, address=0x33): self.i2c_device = I2CDevice(i2c_bus, address) self._I2CReadWords(0x2400, eeData) # print(eeData) self._ExtractParameters() @property def serial_number(self): """ 3-item tuple of hex values that are unique to each MLX90640 """ serialWords = [0, 0, 0] self._I2CReadWords(MLX90640_DEVICEID1, serialWords) return serialWords @property def refresh_rate(self): """ How fast the MLX90640 will spit out data. Start at lowest speed in RefreshRate and then slowly increase I2C clock rate and rate until you max out. The sensor does not like it if the I2C host cannot 'keep up'!""" controlRegister = [0] self._I2CReadWords(0x800D, controlRegister) return (controlRegister[0] >> 7) & 0x07 @refresh_rate.setter def refresh_rate(self, rate): controlRegister = [0] value = (rate & 0x7) << 7 self._I2CReadWords(0x800D, controlRegister) value |= controlRegister[0] & 0xFC7F self._I2CWriteWord(0x800D, value) def getFrame(self, framebuf): """ Request both 'halves' of a frame from the sensor, merge them and calculate the temperature in C for each of 32x24 pixels. Placed into the 768-element array passed in! """ emissivity = 0.95 tr = 23.15 mlx90640Frame = [0] * 834 for _ in range(2): status = self._GetFrameData(mlx90640Frame) if status < 0: raise RuntimeError("Frame data error") # For a MLX90640 in the open air the shift is -8 degC. tr = self._GetTa(mlx90640Frame) - OPENAIR_TA_SHIFT self._CalculateTo(mlx90640Frame, emissivity, tr, framebuf) def _GetFrameData(self, frameData): dataReady = 0 cnt = 0 statusRegister = [0] controlRegister = [0] while dataReady == 0: self._I2CReadWords(0x8000, statusRegister) dataReady = statusRegister[0] & 0x0008 # print("ready status: 0x%x" % dataReady) while (dataReady != 0) and (cnt < 5): self._I2CWriteWord(0x8000, 0x0030) # print("Read frame", cnt) self._I2CReadWords(0x0400, frameData, end=832) self._I2CReadWords(0x8000, statusRegister) dataReady = statusRegister[0] & 0x0008 # print("frame ready: 0x%x" % dataReady) cnt += 1 if cnt > 4: raise RuntimeError("Too many retries") self._I2CReadWords(0x800D, controlRegister) frameData[832] = controlRegister[0] frameData[833] = statusRegister[0] & 0x0001 return frameData[833] def _GetTa(self, frameData): vdd = self._GetVdd(frameData) ptat = frameData[800] if ptat > 32767: ptat -= 65536 ptatArt = frameData[768] if ptatArt > 32767: ptatArt -= 65536 ptatArt = (ptat / (ptat * self.alphaPTAT + ptatArt)) * math.pow(2, 18) ta = ptatArt / (1 + self.KvPTAT * (vdd - 3.3)) - self.vPTAT25 ta = ta / self.KtPTAT + 25 return ta def _GetVdd(self, frameData): vdd = frameData[810] if vdd > 32767: vdd -= 65536 resolutionRAM = (frameData[832] & 0x0C00) >> 10 resolutionCorrection = math.pow(2, self.resolutionEE) / math.pow( 2, resolutionRAM ) vdd = (resolutionCorrection * vdd - self.vdd25) / self.kVdd + 3.3 return vdd def _CalculateTo(self, frameData, emissivity, tr, result): # pylint: disable=too-many-locals, too-many-branches, too-many-statements subPage = frameData[833] alphaCorrR = [0] * 4 irDataCP = [0, 0] vdd = self._GetVdd(frameData) ta = self._GetTa(frameData) ta4 = ta + 273.15 ta4 = ta4 * ta4 ta4 = ta4 * ta4 tr4 = tr + 273.15 tr4 = tr4 * tr4 tr4 = tr4 * tr4 taTr = tr4 - (tr4 - ta4) / emissivity ktaScale = math.pow(2, self.ktaScale) kvScale = math.pow(2, self.kvScale) alphaScale = math.pow(2, self.alphaScale) alphaCorrR[0] = 1 / (1 + self.ksTo[0] * 40) alphaCorrR[1] = 1 alphaCorrR[2] = 1 + self.ksTo[1] * self.ct[2] alphaCorrR[3] = alphaCorrR[2] * (1 + self.ksTo[2] * (self.ct[3] - self.ct[2])) # --------- Gain calculation ----------------------------------- gain = frameData[778] if gain > 32767: gain -= 65536 gain = self.gainEE / gain # --------- To calculation ------------------------------------- mode = (frameData[832] & 0x1000) >> 5 irDataCP[0] = frameData[776] irDataCP[1] = frameData[808] for i in range(2): if irDataCP[i] > 32767: irDataCP[i] -= 65536 irDataCP[i] *= gain irDataCP[0] -= ( self.cpOffset[0] * (1 + self.cpKta * (ta - 25)) * (1 + self.cpKv * (vdd - 3.3)) ) if mode == self.calibrationModeEE: irDataCP[1] -= ( self.cpOffset[1] * (1 + self.cpKta * (ta - 25)) * (1 + self.cpKv * (vdd - 3.3)) ) else: irDataCP[1] -= ( (self.cpOffset[1] + self.ilChessC[0]) * (1 + self.cpKta * (ta - 25)) * (1 + self.cpKv * (vdd - 3.3)) ) for pixelNumber in range(768): ilPattern = pixelNumber // 32 - (pixelNumber // 64) * 2 chessPattern = ilPattern ^ (pixelNumber - (pixelNumber // 2) * 2) conversionPattern = ( (pixelNumber + 2) // 4 - (pixelNumber + 3) // 4 + (pixelNumber + 1) // 4 - pixelNumber // 4 ) * (1 - 2 * ilPattern) if mode == 0: pattern = ilPattern else: pattern = chessPattern if pattern == frameData[833]: irData = frameData[pixelNumber] if irData > 32767: irData -= 65536 irData *= gain kta = self.kta[pixelNumber] / ktaScale kv = self.kv[pixelNumber] / kvScale irData -= ( self.offset[pixelNumber] * (1 + kta * (ta - 25)) * (1 + kv * (vdd - 3.3)) ) if mode != self.calibrationModeEE: irData += ( self.ilChessC[2] * (2 * ilPattern - 1) - self.ilChessC[1] * conversionPattern ) irData = irData - self.tgc * irDataCP[subPage] irData /= emissivity alphaCompensated = SCALEALPHA * alphaScale / self.alpha[pixelNumber] alphaCompensated *= 1 + self.KsTa * (ta - 25) Sx = ( alphaCompensated * alphaCompensated * alphaCompensated * (irData + alphaCompensated * taTr) ) Sx = math.sqrt(math.sqrt(Sx)) * self.ksTo[1] To = ( math.sqrt( math.sqrt( irData / (alphaCompensated * (1 - self.ksTo[1] * 273.15) + Sx) + taTr ) ) - 273.15 ) if To < self.ct[1]: torange = 0 elif To < self.ct[2]: torange = 1 elif To < self.ct[3]: torange = 2 else: torange = 3 To = ( math.sqrt( math.sqrt( irData / ( alphaCompensated * alphaCorrR[torange] * (1 + self.ksTo[torange] * (To - self.ct[torange])) ) + taTr ) ) - 273.15 ) result[pixelNumber] = To # pylint: enable=too-many-locals, too-many-branches, too-many-statements def _ExtractParameters(self): self._ExtractVDDParameters() self._ExtractPTATParameters() self._ExtractGainParameters() self._ExtractTgcParameters() self._ExtractResolutionParameters() self._ExtractKsTaParameters() self._ExtractKsToParameters() self._ExtractCPParameters() self._ExtractAlphaParameters() self._ExtractOffsetParameters() self._ExtractKtaPixelParameters() self._ExtractKvPixelParameters() self._ExtractCILCParameters() self._ExtractDeviatingPixels() def _ExtractVDDParameters(self): # extract VDD self.kVdd = (eeData[51] & 0xFF00) >> 8 if self.kVdd > 127: self.kVdd -= 256 # convert to signed self.kVdd *= 32 self.vdd25 = eeData[51] & 0x00FF self.vdd25 = ((self.vdd25 - 256) << 5) - 8192 def _ExtractPTATParameters(self): # extract PTAT self.KvPTAT = (eeData[50] & 0xFC00) >> 10 if self.KvPTAT > 31: self.KvPTAT -= 64 self.KvPTAT /= 4096 self.KtPTAT = eeData[50] & 0x03FF if self.KtPTAT > 511: self.KtPTAT -= 1024 self.KtPTAT /= 8 self.vPTAT25 = eeData[49] self.alphaPTAT = (eeData[16] & 0xF000) / math.pow(2, 14) + 8 def _ExtractGainParameters(self): # extract Gain self.gainEE = eeData[48] if self.gainEE > 32767: self.gainEE -= 65536 def _ExtractTgcParameters(self): # extract Tgc #print(eeData[60]) self.tgc = eeData[60] & 0x00FF #print(self.tgc) if self.tgc > 127: self.tgc -= 256 self.tgc /= 32 #print(self.tgc) def _ExtractResolutionParameters(self): # extract resolution self.resolutionEE = (eeData[56] & 0x3000) >> 12 def _ExtractKsTaParameters(self): # extract KsTa self.KsTa = (eeData[60] & 0xFF00) >> 8 if self.KsTa > 127: self.KsTa -= 256 self.KsTa /= 8192 def _ExtractKsToParameters(self): # extract ksTo step = ((eeData[63] & 0x3000) >> 12) * 10 self.ct[0] = -40 self.ct[1] = 0 self.ct[2] = (eeData[63] & 0x00F0) >> 4 self.ct[3] = (eeData[63] & 0x0F00) >> 8 self.ct[2] *= step self.ct[3] = self.ct[2] + self.ct[3] * step KsToScale = (eeData[63] & 0x000F) + 8 KsToScale = 1 << KsToScale self.ksTo[0] = eeData[61] & 0x00FF self.ksTo[1] = (eeData[61] & 0xFF00) >> 8 self.ksTo[2] = eeData[62] & 0x00FF self.ksTo[3] = (eeData[62] & 0xFF00) >> 8 for i in range(4): if self.ksTo[i] > 127: self.ksTo[i] -= 256 self.ksTo[i] /= KsToScale self.ksTo[4] = -0.0002 def _ExtractCPParameters(self): # extract CP offsetSP = [0] * 2 alphaSP = [0] * 2 alphaScale = ((eeData[32] & 0xF000) >> 12) + 27 offsetSP[0] = eeData[58] & 0x03FF if offsetSP[0] > 511: offsetSP[0] -= 1024 offsetSP[1] = (eeData[58] & 0xFC00) >> 10 if offsetSP[1] > 31: offsetSP[1] -= 64 offsetSP[1] += offsetSP[0] alphaSP[0] = eeData[57] & 0x03FF if alphaSP[0] > 511: alphaSP[0] -= 1024 alphaSP[0] /= math.pow(2, alphaScale) alphaSP[1] = (eeData[57] & 0xFC00) >> 10 if alphaSP[1] > 31: alphaSP[1] -= 64 alphaSP[1] = (1 + alphaSP[1] / 128) * alphaSP[0] cpKta = eeData[59] & 0x00FF if cpKta > 127: cpKta -= 256 ktaScale1 = ((eeData[56] & 0x00F0) >> 4) + 8 self.cpKta = cpKta / math.pow(2, ktaScale1) cpKv = (eeData[59] & 0xFF00) >> 8 if cpKv > 127: cpKv -= 256 kvScale = (eeData[56] & 0x0F00) >> 8 self.cpKv = cpKv / math.pow(2, kvScale) self.cpAlpha[0] = alphaSP[0] self.cpAlpha[1] = alphaSP[1] self.cpOffset[0] = offsetSP[0] self.cpOffset[1] = offsetSP[1] #print(self.cpAlpha[0]) #print(self.cpAlpha[1]) def _ExtractAlphaParameters(self): # extract alpha accRemScale = eeData[32] & 0x000F accColumnScale = (eeData[32] & 0x00F0) >> 4 accRowScale = (eeData[32] & 0x0F00) >> 8 alphaScale = ((eeData[32] & 0xF000) >> 12) + 30 alphaRef = eeData[33] accRow = [0] * 24 accColumn = [0] * 32 alphaTemp = [0] * 768 for i in range(6): p = i * 4 accRow[p + 0] = eeData[34 + i] & 0x000F accRow[p + 1] = (eeData[34 + i] & 0x00F0) >> 4 accRow[p + 2] = (eeData[34 + i] & 0x0F00) >> 8 accRow[p + 3] = (eeData[34 + i] & 0xF000) >> 12 for i in range(24): if accRow[i] > 7: accRow[i] -= 16 for i in range(8): p = i * 4 accColumn[p + 0] = eeData[40 + i] & 0x000F accColumn[p + 1] = (eeData[40 + i] & 0x00F0) >> 4 accColumn[p + 2] = (eeData[40 + i] & 0x0F00) >> 8 accColumn[p + 3] = (eeData[40 + i] & 0xF000) >> 12 for i in range(32): if accColumn[i] > 7: accColumn[i] -= 16 for i in range(24): for j in range(32): p = 32 * i + j alphaTemp[p] = (eeData[64 + p] & 0x03F0) >> 4 if alphaTemp[p] > 31: alphaTemp[p] -= 64 alphaTemp[p] *= 1 << accRemScale alphaTemp[p] += ( alphaRef + (accRow[i] << accRowScale) + (accColumn[j] << accColumnScale) ) alphaTemp[p] /= math.pow(2, alphaScale) alphaTemp[p] -= self.tgc * (self.cpAlpha[0] + self.cpAlpha[1]) / 2 alphaTemp[p] = SCALEALPHA / alphaTemp[p] # print("alphaTemp: ", alphaTemp) temp = max(alphaTemp) #print("temp", temp) alphaScale = 0 while temp < 32768: temp *= 2 alphaScale += 1 for i in range(768): temp = alphaTemp[i] * math.pow(2, alphaScale) self.alpha[i] = int(temp + 0.5) self.alphaScale = alphaScale def _ExtractOffsetParameters(self): # extract offset occRow = [0] * 24 occColumn = [0] * 32 occRemScale = eeData[16] & 0x000F occColumnScale = (eeData[16] & 0x00F0) >> 4 occRowScale = (eeData[16] & 0x0F00) >> 8 offsetRef = eeData[17] if offsetRef > 32767: offsetRef -= 65536 for i in range(6): p = i * 4 occRow[p + 0] = eeData[18 + i] & 0x000F occRow[p + 1] = (eeData[18 + i] & 0x00F0) >> 4 occRow[p + 2] = (eeData[18 + i] & 0x0F00) >> 8 occRow[p + 3] = (eeData[18 + i] & 0xF000) >> 12 for i in range(24): if occRow[i] > 7: occRow[i] -= 16 for i in range(8): p = i * 4 occColumn[p + 0] = eeData[24 + i] & 0x000F occColumn[p + 1] = (eeData[24 + i] & 0x00F0) >> 4 occColumn[p + 2] = (eeData[24 + i] & 0x0F00) >> 8 occColumn[p + 3] = (eeData[24 + i] & 0xF000) >> 12 for i in range(32): if occColumn[i] > 7: occColumn[i] -= 16 for i in range(24): for j in range(32): p = 32 * i + j self.offset[p] = (eeData[64 + p] & 0xFC00) >> 10 if self.offset[p] > 31: self.offset[p] -= 64 self.offset[p] *= 1 << occRemScale self.offset[p] += ( offsetRef + (occRow[i] << occRowScale) + (occColumn[j] << occColumnScale) ) def _ExtractKtaPixelParameters(self): # pylint: disable=too-many-locals # extract KtaPixel KtaRC = [0] * 4 ktaTemp = [0] * 768 KtaRoCo = (eeData[54] & 0xFF00) >> 8 if KtaRoCo > 127: KtaRoCo -= 256 KtaRC[0] = KtaRoCo KtaReCo = eeData[54] & 0x00FF if KtaReCo > 127: KtaReCo -= 256 KtaRC[2] = KtaReCo KtaRoCe = (eeData[55] & 0xFF00) >> 8 if KtaRoCe > 127: KtaRoCe -= 256 KtaRC[1] = KtaRoCe KtaReCe = eeData[55] & 0x00FF if KtaReCe > 127: KtaReCe -= 256 KtaRC[3] = KtaReCe ktaScale1 = ((eeData[56] & 0x00F0) >> 4) + 8 ktaScale2 = eeData[56] & 0x000F for i in range(24): for j in range(32): p = 32 * i + j split = 2 * (p // 32 - (p // 64) * 2) + p % 2 ktaTemp[p] = (eeData[64 + p] & 0x000E) >> 1 if ktaTemp[p] > 3: ktaTemp[p] -= 8 ktaTemp[p] *= 1 << ktaScale2 ktaTemp[p] += KtaRC[split] ktaTemp[p] /= math.pow(2, ktaScale1) # ktaTemp[p] = ktaTemp[p] * mlx90640->offset[p]; temp = abs(ktaTemp[0]) for kta in ktaTemp: temp = max(temp, abs(kta)) ktaScale1 = 0 while temp < 64: temp *= 2 ktaScale1 += 1 for i in range(768): temp = ktaTemp[i] * math.pow(2, ktaScale1) if temp < 0: self.kta[i] = int(temp - 0.5) else: self.kta[i] = int(temp + 0.5) self.ktaScale = ktaScale1 def _ExtractKvPixelParameters(self): KvT = [0] * 4 kvTemp = [0] * 768 KvRoCo = (eeData[52] & 0xF000) >> 12 if KvRoCo > 7: KvRoCo -= 16 KvT[0] = KvRoCo KvReCo = (eeData[52] & 0x0F00) >> 8 if KvReCo > 7: KvReCo -= 16 KvT[2] = KvReCo KvRoCe = (eeData[52] & 0x00F0) >> 4 if KvRoCe > 7: KvRoCe -= 16 KvT[1] = KvRoCe KvReCe = eeData[52] & 0x000F if KvReCe > 7: KvReCe -= 16 KvT[3] = KvReCe kvScale = (eeData[56] & 0x0F00) >> 8 for i in range(24): for j in range(32): p = 32 * i + j split = 2 * (p // 32 - (p // 64) * 2) + p % 2 kvTemp[p] = KvT[split] kvTemp[p] /= math.pow(2, kvScale) # kvTemp[p] = kvTemp[p] * mlx90640->offset[p]; temp = abs(kvTemp[0]) for kv in kvTemp: temp = max(temp, abs(kv)) kvScale = 0 while temp < 64: temp *= 2 kvScale += 1 for i in range(768): temp = kvTemp[i] * math.pow(2, kvScale) if temp < 0: self.kv[i] = int(temp - 0.5) else: self.kv[i] = int(temp + 0.5) self.kvScale = kvScale def _ExtractCILCParameters(self): ilChessC = [0] * 3 self.calibrationModeEE = (eeData[10] & 0x0800) >> 4 self.calibrationModeEE = self.calibrationModeEE ^ 0x80 ilChessC[0] = eeData[53] & 0x003F if ilChessC[0] > 31: ilChessC[0] -= 64 ilChessC[0] /= 16.0 ilChessC[1] = (eeData[53] & 0x07C0) >> 6 if ilChessC[1] > 15: ilChessC[1] -= 32 ilChessC[1] /= 2.0 ilChessC[2] = (eeData[53] & 0xF800) >> 11 if ilChessC[2] > 15: ilChessC[2] -= 32 ilChessC[2] /= 8.0 self.ilChessC = ilChessC def _ExtractDeviatingPixels(self): self.brokenPixels = [0xFFFF] * 5 self.outlierPixels = [0xFFFF] * 5 pixCnt = 0 brokenPixCnt = 0 outlierPixCnt = 0 while (pixCnt < 768) and (brokenPixCnt < 5) and (outlierPixCnt < 5): if eeData[pixCnt + 64] == 0: self.brokenPixels[brokenPixCnt] = pixCnt brokenPixCnt += 1 elif (eeData[pixCnt + 64] & 0x0001) != 0: self.outlierPixels[outlierPixCnt] = pixCnt outlierPixCnt += 1 pixCnt += 1 if brokenPixCnt > 4: raise RuntimeError("More than 4 broken pixels") if outlierPixCnt > 4: raise RuntimeError("More than 4 outlier pixels") if (brokenPixCnt + outlierPixCnt) > 4: raise RuntimeError("More than 4 faulty pixels") # print("Found %d broken pixels, %d outliers" % (brokenPixCnt, outlierPixCnt)) # TODO INCOMPLETE def _I2CWriteWord(self, writeAddress, data): cmd = bytearray(4) cmd[0] = writeAddress >> 8 cmd[1] = writeAddress & 0x00FF cmd[2] = data >> 8 cmd[3] = data & 0x00FF dataCheck = [0] with self.i2c_device as i2c: i2c.write(cmd) # print("Wrote:", [hex(i) for i in cmd]) time.sleep(0.001) self._I2CReadWords(writeAddress, dataCheck) # print("dataCheck: 0x%x" % dataCheck[0]) # if (dataCheck != data): # return -2 _inbuf = bytearray(2 * I2C_READ_LEN) def _I2CReadWords(self, addr, buffer, *, end=None): # stamp = time.monotonic() if end is None: remainingWords = len(buffer) else: remainingWords = end offset = 0 addrbuf = bytearray(2) # inbuf = bytearray(2 * I2C_READ_LEN) inbuf = self._inbuf with self.i2c_device as i2c: while remainingWords: addrbuf[0] = addr >> 8 # MSB addrbuf[1] = addr & 0xFF # LSB read_words = min(remainingWords, I2C_READ_LEN) i2c.write_then_readinto( addrbuf, inbuf, in_end=read_words * 2 ) # in bytes # print("-> ", [hex(i) for i in addrbuf]) outwords = struct.unpack( ">" + "H" * read_words, inbuf[0 : read_words * 2] ) # print("<- (", read_words, ")", [hex(i) for i in outwords]) for i, w in enumerate(outwords): buffer[offset + i] = w offset += read_words remainingWords -= read_words addr += read_words ixc = None mlx = None frame = None def init_camera(scl_pin=22, sda_pin=21, freq=100000): """Explicitly initializes the I2C bus and camera after power is stable.""" global ixc, mlx, frame print(f"[Camera] Initializing I2C on SCL:{scl_pin}, SDA:{sda_pin} at {freq}Hz...") ixc = I2C(pins=(scl_pin, sda_pin), frequency=freq) print("[Camera] Probing for MLX90640...") mlx = MLX90640(ixc) # Bonus: Your wiki snapshot recommends 16Hz for smooth images! mlx.refresh_rate = RefreshRate.REFRESH_16_HZ frame = [0] * 768 print("[Camera] Setup successful!") def read_temperature(): if mlx is None: print("[Camera] Error: Camera not initialized. Call init_camera() first.") return None try: print("Querying camera...") mlx.getFrame(frame) return frame except Exception as e: print(f"[Camera] Read error: {e}") return None