fix: isolate fan writes from active RGB frames
This commit is contained in:
@@ -69,7 +69,7 @@ CHECK STORAGE
|
||||
|
||||
### 4. DisplayController、OLED 与 RGB
|
||||
|
||||
每个当前故障都是 DisplayController 中的一个 active request,包含稳定 ID、priority、severity、OLED 显示数据、首次发现时间、最后变化时间和元数据。监控只提交或清除 request,同一轮采样作为一个原子批次,全部更新后只仲裁一次。DisplayController 始终只选一个最高优先级 owner;同一 ID 更新原请求而不重复入队,恢复时删除请求,同优先级新 request 排队而不抢占,owner 恢复后按 priority、first_seen、ID 重新仲裁。OLED 与 RGB 分别由专属工作线程驱动,监控主循环只更新共享显示状态;OLED 在主页和全部当前告警页之间轮播,告警页右上角显示 `当前页/告警总数`,新 owner 会立即唤醒 OLED 线程。告警页显示时 RGB 同步表达该页对象;回到主页时 RGB 保持最高优先级 active owner,全部恢复后才 OFF。相邻 OLED 页面需要的 RGB mode 相同时,RGB 线程不写寄存器、不重置动画相位。历史 power flags 只进入 diagnose 和一次性 Journal 记录。
|
||||
每个当前故障都是 DisplayController 中的一个 active request,包含稳定 ID、priority、severity、OLED 显示数据、首次发现时间、最后变化时间和元数据。监控只提交或清除 request,同一轮采样作为一个原子批次,全部更新后只仲裁一次。DisplayController 始终只选一个最高优先级 owner;同一 ID 更新原请求而不重复入队,恢复时删除请求,同优先级新 request 排队而不抢占,owner 恢复后按 priority、first_seen、ID 重新仲裁。OLED 与 RGB 分别由专属工作线程驱动,监控主循环只更新共享显示状态;OLED 在主页和全部当前告警页之间轮播,告警页右上角显示 `当前页/告警总数`,新 owner 会立即唤醒 OLED 线程。告警页显示时 RGB 同步表达该页对象;回到主页时 RGB 保持最高优先级 active owner,全部恢复后才 OFF。相邻 OLED 页面需要的 RGB mode 相同时,RGB 线程不写寄存器、不重置动画相位。 风扇与 RGB 共用 MCU 0x0D;RGB 正在告警闪烁时,风扇档位命令排到灭灯阶段写入,并以 `FAN_LEVEL` 事件记录实际写入。历史 power flags 只进入 diagnose 和一次性 Journal 记录。
|
||||
|
||||
### 5. RGB 正常状态与告警引擎
|
||||
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
95b1f56c564cecbcb32b402321b5bba0cb909bb979c07ff90abad9236a9a0e95 README.md
|
||||
7df7ae7e17248c2e0fb3a2d418e118668c055d7274e78fd2588515a613a857d0 README.md
|
||||
72c46162c33f9587c6ccb01ad53fbaaf4ecec8cd21014b909e87e8707e253e36 app/oled_font_5x7.bin
|
||||
db6d52dec3e838717a46a4ec243f0cb54f72ede1c639cbadc35230186283d57c app/pigway_pi_control.py
|
||||
d3f597e5b3ae7c04b6edcd7db304d939adf8c5014628e82f168899a40cc34eb7 app/pigway_pi_control.py
|
||||
034931590d83969f9f24222cc57800ab193588beea3d3dcedb98f1690000370d app/pigway_pi_control_api.py
|
||||
ee0f682e8016f75f906d974f00c3e3340edddf30447d441ddfd61a7a34c9e83c config/pigway-pi-control.conf
|
||||
f9fbb4defa45b43abe0ce66973b1f4d553369b3a43c01d04f206c573f7dc96ca install.sh
|
||||
33a681e896cae05b9e9d5f89c5387036c9c1ea48d4db937ae03e5cdb7322705d systemd/pigway-pi-control.service
|
||||
aa21cef9a06593371ccff005335b6c0c826fbf4b5c48e4255b5f4b75ce6e9217 systemd/pigway-pi-control-api.service
|
||||
9f59e7313ee58e687ae6dd29d175c67082674254ac5c5f54168b2735b64c20a4 uninstall.sh
|
||||
8d6e08b57f43dc227d8efd6f8daa6be0b423a8126f59e7f15304ba7b10f898c1 web/index.html
|
||||
27454feccb83ad23342fa9d66caed60d568beeed4862852df98a3bc82bf5b6e3 web/index.html
|
||||
|
||||
@@ -98,6 +98,9 @@ def bus_write(method,*args):
|
||||
raise
|
||||
|
||||
fan_level=0
|
||||
fan_initialized=False
|
||||
pending_fan=None
|
||||
pending_fan_lock=threading.Lock()
|
||||
prev_cpu=None
|
||||
page=0; page_since=time.monotonic()
|
||||
cpu_hi=mem_hi=0
|
||||
@@ -180,13 +183,32 @@ def fan_target(t):
|
||||
if t<FAN_T[4]: return 4
|
||||
return 5
|
||||
def update_fan(t):
|
||||
global fan_level
|
||||
want=fan_target(t); old=fan_level
|
||||
global fan_level,fan_initialized,pending_fan
|
||||
want=fan_target(t); old=fan_level if fan_initialized else None
|
||||
if want>fan_level: fan_level=want
|
||||
elif want<fan_level:
|
||||
boundary = FAN_T[max(0,fan_level-1)] - FAN_HYST
|
||||
if t < boundary: fan_level-=1
|
||||
if old!=fan_level: mcu(0x08,FAN_REG[fan_level])
|
||||
if not fan_initialized or old!=fan_level:
|
||||
with pending_fan_lock:
|
||||
pending_fan=(old,fan_level,float(t))
|
||||
fan_initialized=True
|
||||
|
||||
def apply_pending_fan():
|
||||
global pending_fan
|
||||
with pending_fan_lock:
|
||||
command=pending_fan
|
||||
pending_fan=None
|
||||
if command is None: return
|
||||
old,new,temp=command
|
||||
try:
|
||||
mcu(0x08,FAN_REG[new])
|
||||
except Exception:
|
||||
with pending_fan_lock:
|
||||
if pending_fan is None: pending_fan=command
|
||||
raise
|
||||
log("INFO","FAN_LEVEL",old=FAN_NAME[old] if old is not None else "UNKNOWN",
|
||||
new=FAN_NAME[new],temp_c=f"{temp:.1f}",register=f"0x{FAN_REG[new]:02x}")
|
||||
|
||||
def cpu_pct():
|
||||
global prev_cpu
|
||||
@@ -756,6 +778,11 @@ class DisplayController:
|
||||
rgb_static(NORMAL_COLOR,"normal_flash_on") if on else rgb_off()
|
||||
else: rgb_flash(applied_mode,on)
|
||||
flash_on=on
|
||||
# Fan and RGB share the same MCU. During an alert flash, send
|
||||
# fan changes only while RGB is already dark so MCU-internal
|
||||
# register handling cannot expose a transient unrelated color.
|
||||
if not applied_flash or flash_on is False:
|
||||
with bus_lock: apply_pending_fan()
|
||||
if previous_error is not None: log("INFO","RGB_RECOVERED")
|
||||
previous_error=None
|
||||
except Exception as e:
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ let LANG=localStorage.getItem('pigwayLanguage')||'zh';
|
||||
const EN={
|
||||
'语言':'Language','颜色模式':'Appearance','中文':'中文','自动':'Auto','深色':'Dark','浅色':'Light','◐ 跟随系统':'◐ Follow system','☀ 浅色模式':'☀ Light mode','☾ 深色模式':'☾ Dark mode','正在连接树莓派…':'Connecting to Raspberry Pi…','在线':'Online','离线':'Offline',
|
||||
'运行状态':'Status','事件日志':'Event log','系统配置':'Settings','处理器负载':'CPU load','处理器温度':'CPU temperature','内存占用':'Memory usage','磁盘占用':'Disk usage','主网络':'Primary network','显示状态':'Display status','当前告警':'Active alerts','无告警':'No alerts','服务与进程监控':'Service and process monitors',
|
||||
'查询 systemd journal 中仍在保留的全部硬件 Agent 事件。':'Query all retained hardware Agent events in the systemd journal.','今天':'Today','昨天':'Yesterday','本周':'This week','本月':'This month','全部':'All','开始日期':'Start date','结束日期':'End date','严重程度':'Severity','全部程度':'All severities','调试':'Debug','信息':'Info','注意':'Notice','警告':'Warning','错误':'Error','严重':'Critical','紧急':'Emergency','事件类型':'Event type','全部事件':'All events','关键词':'Keyword','搜索日志内容':'Search log messages','查询':'Search','重置':'Reset','时间':'Time','程度':'Severity','内容':'Message','上一页':'Previous','下一页':'Next',
|
||||
'查询 systemd journal 中仍在保留的全部硬件 Agent 事件。':'Query all retained hardware Agent events in the systemd journal.','今天':'Today','昨天':'Yesterday','本周':'This week','本月':'This month','全部':'All','开始日期':'Start date','结束日期':'End date','严重程度':'Severity','全部程度':'All severities','调试':'Debug','信息':'Info','注意':'Notice','警告':'Warning','错误':'Error','严重':'Critical','紧急':'Emergency','事件类型':'Event type','全部事件':'All events','风扇档位变化':'Fan level changed','关键词':'Keyword','搜索日志内容':'Search log messages','查询':'Search','重置':'Reset','时间':'Time','程度':'Severity','内容':'Message','上一页':'Previous','下一页':'Next',
|
||||
'配置按用途分组,只显示当前模式需要的选项。保存时会备份配置并重启硬件 Agent。':'Settings are grouped by purpose and only relevant options are shown. Saving creates a backup and restarts the hardware Agent.','配置写入令牌(Bearer Token)':'Configuration Bearer Token','显示令牌':'Show token','隐藏令牌':'Hide token','保存并应用':'Save and apply','尚未修改':'No changes',
|
||||
'灯光设置':'Lighting','正常灯效与告警颜色':'Normal effects and alert colors','告警阈值':'Alert thresholds','处理器、内存、温度、磁盘和 Wi-Fi':'CPU, memory, temperature, disk and Wi-Fi','风扇温控':'Fan control','温度档位与降档回差':'Temperature levels and hysteresis','刷新周期':'Refresh intervals','传感器、屏幕、网络和健康检查':'Sensors, display, network and health checks','屏幕布局':'Display layout','OLED 四行文字位置':'OLED four-line positions','板载指示灯':'Board LEDs','树莓派电源灯与活动灯':'Raspberry Pi power and activity LEDs','服务监控':'Service monitoring','监控 systemd 服务':'Monitor systemd services','进程监控':'Process monitoring','监控普通进程关键字':'Monitor process keywords','Web 与 API':'Web and API','监听地址、端口和日志数量':'Bind address, port and log count',
|
||||
'正常运行灯效':'Normal lighting','没有告警时显示;告警出现后自动让位。':'Shown when no alert is active; alerts take priority.','告警对象颜色':'Alert category colors','告警对象决定颜色,严重程度决定闪烁速度。':'The alert category determines color; severity determines flash timing.','普通告警闪烁节奏':'Warning flash timing','严重告警闪烁节奏':'Critical flash timing','紧急告警闪烁节奏':'Emergency flash timing','硬件高级参数':'Advanced hardware settings','自定义颜色':'Custom color','点击色块打开色盘':'Click the swatch to open the color picker','颜色':'Color','红色通道':'Red channel','绿色通道':'Green channel','蓝色通道':'Blue channel','范围 0–255。':'Range: 0–255.','亮灯时间':'On time','灭灯时间':'Off time','毫秒':'ms','秒':'s','像素':'px','条':'entries',
|
||||
@@ -52,7 +52,7 @@ const options={'led.normal_mode':[['off','全灭'],['solid','自定义颜色常
|
||||
const numberRules={'led.normal_r':[0,255,1],'led.normal_g':[0,255,1],'led.normal_b':[0,255,1],'led.normal_flash_on_ms':[50,60000,10],'led.normal_flash_off_ms':[50,60000,10],'led.warning_flash_on_ms':[50,60000,10],'led.warning_flash_off_ms':[50,60000,10],'led.critical_flash_on_ms':[50,60000,10],'led.critical_flash_off_ms':[50,60000,10],'led.emergency_flash_on_ms':[50,60000,10],'led.emergency_flash_off_ms':[50,60000,10],'led.write_delay_ms':[1,1000,1],'alerts.cpu_high':[0,100,1],'alerts.cpu_recover':[0,100,1],'alerts.cpu_high_seconds':[1,86400,1],'alerts.memory_high':[0,100,1],'alerts.memory_recover':[0,100,1],'alerts.memory_high_seconds':[1,86400,1],'alerts.temp_high':[0,120,.1],'alerts.temp_critical':[0,120,.1],'alerts.disk_high':[0,100,1],'alerts.disk_critical':[0,100,1],'alerts.wifi_weak':[-120,0,1],'alerts.wifi_recover':[-120,0,1],'alerts.wifi_weak_seconds':[1,86400,1],'fan.start':[0,100,.1],'fan.level2':[0,100,.1],'fan.level3':[0,100,.1],'fan.level4':[0,100,.1],'fan.max':[0,100,.1],'fan.hysteresis':[0,20,.1],'timing.sensor_interval':[.2,3600,.1],'timing.oled_refresh_interval':[.2,3600,.1],'timing.page_interval':[1,3600,.1],'timing.network_interval':[1,3600,.1],'timing.service_interval':[1,3600,.1],'timing.health_interval':[.2,3600,.1],'display.row1_y':[0,31,1],'display.row2_y':[0,31,1],'display.row3_y':[0,31,1],'display.row4_y':[0,31,1],'api.port':[1,65535,1],'api.log_limit':[1,1000,1]};['cpu','power','memory','storage','network','service'].forEach(g=>['r','g','b'].forEach(c=>numberRules[`led.${g}_${c}`]=[0,255,1]));
|
||||
const units={'led.normal_flash_on_ms':'毫秒','led.normal_flash_off_ms':'毫秒','led.warning_flash_on_ms':'毫秒','led.warning_flash_off_ms':'毫秒','led.critical_flash_on_ms':'毫秒','led.critical_flash_off_ms':'毫秒','led.emergency_flash_on_ms':'毫秒','led.emergency_flash_off_ms':'毫秒','led.write_delay_ms':'毫秒','alerts.cpu_high':'%','alerts.cpu_recover':'%','alerts.cpu_high_seconds':'秒','alerts.memory_high':'%','alerts.memory_recover':'%','alerts.memory_high_seconds':'秒','alerts.temp_high':'°C','alerts.temp_critical':'°C','alerts.disk_high':'%','alerts.disk_critical':'%','alerts.wifi_weak':'dBm','alerts.wifi_recover':'dBm','alerts.wifi_weak_seconds':'秒','fan.start':'°C','fan.level2':'°C','fan.level3':'°C','fan.level4':'°C','fan.max':'°C','fan.hysteresis':'°C','timing.sensor_interval':'秒','timing.oled_refresh_interval':'秒','timing.page_interval':'秒','timing.network_interval':'秒','timing.service_interval':'秒','timing.health_interval':'秒','display.row1_y':'像素','display.row2_y':'像素','display.row3_y':'像素','display.row4_y':'像素','api.port':'端口','api.log_limit':'条'};
|
||||
const sectionOrder=['device','hosts','led','alerts','fan','timing','display','dark_mode','services','processes','api'],bulkSections=sectionOrder.filter(s=>!['device','hosts'].includes(s)),alertColorGroups=[['cpu','处理器、温度与散热'],['power','供电与欠压'],['memory','内存'],['storage','存储'],['network','网络与 Wi-Fi'],['service','服务与进程']];
|
||||
const ownerNames={NORMAL_HOME:'正常主页',ROOT_READONLY:'根文件系统只读',TEMP_CRITICAL:'处理器温度严重',POWER_UNDERVOLT:'供电欠压',THROTTLED:'处理器降频',FREQ_CAPPED:'频率受限',NETWORK_DOWN:'网络断开',INTERNET_DOWN:'互联网断开',SERVICE_DOWN:'服务停止',PROCESS_DOWN:'进程停止',DISK_CRITICAL:'磁盘严重不足',TEMP_HIGH:'处理器温度偏高',CPU_HIGH:'处理器负载过高',MEMORY_HIGH:'内存占用过高',DISK_HIGH:'磁盘空间不足',WIFI_WEAK:'Wi-Fi 信号弱'},rgbNames={OFF:'全灭',NORMAL_SOLID:'正常 · 自定义常亮',NORMAL_FLASH:'正常 · 自定义闪烁',NORMAL_FLOW:'正常 · 流水灯',NORMAL_BREATHE:'正常 · 呼吸灯',NORMAL_MARQUEE:'正常 · 跑马灯',NORMAL_RAINBOW:'正常 · 彩虹灯',NORMAL_COLORFUL:'正常 · 炫彩灯'},eventNames={START:'程序启动',STOP:'程序停止',ALERT_ACTIVE:'告警出现',ALERT_RECOVERED:'告警恢复',DISPLAY_OWNER:'显示对象切换',INTERNET_DOWN:'互联网断开',INTERNET_RECOVERED:'互联网恢复',NET_SWITCH:'网络接口切换',NETWORK_DOWN:'网络断开',NETWORK_RECOVERED:'网络恢复',POWER_LOW:'供电欠压',POWER_RECOVERED:'供电恢复',POWER_HISTORY:'历史电源状态',HISTORY_FLAG:'历史电源状态',ROOT_READONLY:'根文件系统只读',RGB_ERROR:'RGB 控制错误',RGB_MODE:'RGB 模式切换',RGB_RECOVERED:'RGB 控制恢复',SYSTEM:'系统事件',WATCH_DOWN:'监控对象停止',WATCH_RECOVERED:'监控对象恢复'};
|
||||
const ownerNames={NORMAL_HOME:'正常主页',ROOT_READONLY:'根文件系统只读',TEMP_CRITICAL:'处理器温度严重',POWER_UNDERVOLT:'供电欠压',THROTTLED:'处理器降频',FREQ_CAPPED:'频率受限',NETWORK_DOWN:'网络断开',INTERNET_DOWN:'互联网断开',SERVICE_DOWN:'服务停止',PROCESS_DOWN:'进程停止',DISK_CRITICAL:'磁盘严重不足',TEMP_HIGH:'处理器温度偏高',CPU_HIGH:'处理器负载过高',MEMORY_HIGH:'内存占用过高',DISK_HIGH:'磁盘空间不足',WIFI_WEAK:'Wi-Fi 信号弱'},rgbNames={OFF:'全灭',NORMAL_SOLID:'正常 · 自定义常亮',NORMAL_FLASH:'正常 · 自定义闪烁',NORMAL_FLOW:'正常 · 流水灯',NORMAL_BREATHE:'正常 · 呼吸灯',NORMAL_MARQUEE:'正常 · 跑马灯',NORMAL_RAINBOW:'正常 · 彩虹灯',NORMAL_COLORFUL:'正常 · 炫彩灯'},eventNames={FAN_LEVEL:'风扇档位变化',START:'程序启动',STOP:'程序停止',ALERT_ACTIVE:'告警出现',ALERT_RECOVERED:'告警恢复',DISPLAY_OWNER:'显示对象切换',INTERNET_DOWN:'互联网断开',INTERNET_RECOVERED:'互联网恢复',NET_SWITCH:'网络接口切换',NETWORK_DOWN:'网络断开',NETWORK_RECOVERED:'网络恢复',POWER_LOW:'供电欠压',POWER_RECOVERED:'供电恢复',POWER_HISTORY:'历史电源状态',HISTORY_FLAG:'历史电源状态',ROOT_READONLY:'根文件系统只读',RGB_ERROR:'RGB 控制错误',RGB_MODE:'RGB 模式切换',RGB_RECOVERED:'RGB 控制恢复',SYSTEM:'系统事件',WATCH_DOWN:'监控对象停止',WATCH_RECOVERED:'监控对象恢复'};
|
||||
function ownerName(id){const [type,suffix]=String(id||'').split('|',2),name=ownerNames[type]||type;return tr(name)+(suffix?` · ${suffix}`:'')}function rgbName(mode){if(rgbNames[mode])return tr(rgbNames[mode]);const p=String(mode||'').split('_'),groups={CPU:'处理器',POWER:'供电',MEMORY:'内存',STORAGE:'存储',NETWORK:'网络',SERVICE:'服务'},levels={WARNING:'普通告警',CRITICAL:'严重告警',EMERGENCY:'紧急告警'};return`${tr(groups[p[0]]||p[0])} · ${tr(levels[p[1]]||p[1])}${LANG==='zh'?'闪烁':' flash'}`}function eventName(id){return tr(eventNames[id]||(LANG==='zh'?`未知事件(${id})`:String(id).toLowerCase().replaceAll('_',' ')))}
|
||||
function field(section,key,value,extra=''){const id=`${section}.${key}`,opts=options[id],rule=numberRules[id],label=tr(labels[id]||key),help=tr(hints[id]||''),unit=tr(units[id]||'');let control;if(opts)control=`<select data-section="${esc(section)}" data-key="${esc(key)}">${opts.map(([v,l])=>`<option value="${v}" ${String(value)===v?'selected':''}>${tr(l)}</option>`).join('')}</select>`;else{const input=`<input ${rule?'type="number"':''} ${rule?`min="${rule[0]}" max="${rule[1]}" step="${rule[2]}"`:''} data-section="${esc(section)}" data-key="${esc(key)}" value="${esc(value)}">`;control=unit?`<div class="input-unit">${input}<span class="unit">${unit}</span></div>`:input}return`<div class="field ${extra}"><label>${esc(label)}${control}</label>${help?`<div class="hint">${esc(help)}</div>`:''}</div>`}
|
||||
function rgbGroup(values,prefix,title){const r=Number(values[`${prefix}_r`]||0),g=Number(values[`${prefix}_g`]||0),b=Number(values[`${prefix}_b`]||0),hex='#'+[r,g,b].map(x=>Math.max(0,Math.min(255,x)).toString(16).padStart(2,'0')).join('');return`<div class="group"><div class="group-head"><h3>${tr(title)}</h3><span class="muted">${tr('点击色块打开色盘')}</span></div><div class="rgb-row"><div class="color-field"><label>${tr('颜色')}</label><div class="swatch-wrap"><span class="swatch" data-swatch="${prefix}" style="background:${hex}"></span><input class="color-picker" type="color" data-color-prefix="${prefix}" value="${hex}" title="${LANG==='zh'?'选择':'Choose ' }${esc(tr(title))}"></div></div>${['r','g','b'].map(c=>field('led',`${prefix}_${c}`,values[`${prefix}_${c}`])).join('')}</div></div>`}
|
||||
|
||||
Reference in New Issue
Block a user