From d3ccca4b37bbc5ebfc8dd147321f15508dccddf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A1=E5=8F=A3=E6=B5=AA=E4=BA=BA?= Date: Sat, 26 Sep 2026 22:24:43 +0800 Subject: [PATCH] fix: redesign RGB alert engine --- README.md | 10 ++++-- SHA256SUMS | 6 ++-- app/pigway_pi_control.py | 61 ++++++++++++++++++++++++----------- config/pigway-pi-control.conf | 47 +++++++++++++-------------- 4 files changed, 76 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index f7a0ced..0fc1188 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,11 @@ CHECK STORAGE - 主循环异常计数 - I²C相关异常计数基础字段 -### 4. 结构化事件日志 +### 4. RGB 告警引擎 + +RGB 与 OLED 页面完全解耦,只使用所有 active alert 的最高 severity。Warning 为黄色 3 秒呼吸,Critical 为橙红色 1.5 秒呼吸,Emergency 为纯红快速频闪;无 active alert 时 OFF。动画不写循环日志,模式切换仅记录一次 `RGB_MODE`。 + +### 5. 结构化事件日志 日志继续交给 systemd journal,不额外制造长期 `.log` 文件。 @@ -104,7 +108,7 @@ journalctl -u pigway-pi-control -b --no-pager journalctl -u pigway-pi-control --no-pager | grep 'event=POWER' ``` -### 5. 诊断命令 +### 6. 诊断命令 ```bash sudo /usr/local/sbin/pigway-pi-control --diagnose @@ -116,7 +120,7 @@ sudo /usr/local/sbin/pigway-pi-control --diagnose RC2 保留 v3.2.0 的 RGB MCU 寄存器写入顺序;独立闪烁时钟使用所有有效告警的最高 severity,不受 OLED 页面及网络/watch 检查等待影响。服务恢复后自动切换到下一有效告警,没有告警时 OFF。 -### 6. 关机收尾 +### 7. 关机收尾 收到 systemd SIGTERM / 系统关机时: diff --git a/SHA256SUMS b/SHA256SUMS index c9e3df1..540855b 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,7 +1,7 @@ -192d29003ffd867d440753c1903d194a51ec9c2cadbc5442d0e8d1977364217d README.md +55fe2917b92a896fb346fb3c9c515fafcf47d7ea095c0d92dfea66a7b3edfc19 README.md 72c46162c33f9587c6ccb01ad53fbaaf4ecec8cd21014b909e87e8707e253e36 app/oled_font_5x7.bin -1445d390eef0a66c14d827f8468064f5a09ae2f005e04a636ae38f77dd0bb5d7 app/pigway_pi_control.py -7d64d7fcf2b17580b4565b99e290a85e4317dbf2cc71224adcacb56489fcf1b3 config/pigway-pi-control.conf +75138ce957a501dc34bf2d886476b92e897d16d42c06ccbffda27524f164cce4 app/pigway_pi_control.py +fbd8829929bc755cef6beeded475ac5eaf1a5b9152b4294bb8d169e224e3bc9c config/pigway-pi-control.conf 4c2816da08378f2927c746b70ecfa7d5312e1730c08727cd89e8c2801e707054 install.sh 81aea0a89b298b2512fcb9bf6418618ba7a66474d425390cf95a0128c4093474 systemd/pigway-pi-control.service a3d74973cafbabaf58a660a8c83d28347d1c06d9fd2dab3ecfce5b7607f6fee2 uninstall.sh diff --git a/app/pigway_pi_control.py b/app/pigway_pi_control.py index 7a095ee..cc7ade8 100755 --- a/app/pigway_pi_control.py +++ b/app/pigway_pi_control.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -import time,socket,subprocess,configparser,signal,re,errno,threading,argparse +import time,socket,subprocess,configparser,signal,re,errno,threading,argparse,math from pathlib import Path APP_VERSION="3.3.0" from smbus2 import SMBus @@ -236,7 +236,7 @@ OLED_REFRESH=cf("timing","oled_refresh_interval",1.0) PAGE_INTERVAL=cf("timing","page_interval",10.0) NETWORK_INTERVAL=cf("timing","network_interval",5.0) SERVICE_INTERVAL=cf("timing","service_interval",5.0) -HEALTH_INTERVAL=cf("timing","health_interval",5.0) +HEALTH_INTERVAL=cf("timing","health_interval",1.0) CPU_HIGH=cf("alerts","cpu_high",90.0) CPU_RECOVER=cf("alerts","cpu_recover",75.0) @@ -259,9 +259,13 @@ ROW_Y=[cf("display","row1_y",0,int),cf("display","row2_y",8,int),cf("display","r 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) -LED_WARN_ON=cf("led","warning_on_ms",300,int); LED_WARN_OFF=cf("led","warning_off_ms",2700,int) -LED_CRIT_ON=cf("led","critical_on_ms",400,int); LED_CRIT_OFF=cf("led","critical_off_ms",1100,int) -LED_EMERG_ON=cf("led","emergency_on_ms",400,int); LED_EMERG_OFF=cf("led","emergency_off_ms",400,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",40,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)) def check_watch(w): typ,label,target=w @@ -357,16 +361,29 @@ def make_alerts(cpu,temp,mem,disk,sample_elapsed=0.0): a.sort(key=lambda x:x["sev"],reverse=True) return a -def led_for(a): - # Color = fault object. Blink frequency = severity. All alerts blink. - if not a: - rgb_off(); return - if a["sev"]>=3: on_ms,off_ms=LED_EMERG_ON,LED_EMERG_OFF - elif a["sev"]>=2: on_ms,off_ms=LED_CRIT_ON,LED_CRIT_OFF - else: on_ms,off_ms=LED_WARN_ON,LED_WARN_OFF - cycle=max(1,on_ms+off_ms) - phase=int(time.monotonic()*1000)%cycle - rgb(*a["color"]) if phase=3: return "EMERGENCY_FLASH" + 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.5-0.5*math.cos(2.0*math.pi*phase) + +def rgb_engine(mode,elapsed): + if mode=="OFF": + rgb_off() + elif mode=="WARNING_BREATHE": + level=breathe(WARNING_BREATHE_SECONDS,elapsed) + rgb(*(channel*level for channel in RGB_WARNING)) + elif mode=="CRITICAL_BREATHE": + level=breathe(CRITICAL_BREATHE_SECONDS,elapsed) + rgb(*(channel*level for channel in RGB_CRITICAL)) + else: + on=int(elapsed*1000/max(50,EMERGENCY_FLASH_MS))%2==0 + rgb(RGB_EMERGENCY[0] if on else 0,0,0) def render_home(cpu,temp,mem,disk): @@ -417,21 +434,29 @@ def find_live_alert(key, live): if alert_key(a)==key: return a return None -# RGB has its own clock so slow network/watch checks cannot stretch blink pulses. +# RGB has its own clock and consumes only the highest-severity active alert. +# OLED rendering never reads or writes RGB state. led_stop=threading.Event() led_alert=None def led_worker(): previous_error=None + current_mode="OFF" + mode_started=time.monotonic() while not led_stop.is_set(): try: - with bus_lock: led_for(led_alert) + new_mode=rgb_mode_for(led_alert) + if new_mode!=current_mode: + log("INFO","RGB_MODE",old=current_mode,new=new_mode) + current_mode=new_mode; mode_started=time.monotonic() + # Keep the verified five-write MCU RGB transaction contiguous. + with bus_lock: rgb_engine(current_mode,time.monotonic()-mode_started) 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) - led_stop.wait(0.02) + led_stop.wait(max(0.02,min(0.05,RGB_UPDATE_MS/1000.0))) def cleanup(): log("INFO","STOP",loop_errors=agent["loop_errors"],i2c_errors=agent["i2c_errors"]) diff --git a/config/pigway-pi-control.conf b/config/pigway-pi-control.conf index 089653b..d13d20c 100644 --- a/config/pigway-pi-control.conf +++ b/config/pigway-pi-control.conf @@ -50,8 +50,8 @@ service_interval = 5 # 本机健康检查周期,单位:秒。 # 检查 Raspberry Pi 欠压/降频状态、根文件系统只读状态等。 -# 推荐:5。 -health_interval = 5 +# 推荐:1。当前电源状态必须及时反映到 OLED/RGB;历史状态仅记录一次日志。 +health_interval = 1 [fan] @@ -139,34 +139,33 @@ wifi_weak_seconds = 30 [led] -# RGB 规则: -# 正常状态 = 完全熄灭 -# 颜色 = 哪一类对象发生故障 -# 闪烁频率 = 严重程度,越严重闪得越快 -# -# 当前颜色语义: -# CPU负载 = 黄 -# 温度/散热 = 红 -# 内存 = 紫 -# 磁盘 = 橙 -# 网络 = 蓝 -# 服务/进程 = 青 +# RGB 只由所有 active alert 中最高 severity 决定,与 OLED 当前页面完全无关: +# Warning (1) = 黄色慢呼吸 +# Critical (2) = 橙红色快呼吸 +# Emergency (3) = 纯红快速频闪 +# 无 active alert 时 RGB OFF。每次写入明确设置 R/G/B,绝不沿用旧颜色。 # # brightness 是 RGB 最大输出百分比(0~100)。 # 3颗 RGB 会一起亮,卧室建议 5~15。 brightness = 10 -# Warning:慢闪。以下单位均为毫秒。 -warning_on_ms = 300 -warning_off_ms = 2700 +# 旧 warning_on_ms / warning_off_ms / critical_on_ms / critical_off_ms / +# emergency_on_ms / emergency_off_ms 仍可保留,RC3 忽略它们以兼容旧配置。 +warning_breathe_seconds = 3.0 +critical_breathe_seconds = 1.5 +emergency_flash_ms = 300 +rgb_update_ms = 40 -# Critical:中速闪烁。 -critical_on_ms = 400 -critical_off_ms = 1100 - -# Emergency:快速闪烁。 -emergency_on_ms = 400 -emergency_off_ms = 400 +# RGB 基色(0~255)。安全默认值严格不含蓝色,避免紫色/蓝色残留。 +warning_r = 255 +warning_g = 255 +warning_b = 0 +critical_r = 255 +critical_g = 72 +critical_b = 0 +emergency_r = 255 +emergency_g = 0 +emergency_b = 0 [dark_mode]