From a8ca071b561d07e245fb11976c23061084eed440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A1=E5=8F=A3=E6=B5=AA=E4=BA=BA?= Date: Sun, 27 Sep 2026 10:36:32 +0800 Subject: [PATCH] feat: add configurable normal RGB effects --- README.md | 6 ++-- SHA256SUMS | 12 ++++---- app/pigway_pi_control.py | 55 ++++++++++++++++++++++++++++++++--- app/pigway_pi_control_api.py | 12 ++++++-- config/pigway-pi-control.conf | 30 +++++++++++++++++-- install.sh | 8 +++++ web/index.html | 18 ++++++++++-- 7 files changed, 122 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 54e8186..fac7189 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,11 @@ CHECK STORAGE 每个当前故障都是 DisplayController 中的一个 active request,包含稳定 ID、priority、severity、OLED 显示数据、首次发现时间、最后变化时间和元数据。监控只提交或清除 request,同一轮采样作为一个原子批次,全部更新后只仲裁一次。DisplayController 始终只选一个最高优先级 owner;同一 ID 更新原请求而不重复入队,恢复时删除请求,同优先级新 request 排队而不抢占,owner 恢复后按 priority、first_seen、ID 重新仲裁。OLED 在主页和全部当前告警页之间轮播,告警页右上角显示 `当前页/告警总数`,新 owner 会立即抢占一次 OLED。告警页显示时 RGB 同步表达该页对象;回到主页时 RGB 保持最高优先级 active owner,全部恢复后才 OFF。历史 power flags 只进入 diagnose 和一次性 Journal 记录。 -### 5. RGB 告警引擎 +### 5. RGB 正常状态与告警引擎 -RGB 模式由当前 OLED 告警页决定;OLED 位于主页时则使用最高优先级 active owner。颜色表示故障对象:CPU负载、频率、throttling、温度与散热相关为红色,供电/欠压为橙色,内存为紫色,存储为白色,网络/Wi-Fi 为蓝色,服务/进程为青色。频率表示严重等级:默认 Warning 为1500ms亮/1500ms灭,Critical 为750ms亮/750ms灭,Emergency 为300ms亮/300ms灭;没有 active alert 时 OFF。`[led]` 可分别配置六类对象的 R/G/B 值,以及三个严重等级的亮灯和灭灯时长,非法颜色值会限制到0..255,间隔最小为50ms。每次点亮使用 v3.2.0 已验证的完整 selector/R/G/B 写入顺序,只在亮/灭边沿写 MCU,不做高频软件 PWM。相邻页面 RGB mode 相同时不关闭、不重启节奏。动画不写循环日志,owner 变化记录 `DISPLAY_OWNER`,实际模式变化才记录 `RGB_MODE`。 +没有 active alert 时,RGB 默认全灭。`[led] normal_mode` 可改为自定义颜色常亮/闪烁,或 MCU 已确认的流水、呼吸、跑马、彩虹和炫彩效果。常亮与闪烁使用 0..255 的自定义 R/G/B;内置流水/呼吸使用 MCU 固定的红、绿、蓝、黄、紫、青、白七色和慢/中/快三档速度,跑马/彩虹/炫彩由 MCU 自行配色。内置效果只在模式切换时写一次寄存器,不循环刷 I²C。 + +任何 active alert 都会立即抢占正常效果。RGB 告警模式由当前 OLED 告警页决定;OLED 位于主页时则使用最高优先级 active owner。颜色表示故障对象:CPU负载、频率、throttling、温度与散热相关为红色,供电/欠压为橙色,内存为紫色,存储为白色,网络/Wi-Fi 为蓝色,服务/进程为青色。频率表示严重等级:默认 Warning 为1500ms亮/1500ms灭,Critical 为750ms亮/750ms灭,Emergency 为300ms亮/300ms灭。`[led]` 可分别配置六类对象的 R/G/B 值,以及三个严重等级的亮灯和灭灯时长,非法颜色值会限制到0..255,间隔最小为50ms。每次点亮使用 v3.2.0 已验证的完整 selector/R/G/B 写入顺序,只在亮/灭边沿写 MCU,不做高频软件 PWM。相邻页面 RGB mode 相同时不关闭、不重启节奏。动画不写循环日志,owner 变化记录 `DISPLAY_OWNER`,实际模式变化才记录 `RGB_MODE`。 ### 6. 结构化事件日志 diff --git a/SHA256SUMS b/SHA256SUMS index 8142fb5..25f1a20 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,10 +1,10 @@ -1d30072ac84d648985cbead711b7cfa55254bc38ce1d48ac71b976333e83126d README.md +40154b1fabac7bdfc7e4a6cd1a88ffa42e6842ef1da625594d5d9e4b904e99b9 README.md 72c46162c33f9587c6ccb01ad53fbaaf4ecec8cd21014b909e87e8707e253e36 app/oled_font_5x7.bin -29f59e41052e1e251bf2f38274cdd321fc195602220b61ac8890202128d7c50a app/pigway_pi_control.py -6bde03a945b8a8a93694ad4c453a6dcefed1135278ef2cc097029a54046093c1 app/pigway_pi_control_api.py -075cc50f9bb46389323c46fc6625fbd59925a2d9755fb1d5a714ea5a94e00908 config/pigway-pi-control.conf -0c1a68c6ba447f1c17bd5f7fb66b29a54588084023340ac0b7d959717952bcad install.sh +c3062ad1122c36a762bc3ca9d44ebcb5a6a5a37446a85fb68312de4fd8aa5db9 app/pigway_pi_control.py +3a13b4e8f9d306e77f82eba78b78fe9b1932488884d6f3f976754bfa94f0153d app/pigway_pi_control_api.py +75fa662ed21138a8e394de83e46281dab039433cc0d7a8f787e1a049f4b47ec8 config/pigway-pi-control.conf +1276369d600e9fd560d03e26146bcaeb40a353829978d1ebaa578d385b27af72 install.sh 4f6e8db3698a62f57823d3298a0e6814593de6fa3d31e4751b7ea4a76e553e35 systemd/pigway-pi-control.service aa21cef9a06593371ccff005335b6c0c826fbf4b5c48e4255b5f4b75ce6e9217 systemd/pigway-pi-control-api.service 9f59e7313ee58e687ae6dd29d175c67082674254ac5c5f54168b2735b64c20a4 uninstall.sh -f47627ac7007e54258e3e642358d6829991cbe5469e439fda530d6c9118bea4e web/index.html +7694998270db13407c197707ee41f8fb8d8d9ffa8d2325f20d7dd7719ab46815 web/index.html diff --git a/app/pigway_pi_control.py b/app/pigway_pi_control.py index a33f5b2..d25b7d1 100755 --- a/app/pigway_pi_control.py +++ b/app/pigway_pi_control.py @@ -253,6 +253,20 @@ RGB_WRITE_DELAY_MS=max(1,cf("led","write_delay_ms",10,int)) WARNING_FLASH=(max(50,cf("led","warning_flash_on_ms",1500,int)),max(50,cf("led","warning_flash_off_ms",1500,int))) CRITICAL_FLASH=(max(50,cf("led","critical_flash_on_ms",750,int)),max(50,cf("led","critical_flash_off_ms",750,int))) EMERGENCY_FLASH=(max(50,cf("led","emergency_flash_on_ms",300,int)),max(50,cf("led","emergency_flash_off_ms",300,int))) +NORMAL_MODE=cf("led","normal_mode","off",str).strip().lower() +NORMAL_MODES={"off","solid","flash","flow","breathe","marquee","rainbow","colorful"} +if NORMAL_MODE not in NORMAL_MODES: NORMAL_MODE="off" +NORMAL_COLOR=tuple(max(0,min(255,cf("led",f"normal_{channel}",value,int))) + for channel,value in zip(("r","g","b"),(0,0,0))) +NORMAL_FLASH=(max(50,cf("led","normal_flash_on_ms",1000,int)), + max(50,cf("led","normal_flash_off_ms",1000,int))) +NORMAL_EFFECT_SPEED=max(1,min(3,cf("led","normal_effect_speed",2,int))) +NORMAL_EFFECT_COLOR=cf("led","normal_effect_color","green",str).strip().lower() +RGB_EFFECT_COLORS={"red":0,"green":1,"blue":2,"yellow":3,"purple":4,"cyan":5,"white":6} +if NORMAL_EFFECT_COLOR not in RGB_EFFECT_COLORS: NORMAL_EFFECT_COLOR="green" +RGB_EFFECTS={"flow":0,"breathe":1,"marquee":2,"rainbow":3,"colorful":4} +NORMAL_RGB_MODE={"off":"OFF","solid":"NORMAL_SOLID","flash":"NORMAL_FLASH", + **{name:f"NORMAL_{name.upper()}" for name in RGB_EFFECTS}}[NORMAL_MODE] def rgb_cfg(name,default): return tuple(max(0,min(255,cf("led",f"{name}_{channel}",value,int))) @@ -441,6 +455,30 @@ def rgb_flash(mode,on): mcu(0x03,channels[2]) _last_rgb=state +def rgb_static(channels,state_name): + global _last_rgb + state=(state_name,channels) + if _last_rgb==state: return + mcu(0x07,0x00); time.sleep(RGB_WRITE_DELAY_MS/1000.0) + mcu(0x00,0xff); time.sleep(RGB_WRITE_DELAY_MS/1000.0) + mcu(0x01,channels[0]); time.sleep(RGB_WRITE_DELAY_MS/1000.0) + mcu(0x02,channels[1]); time.sleep(RGB_WRITE_DELAY_MS/1000.0) + mcu(0x03,channels[2]) + _last_rgb=state + +def rgb_normal_effect(mode): + global _last_rgb + effect_name=mode.removeprefix("NORMAL_").lower() + state=("effect",effect_name,NORMAL_EFFECT_SPEED,NORMAL_EFFECT_COLOR) + if _last_rgb==state: return + # Seller protocol: close the previous RGB state, then set effect, speed and + # the limited built-in palette. Write this sequence once per mode change. + mcu(0x07,0x00); time.sleep(0.001) + mcu(0x04,RGB_EFFECTS[effect_name]); time.sleep(0.001) + mcu(0x05,NORMAL_EFFECT_SPEED); time.sleep(0.001) + mcu(0x06,RGB_EFFECT_COLORS[NORMAL_EFFECT_COLOR]) + _last_rgb=state + def render_home(cpu,temp,mem,disk): # 128x32 fixed columns. Both columns are LEFT-aligned. # Seller 5x7 font has 6px character pitch; right column starts at x=72. @@ -485,7 +523,7 @@ class DisplayController: now=time.monotonic() self.lock=threading.RLock() self.requests={"NORMAL_HOME":{ - "id":"NORMAL_HOME","priority":0,"severity":0,"rgb_mode":"OFF", + "id":"NORMAL_HOME","priority":0,"severity":0,"rgb_mode":NORMAL_RGB_MODE, "title":"","l2":"","l3":"","l4":"","active":True, "first_seen":now,"last_changed":now,"metadata":{"home":(0.0,0.0,0.0,0.0)}, }} @@ -637,18 +675,27 @@ class DisplayController: if wanted_mode!=applied_mode: with bus_lock: if wanted_mode=="OFF": rgb_off() - elif wanted_flash: rgb_flash(wanted_mode,True) + elif wanted_mode=="NORMAL_SOLID": rgb_static(NORMAL_COLOR,"normal_solid") + elif wanted_mode=="NORMAL_FLASH": rgb_static(NORMAL_COLOR,"normal_flash_on") + elif wanted_mode.startswith("NORMAL_"): rgb_normal_effect(wanted_mode) + elif wanted_flash: + rgb_off() + rgb_flash(wanted_mode,True) else: raise ValueError(f"unsupported RGB mode: {wanted_mode}") log("INFO","RGB_MODE",old=applied_mode,new=wanted_mode) applied_mode=wanted_mode; animation_started=time.monotonic() applied_flash=applied_mode.endswith("_FLASH") if applied_flash: elapsed_ms=(time.monotonic()-animation_started)*1000.0 - if "_EMERGENCY_" in applied_mode: on_ms,off_ms=EMERGENCY_FLASH + if applied_mode=="NORMAL_FLASH": on_ms,off_ms=NORMAL_FLASH + elif "_EMERGENCY_" in applied_mode: on_ms,off_ms=EMERGENCY_FLASH elif "_CRITICAL_" in applied_mode: on_ms,off_ms=CRITICAL_FLASH else: on_ms,off_ms=WARNING_FLASH on=elapsed_ms%(on_ms+off_ms) R -> G -> B 完整写入顺序, # 只在亮/灭边沿访问 MCU,不做高频软件 PWM。 write_delay_ms = 10 diff --git a/install.sh b/install.sh index 2106123..9930561 100755 --- a/install.sh +++ b/install.sh @@ -103,6 +103,10 @@ path=Path(sys.argv[1]) text=path.read_text() sections={ "led":( + ("normal_mode","off"), + ("normal_r",0),("normal_g",0),("normal_b",0), + ("normal_flash_on_ms",1000),("normal_flash_off_ms",1000), + ("normal_effect_speed",2),("normal_effect_color","green"), ("cpu_r",255),("cpu_g",0),("cpu_b",0), ("power_r",255),("power_g",40),("power_b",0), ("memory_r",255),("memory_g",0),("memory_b",255), @@ -129,6 +133,10 @@ for section,defaults in sections.items(): if not re.search(rf"(?m)^\s*{re.escape(key)}\s*=",chunk)] if missing: note="# Added by installer; existing user values are preserved.\n" + if section=="led" and any(key.startswith("normal_") for key,_ in missing): + note+=("# Normal RGB: normal_mode = off/solid/flash/flow/breathe/marquee/rainbow/colorful.\n" + "# normal_r/g/b = 0..255; normal_flash_*_ms = 50..60000; effect speed = 1..3.\n" + "# Effect color = red/green/blue/yellow/purple/cyan/white (flow/breathe only).\n") block=note+"".join(f"{key} = {value}\n" for key,value in missing) text=text[:end].rstrip()+"\n"+block+"\n\n"+text[end:].lstrip("\n") path.write_text(text) diff --git a/web/index.html b/web/index.html index c124c88..ea99db7 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,7 @@ PIGWay Pi Control
@@ -28,13 +28,25 @@