fix: redesign RGB alert engine

This commit is contained in:
way
2026-09-26 22:24:43 +08:00
parent 3a302b9c78
commit d3ccca4b37
4 changed files with 76 additions and 48 deletions
+7 -3
View File
@@ -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 / 系统关机时:
+3 -3
View File
@@ -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
+43 -18
View File
@@ -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<on_ms else rgb_off()
def rgb_mode_for(a):
if not a: return "OFF"
if a["sev"]>=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"])
+23 -24
View File
@@ -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]