"""Optional Yahboom cooling HAT transport; importing it never opens I2C. Built-in effects avoid continuous host writes. Custom RGB remains experimental: selecting all LEDs before each channel helped one slow calibration run, but did not establish reliable operation across runs or prove any MCU timing guarantee. """ import time import fcntl class Yahboom: def __init__(self, lock, on_error, font): from smbus2 import SMBus # Ownership belongs to the physical device, not to an API socket path. self.ownership = open('/run/lock/pigway-i2c-1-mcu-0d.lock', 'a') try: fcntl.flock(self.ownership, fcntl.LOCK_EX | fcntl.LOCK_NB) self.bus = SMBus(1) except Exception: self.ownership.close() raise self.lock = lock self.on_error = on_error self.state = None self.font = font self.fan_level = None def write(self, method, *args): with self.lock: try: return getattr(self.bus, method)(*args) except OSError: self.state = None self.on_error() raise def _mcu(self, register, value): self.write("write_byte_data", 0x0D, register, value) def off(self): with self.lock: if self.state == ("off",): return self._mcu(0x07, 0) self.state = ("off",) def effect(self, effect, speed, color, delay_ms=10): state = ("effect", effect, speed, color) with self.lock: if self.state == state: return # Parameters first, then the documented effect selector. Never # restart an unchanged effect when OLED pages or owners change. self._mcu(0x05, speed) time.sleep(delay_ms / 1000) self._mcu(0x06, color) time.sleep(delay_ms / 1000) self._mcu(0x04, effect) self.state = state def color(self, channels): channels = tuple(channels) if len(channels) != 3 or any(not 0 <= x <= 255 for x in channels): raise ValueError("RGB channels must be 0..255") state = ("color", channels) with self.lock: if self.state == state: return if self.state is None or self.state[0] != "color": self._mcu(0x07, 0) time.sleep(0.005) # One complete RGB update, in fixed R/G/B order. No guessed block # write, clear register, or MCU effect command on each flash edge. for register, value in enumerate(channels, 1): self._mcu(0x00, 0xFF) time.sleep(0.005) self._mcu(register, value) time.sleep(0.005) self.state = state def fan(self, level): values = (0, 4, 6, 8, 9, 1) with self.lock: if level != self.fan_level: self._mcu(0x08, values[level]) self.fan_level = level def oled_init(self): for c in (0xAE,0xD5,0x80,0xA8,0x1F,0xD3,0x00,0x40,0x8D,0x14, 0x20,0x00,0xA1,0xC8,0xDA,0x02,0x81,0x7F,0xD9,0xF1, 0xDB,0x40,0xA4,0xA6,0xAF): self.write("write_byte_data", 0x3C, 0, c) def oled(self, lines, home=False): # Same 5x7 font and page layout as the verified 128x32 implementation. from PIL import Image im = Image.new("1", (128,32), 0) pix = im.load() def text(x,y,value): for ch in value: code=ord(ch) if code*5+5>len(self.font): code=ord("?") for cx in range(5): col=self.font[code*5+cx] for cy in range(7): if col & (1<122: break for row,(left,right) in enumerate(lines[:4]): text(0,row*8,str(left)) if right: text(72 if home else max(0,128-len(str(right))*6),row*8,str(right)) data=[sum(pix[x,pg*8+bit]<