fix: use calibrated MCU RGB effects
This commit is contained in:
+83
-50
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import time,socket,subprocess,configparser,signal,re,errno,threading,argparse,math
|
||||
import time,socket,subprocess,configparser,signal,re,errno,threading,argparse
|
||||
from pathlib import Path
|
||||
APP_VERSION="3.3.0"
|
||||
from smbus2 import SMBus
|
||||
@@ -121,19 +121,21 @@ def rgb_off():
|
||||
mcu(0x07,0x00) # seller closeRGB(): true OFF
|
||||
_last_rgb=("off",)
|
||||
|
||||
def rgb_begin_mode():
|
||||
def rgb_mode(mode):
|
||||
global _last_rgb
|
||||
mcu(0x07,0x00) # stop a previous mode once, never once per animation frame
|
||||
_last_rgb=None
|
||||
|
||||
def rgb_frame(r,g,b):
|
||||
global _last_rgb
|
||||
scale=max(0,min(100,LED_BRIGHT))/100.0
|
||||
state=(int(r*scale),int(g*scale),int(b*scale))
|
||||
if _last_rgb==state: return
|
||||
mcu(0x00,0xff) # seller protocol: >=3 => all three LEDs
|
||||
mcu(0x01,state[0]); mcu(0x02,state[1]); mcu(0x03,state[2])
|
||||
_last_rgb=state
|
||||
settings={
|
||||
"WARNING_BREATHE":(RGB_WARNING_SPEED,RGB_WARNING_COLOR,0x01),
|
||||
"CRITICAL_BREATHE":(RGB_CRITICAL_SPEED,RGB_CRITICAL_COLOR,0x01),
|
||||
}
|
||||
state=settings.get(mode)
|
||||
if state is None: raise ValueError(f"unsupported RGB mode: {mode}")
|
||||
if _last_rgb==(mode,): return
|
||||
speed,color,effect=state
|
||||
# Hardware-calibrated MCU order. The final effect write commits the mode.
|
||||
mcu(0x05,speed); time.sleep(RGB_WRITE_DELAY_MS/1000.0)
|
||||
mcu(0x06,color); time.sleep(RGB_WRITE_DELAY_MS/1000.0)
|
||||
mcu(0x04,effect)
|
||||
_last_rgb=(mode,)
|
||||
|
||||
def set_pi_led(name,off=True):
|
||||
base=Path("/sys/class/leds")/name
|
||||
@@ -262,14 +264,12 @@ FAN_HYST=cf("fan","hysteresis",1.5)
|
||||
ROW_Y=[cf("display","row1_y",0,int),cf("display","row2_y",8,int),cf("display","row3_y",16,int),cf("display","row4_y",24,int)]
|
||||
DARK_PWR=cf("dark_mode","pi_pwr_led",True,lambda x: str(x).strip().lower() in ("off","0","false","no"))
|
||||
DARK_ACT=cf("dark_mode","pi_act_led",True,lambda x: str(x).strip().lower() in ("off","0","false","no"))
|
||||
LED_BRIGHT=cf("led","brightness",10,int)
|
||||
WARNING_BREATHE_SECONDS=cf("led","warning_breathe_seconds",3.0)
|
||||
CRITICAL_BREATHE_SECONDS=cf("led","critical_breathe_seconds",1.5)
|
||||
EMERGENCY_FLASH_MS=cf("led","emergency_flash_ms",300,int)
|
||||
RGB_UPDATE_MS=cf("led","rgb_update_ms",80,int)
|
||||
RGB_WARNING=(cf("led","warning_r",255,int),cf("led","warning_g",255,int),cf("led","warning_b",0,int))
|
||||
RGB_CRITICAL=(cf("led","critical_r",255,int),cf("led","critical_g",72,int),cf("led","critical_b",0,int))
|
||||
RGB_EMERGENCY=(cf("led","emergency_r",255,int),cf("led","emergency_g",0,int),cf("led","emergency_b",0,int))
|
||||
RGB_WRITE_DELAY_MS=max(1,cf("led","write_delay_ms",10,int))
|
||||
RGB_WARNING_SPEED=max(1,min(3,cf("led","warning_speed",1,int)))
|
||||
RGB_WARNING_COLOR=max(0,min(6,cf("led","warning_color",3,int)))
|
||||
RGB_CRITICAL_SPEED=max(1,min(3,cf("led","critical_speed",3,int)))
|
||||
RGB_CRITICAL_COLOR=max(0,min(6,cf("led","critical_color",0,int)))
|
||||
|
||||
def check_watch(w):
|
||||
typ,label,target=w
|
||||
@@ -419,22 +419,18 @@ def rgb_mode_for(a):
|
||||
if a["sev"]>=2: return "CRITICAL_BREATHE"
|
||||
return "WARNING_BREATHE"
|
||||
|
||||
def breathe(level_seconds,elapsed):
|
||||
# A full sine-derived ease in/out cycle gives continuous light transitions.
|
||||
phase=(elapsed/max(0.1,level_seconds))%1.0
|
||||
return 0.05+0.95*(0.5-0.5*math.cos(2.0*math.pi*phase))
|
||||
|
||||
def rgb_engine(mode,elapsed):
|
||||
if mode=="WARNING_BREATHE":
|
||||
level=breathe(WARNING_BREATHE_SECONDS,elapsed)
|
||||
rgb_frame(*(channel*level for channel in RGB_WARNING))
|
||||
elif mode=="CRITICAL_BREATHE":
|
||||
level=breathe(CRITICAL_BREATHE_SECONDS,elapsed)
|
||||
rgb_frame(*(channel*level for channel in RGB_CRITICAL))
|
||||
def rgb_emergency(on):
|
||||
global _last_rgb
|
||||
state=("EMERGENCY_FLASH",bool(on))
|
||||
if _last_rgb==state: return
|
||||
if not on:
|
||||
mcu(0x07,0x00)
|
||||
else:
|
||||
on=int(elapsed*1000/max(50,EMERGENCY_FLASH_MS))%2==0
|
||||
rgb_frame(RGB_EMERGENCY[0] if on else 0,0,0)
|
||||
|
||||
mcu(0x00,0xff); time.sleep(RGB_WRITE_DELAY_MS/1000.0)
|
||||
mcu(0x01,0xff); time.sleep(RGB_WRITE_DELAY_MS/1000.0)
|
||||
mcu(0x02,0x00); time.sleep(RGB_WRITE_DELAY_MS/1000.0)
|
||||
mcu(0x03,0x00)
|
||||
_last_rgb=state
|
||||
|
||||
def render_home(cpu,temp,mem,disk):
|
||||
# 128x32 fixed columns. Both columns are LEFT-aligned.
|
||||
@@ -470,8 +466,8 @@ def render_home(cpu,temp,mem,disk):
|
||||
bus_write("write_i2c_block_data",OLED,0x40,data[i:i+16])
|
||||
|
||||
|
||||
def render_alert(a):
|
||||
oled_show([(a["title"],""),
|
||||
def render_alert(a,index,total):
|
||||
oled_show([(a["title"],f"{index}/{total}"),
|
||||
(a["l2"],""),(a["l3"],""),(a["l4"],"")])
|
||||
|
||||
|
||||
@@ -488,12 +484,14 @@ class DisplayController:
|
||||
self.current_priority=0
|
||||
self.current_rgb_mode="OFF"
|
||||
self.owner_since=now
|
||||
self.current_oled_page_id="NORMAL_HOME"
|
||||
self.page_started=now
|
||||
self.batch_depth=0
|
||||
self.stop_event=threading.Event()
|
||||
self.rgb_thread=threading.Thread(target=self._rgb_worker,name="rgb",daemon=True)
|
||||
|
||||
def start(self):
|
||||
oled_init(); rgb_off(); self.refresh(); self.rgb_thread.start()
|
||||
oled_init(); self.refresh(); rgb_off(); self.rgb_thread.start()
|
||||
|
||||
def update_home(self,cpu,temp,mem,disk):
|
||||
with self.lock:
|
||||
@@ -526,7 +524,10 @@ class DisplayController:
|
||||
with self.lock:
|
||||
if self.requests.pop(request_id,None) is not None:
|
||||
log("INFO","ALERT_RECOVERED",alert=request_id)
|
||||
if self.batch_depth==0: self._arbitrate_locked()
|
||||
if self.batch_depth==0:
|
||||
self._arbitrate_locked()
|
||||
if self.current_oled_page_id not in self.requests:
|
||||
self._show_page_locked(self.current_owner_id)
|
||||
|
||||
def begin_update(self):
|
||||
with self.lock: self.batch_depth+=1
|
||||
@@ -535,7 +536,10 @@ class DisplayController:
|
||||
with self.lock:
|
||||
if self.batch_depth<=0: raise RuntimeError("display update batch imbalance")
|
||||
self.batch_depth-=1
|
||||
if self.batch_depth==0: self._arbitrate_locked()
|
||||
if self.batch_depth==0:
|
||||
self._arbitrate_locked()
|
||||
if self.current_oled_page_id not in self.requests:
|
||||
self._show_page_locked(self.current_owner_id)
|
||||
|
||||
def _arbitrate_locked(self):
|
||||
current=self.requests.get(self.current_owner_id)
|
||||
@@ -552,14 +556,39 @@ class DisplayController:
|
||||
self.current_rgb_mode=request["rgb_mode"]
|
||||
self.owner_since=time.monotonic()
|
||||
log("INFO","DISPLAY_OWNER",old=old,new=request["id"],priority=request["priority"])
|
||||
self._render_locked(request)
|
||||
self._show_page_locked(request["id"])
|
||||
|
||||
def _render_locked(self,request):
|
||||
if request["id"]=="NORMAL_HOME": render_home(*request["metadata"]["home"])
|
||||
else: render_alert(request)
|
||||
def _ordered_alerts_locked(self):
|
||||
return sorted((r for r in self.requests.values() if r["id"]!="NORMAL_HOME"),
|
||||
key=lambda r:(-r["priority"],r["first_seen"],r["id"]))
|
||||
|
||||
def _show_page_locked(self,page_id):
|
||||
alerts=self._ordered_alerts_locked()
|
||||
request=self.requests.get(page_id,self.requests["NORMAL_HOME"])
|
||||
self.current_oled_page_id=request["id"]
|
||||
self.page_started=time.monotonic()
|
||||
if request["id"]=="NORMAL_HOME":
|
||||
render_home(*request["metadata"]["home"])
|
||||
else:
|
||||
alert_ids=[a["id"] for a in alerts]
|
||||
render_alert(request,alert_ids.index(request["id"])+1,len(alerts))
|
||||
|
||||
def refresh(self):
|
||||
with self.lock: self._render_locked(self.requests[self.current_owner_id])
|
||||
with self.lock:
|
||||
alerts=self._ordered_alerts_locked()
|
||||
pages=["NORMAL_HOME"]+[a["id"] for a in alerts]
|
||||
if self.current_oled_page_id not in pages:
|
||||
self._show_page_locked(self.current_owner_id)
|
||||
return
|
||||
if len(pages)>1 and time.monotonic()-self.page_started>=PAGE_INTERVAL:
|
||||
pos=pages.index(self.current_oled_page_id)
|
||||
self._show_page_locked(pages[(pos+1)%len(pages)])
|
||||
return
|
||||
request=self.requests[self.current_oled_page_id]
|
||||
if request["id"]=="NORMAL_HOME": render_home(*request["metadata"]["home"])
|
||||
else:
|
||||
alert_ids=[a["id"] for a in alerts]
|
||||
render_alert(request,alert_ids.index(request["id"])+1,len(alerts))
|
||||
|
||||
def owner_snapshot(self):
|
||||
with self.lock: return dict(self.requests[self.current_owner_id])
|
||||
@@ -572,17 +601,21 @@ class DisplayController:
|
||||
if wanted_mode!=applied_mode:
|
||||
with bus_lock:
|
||||
if wanted_mode=="OFF": rgb_off()
|
||||
else: rgb_begin_mode()
|
||||
elif wanted_mode=="EMERGENCY_FLASH": rgb_emergency(True)
|
||||
else: rgb_mode(wanted_mode)
|
||||
log("INFO","RGB_MODE",old=applied_mode,new=wanted_mode)
|
||||
applied_mode=wanted_mode
|
||||
if applied_mode!="OFF":
|
||||
with bus_lock: rgb_engine(applied_mode,time.monotonic()-animation_started)
|
||||
applied_mode=wanted_mode; animation_started=time.monotonic()
|
||||
if applied_mode=="EMERGENCY_FLASH":
|
||||
elapsed=time.monotonic()-animation_started
|
||||
on=int(elapsed*1000/max(50,EMERGENCY_FLASH_MS))%2==0
|
||||
with bus_lock: rgb_emergency(on)
|
||||
if previous_error is not None: log("INFO","RGB_RECOVERED")
|
||||
previous_error=None
|
||||
except Exception as e:
|
||||
if repr(e)!=previous_error: log("ERROR","RGB_ERROR",error=repr(e))
|
||||
previous_error=repr(e)
|
||||
self.stop_event.wait(max(0.04,min(0.5,RGB_UPDATE_MS/1000.0)))
|
||||
delay=0.04 if applied_mode=="EMERGENCY_FLASH" else 0.25
|
||||
self.stop_event.wait(delay)
|
||||
|
||||
def shutdown(self):
|
||||
self.stop_event.set()
|
||||
|
||||
Reference in New Issue
Block a user