Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73907320c8 | ||
|
|
ad25c08eb1 | ||
|
|
5d541d5ef6 | ||
|
|
a8ca071b56 | ||
|
|
419d27b570 | ||
|
|
73a60b4866 | ||
|
|
576006b2fd |
@@ -1,4 +1,4 @@
|
|||||||
# Pigway Pi Control v3.6.0
|
# PIGWay Pi Control v3.6.0
|
||||||
|
|
||||||
Raspberry Pi 4B + 配套 128×32 OLED / 风扇 / RGB 散热扩展板的本机硬件监控 Agent。
|
Raspberry Pi 4B + 配套 128×32 OLED / 风扇 / RGB 散热扩展板的本机硬件监控 Agent。
|
||||||
|
|
||||||
@@ -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 记录。
|
每个当前故障都是 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 自行配色。内置效果只在模式切换时按 speed → color → effect 的实机校准顺序写一次寄存器,不循环刷 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. 结构化事件日志
|
### 6. 结构化事件日志
|
||||||
|
|
||||||
@@ -208,6 +210,7 @@ sudo ./install.sh
|
|||||||
- 安装必要依赖
|
- 安装必要依赖
|
||||||
- 检查 I²C,并在安装前检测 `/dev/i2c-1` 是否被其他 OLED/LED 控制进程占用
|
- 检查 I²C,并在安装前检测 `/dev/i2c-1` 是否被其他 OLED/LED 控制进程占用
|
||||||
- 发现冲突时列出进程并退出,由用户决定如何处理;安装器不会停止、禁用或杀死其他服务
|
- 发现冲突时列出进程并退出,由用户决定如何处理;安装器不会停止、禁用或杀死其他服务
|
||||||
|
- 确认需要多个进程共用 I²C 时,可用 `sudo ./install.sh --force-i2c-conflict` 强制继续;冲突进程仍由用户自行管理
|
||||||
- 安装字体、程序、配置和 systemd unit
|
- 安装字体、程序、配置和 systemd unit
|
||||||
- 完整保留用户现有配置(包括所有 section、用户值和注释),仅补入缺失的 `[led]` 和 `[api]` 选项,已有用户值优先
|
- 完整保留用户现有配置(包括所有 section、用户值和注释),仅补入缺失的 `[led]` 和 `[api]` 选项,已有用户值优先
|
||||||
- 生成只读 root token,安装 API/Web 文件
|
- 生成只读 root token,安装 API/Web 文件
|
||||||
|
|||||||
+8
-8
@@ -1,10 +1,10 @@
|
|||||||
0960943a15ae23cd067f1169df6640723500107e8c41af070b0c9b889f29914e README.md
|
a182abe8c1f188b9e9b47fdc24802fedd6a569b462a85254185521fe68936a76 README.md
|
||||||
72c46162c33f9587c6ccb01ad53fbaaf4ecec8cd21014b909e87e8707e253e36 app/oled_font_5x7.bin
|
72c46162c33f9587c6ccb01ad53fbaaf4ecec8cd21014b909e87e8707e253e36 app/oled_font_5x7.bin
|
||||||
29f59e41052e1e251bf2f38274cdd321fc195602220b61ac8890202128d7c50a app/pigway_pi_control.py
|
d71d949643d31461f2e62ded3818e1a0f1711d1420756ad0cc32eb04cde0fad3 app/pigway_pi_control.py
|
||||||
f32be5b63fe2be51e39c106c232ae3cc14dab7c0c3719e2609b9e93a7802b6c3 app/pigway_pi_control_api.py
|
3a13b4e8f9d306e77f82eba78b78fe9b1932488884d6f3f976754bfa94f0153d app/pigway_pi_control_api.py
|
||||||
9f3b2be557b3cd93c309f8a1cadce7d34b23ca06c1ff69231d934e8180fefb48 config/pigway-pi-control.conf
|
75fa662ed21138a8e394de83e46281dab039433cc0d7a8f787e1a049f4b47ec8 config/pigway-pi-control.conf
|
||||||
15b431e62b00886b6561597db059f01f116b5173bdae7a5817f617f7bc73a3ec install.sh
|
1276369d600e9fd560d03e26146bcaeb40a353829978d1ebaa578d385b27af72 install.sh
|
||||||
81aea0a89b298b2512fcb9bf6418618ba7a66474d425390cf95a0128c4093474 systemd/pigway-pi-control.service
|
4f6e8db3698a62f57823d3298a0e6814593de6fa3d31e4751b7ea4a76e553e35 systemd/pigway-pi-control.service
|
||||||
5d7562c496de597f61b62af48f8f6350ad270e1ebf725426d9f1e5b5d698055e systemd/pigway-pi-control-api.service
|
aa21cef9a06593371ccff005335b6c0c826fbf4b5c48e4255b5f4b75ce6e9217 systemd/pigway-pi-control-api.service
|
||||||
9f59e7313ee58e687ae6dd29d175c67082674254ac5c5f54168b2735b64c20a4 uninstall.sh
|
9f59e7313ee58e687ae6dd29d175c67082674254ac5c5f54168b2735b64c20a4 uninstall.sh
|
||||||
4b4066d104202f664f5df07c97d4e256dde32b5f4426e6a955a3ecef5e08df60 web/index.html
|
7694998270db13407c197707ee41f8fb8d8d9ffa8d2325f20d7dd7719ab46815 web/index.html
|
||||||
|
|||||||
@@ -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)))
|
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)))
|
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)))
|
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):
|
def rgb_cfg(name,default):
|
||||||
return tuple(max(0,min(255,cf("led",f"{name}_{channel}",value,int)))
|
return tuple(max(0,min(255,cf("led",f"{name}_{channel}",value,int)))
|
||||||
@@ -441,6 +455,29 @@ def rgb_flash(mode,on):
|
|||||||
mcu(0x03,channels[2])
|
mcu(0x03,channels[2])
|
||||||
_last_rgb=state
|
_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
|
||||||
|
# Hardware-calibrated order: speed and color are parameters; the final
|
||||||
|
# effect register write commits and starts the selected MCU animation.
|
||||||
|
mcu(0x05,NORMAL_EFFECT_SPEED); time.sleep(RGB_WRITE_DELAY_MS/1000.0)
|
||||||
|
mcu(0x06,RGB_EFFECT_COLORS[NORMAL_EFFECT_COLOR]); time.sleep(RGB_WRITE_DELAY_MS/1000.0)
|
||||||
|
mcu(0x04,RGB_EFFECTS[effect_name])
|
||||||
|
_last_rgb=state
|
||||||
|
|
||||||
def render_home(cpu,temp,mem,disk):
|
def render_home(cpu,temp,mem,disk):
|
||||||
# 128x32 fixed columns. Both columns are LEFT-aligned.
|
# 128x32 fixed columns. Both columns are LEFT-aligned.
|
||||||
# Seller 5x7 font has 6px character pitch; right column starts at x=72.
|
# Seller 5x7 font has 6px character pitch; right column starts at x=72.
|
||||||
@@ -485,7 +522,7 @@ class DisplayController:
|
|||||||
now=time.monotonic()
|
now=time.monotonic()
|
||||||
self.lock=threading.RLock()
|
self.lock=threading.RLock()
|
||||||
self.requests={"NORMAL_HOME":{
|
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,
|
"title":"","l2":"","l3":"","l4":"","active":True,
|
||||||
"first_seen":now,"last_changed":now,"metadata":{"home":(0.0,0.0,0.0,0.0)},
|
"first_seen":now,"last_changed":now,"metadata":{"home":(0.0,0.0,0.0,0.0)},
|
||||||
}}
|
}}
|
||||||
@@ -637,18 +674,27 @@ class DisplayController:
|
|||||||
if wanted_mode!=applied_mode:
|
if wanted_mode!=applied_mode:
|
||||||
with bus_lock:
|
with bus_lock:
|
||||||
if wanted_mode=="OFF": rgb_off()
|
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}")
|
else: raise ValueError(f"unsupported RGB mode: {wanted_mode}")
|
||||||
log("INFO","RGB_MODE",old=applied_mode,new=wanted_mode)
|
log("INFO","RGB_MODE",old=applied_mode,new=wanted_mode)
|
||||||
applied_mode=wanted_mode; animation_started=time.monotonic()
|
applied_mode=wanted_mode; animation_started=time.monotonic()
|
||||||
applied_flash=applied_mode.endswith("_FLASH")
|
applied_flash=applied_mode.endswith("_FLASH")
|
||||||
if applied_flash:
|
if applied_flash:
|
||||||
elapsed_ms=(time.monotonic()-animation_started)*1000.0
|
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
|
elif "_CRITICAL_" in applied_mode: on_ms,off_ms=CRITICAL_FLASH
|
||||||
else: on_ms,off_ms=WARNING_FLASH
|
else: on_ms,off_ms=WARNING_FLASH
|
||||||
on=elapsed_ms%(on_ms+off_ms)<on_ms
|
on=elapsed_ms%(on_ms+off_ms)<on_ms
|
||||||
with bus_lock: rgb_flash(applied_mode,on)
|
with bus_lock:
|
||||||
|
if applied_mode=="NORMAL_FLASH":
|
||||||
|
rgb_static(NORMAL_COLOR,"normal_flash_on") if on else rgb_off()
|
||||||
|
else: rgb_flash(applied_mode,on)
|
||||||
if previous_error is not None: log("INFO","RGB_RECOVERED")
|
if previous_error is not None: log("INFO","RGB_RECOVERED")
|
||||||
previous_error=None
|
previous_error=None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ TOKEN=Path("/etc/pigway-pi-control-api.token")
|
|||||||
WEB=Path("/usr/local/share/pigway-pi-control/web/index.html")
|
WEB=Path("/usr/local/share/pigway-pi-control/web/index.html")
|
||||||
ALLOWED_SECTIONS={"services","processes","alerts","fan","led","timing","dark_mode","display","api"}
|
ALLOWED_SECTIONS={"services","processes","alerts","fan","led","timing","dark_mode","display","api"}
|
||||||
NAME_RE=re.compile(r"^[A-Za-z0-9_.-]{1,64}$")
|
NAME_RE=re.compile(r"^[A-Za-z0-9_.-]{1,64}$")
|
||||||
RGB_KEY_RE=re.compile(r"^(cpu|power|memory|storage|network|service)_[rgb]$")
|
RGB_KEY_RE=re.compile(r"^(cpu|power|memory|storage|network|service|normal)_[rgb]$")
|
||||||
FLASH_KEY_RE=re.compile(r"^(warning|critical|emergency)_flash_(on|off)_ms$")
|
FLASH_KEY_RE=re.compile(r"^(warning|critical|emergency|normal)_flash_(on|off)_ms$")
|
||||||
|
NORMAL_MODES={"off","solid","flash","flow","breathe","marquee","rainbow","colorful"}
|
||||||
|
EFFECT_COLORS={"red","green","blue","yellow","purple","cyan","white"}
|
||||||
|
|
||||||
def load_cfg():
|
def load_cfg():
|
||||||
cfg=configparser.ConfigParser(); cfg.read(CFG)
|
cfg=configparser.ConfigParser(); cfg.read(CFG)
|
||||||
@@ -36,6 +38,12 @@ def validate_value(section,key,value,existing):
|
|||||||
elif FLASH_KEY_RE.fullmatch(key):
|
elif FLASH_KEY_RE.fullmatch(key):
|
||||||
number=int(value)
|
number=int(value)
|
||||||
if not 50<=number<=60000: raise ValueError(f"{key} must be 50..60000")
|
if not 50<=number<=60000: raise ValueError(f"{key} must be 50..60000")
|
||||||
|
elif section=="led" and key=="normal_mode":
|
||||||
|
if value.lower() not in NORMAL_MODES: raise ValueError("normal_mode must be off, solid, flash, flow, breathe, marquee, rainbow or colorful")
|
||||||
|
elif section=="led" and key=="normal_effect_speed":
|
||||||
|
if int(value) not in (1,2,3): raise ValueError("normal_effect_speed must be 1, 2 or 3")
|
||||||
|
elif section=="led" and key=="normal_effect_color":
|
||||||
|
if value.lower() not in EFFECT_COLORS: raise ValueError("normal_effect_color must be red, green, blue, yellow, purple, cyan or white")
|
||||||
elif section=="api" and key=="port":
|
elif section=="api" and key=="port":
|
||||||
number=int(value)
|
number=int(value)
|
||||||
if not 1<=number<=65535: raise ValueError("api.port must be 1..65535")
|
if not 1<=number<=65535: raise ValueError("api.port must be 1..65535")
|
||||||
@@ -95,7 +103,7 @@ def journal(limit):
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
server_version="PigwayAPI/3.6"
|
server_version="PIGWayAPI/3.6"
|
||||||
|
|
||||||
def log_message(self,fmt,*args):
|
def log_message(self,fmt,*args):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Pigway Pi Control v3.6.0 - 用户配置文件
|
# PIGWay Pi Control v3.6.0 - 用户配置文件
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
#
|
#
|
||||||
# 正式安装后的配置路径:
|
# 正式安装后的配置路径:
|
||||||
@@ -147,6 +147,34 @@ wifi_weak_seconds = 30
|
|||||||
|
|
||||||
|
|
||||||
[led]
|
[led]
|
||||||
|
# 正常运行、没有任何 active alert 时的 RGB 表现:
|
||||||
|
# off = 全灭(默认,最安静且 I2C 写入最少)
|
||||||
|
# solid = 三灯使用 normal_r/g/b 自定义颜色常亮
|
||||||
|
# flash = 三灯使用 normal_r/g/b 和 normal_flash_*_ms 自定义颜色闪烁
|
||||||
|
# flow = MCU 内置流水灯;使用有限颜色和速度
|
||||||
|
# breathe = MCU 内置呼吸灯;使用有限颜色和速度
|
||||||
|
# marquee = MCU 内置跑马灯;MCU 自行决定颜色变化
|
||||||
|
# rainbow = MCU 内置彩虹灯;MCU 自行决定颜色变化
|
||||||
|
# colorful = MCU 内置炫彩灯;MCU 自行决定颜色变化
|
||||||
|
# 任何 active alert 都会立即抢占正常效果;全部恢复后才重新应用这里的模式。
|
||||||
|
normal_mode = off
|
||||||
|
|
||||||
|
# solid / flash 自定义颜色,三通道范围均为 0..255。
|
||||||
|
normal_r = 0
|
||||||
|
normal_g = 0
|
||||||
|
normal_b = 0
|
||||||
|
|
||||||
|
# flash 亮灯/灭灯时间,单位毫秒;范围 50..60000。
|
||||||
|
normal_flash_on_ms = 1000
|
||||||
|
normal_flash_off_ms = 1000
|
||||||
|
|
||||||
|
# MCU 内置效果速度:1=慢,2=中(默认),3=快。
|
||||||
|
normal_effect_speed = 2
|
||||||
|
# flow / breathe 可使用的 MCU 内置有限颜色:
|
||||||
|
# red / green / blue / yellow / purple / cyan / white
|
||||||
|
# marquee / rainbow / colorful 会忽略该颜色值。
|
||||||
|
normal_effect_color = green
|
||||||
|
|
||||||
# DisplayController 按集中定义的 priority 仲裁 owner。告警页显示时 RGB 同步显示
|
# DisplayController 按集中定义的 priority 仲裁 owner。告警页显示时 RGB 同步显示
|
||||||
# 该页对象;主页使用最高优先级 active owner。颜色表示对象,速度表示严重等级:
|
# 该页对象;主页使用最高优先级 active owner。颜色表示对象,速度表示严重等级:
|
||||||
# CPU/频率/温度/散热 = 红色
|
# CPU/频率/温度/散热 = 红色
|
||||||
@@ -183,8 +211,6 @@ critical_flash_on_ms = 750
|
|||||||
critical_flash_off_ms = 750
|
critical_flash_off_ms = 750
|
||||||
emergency_flash_on_ms = 300
|
emergency_flash_on_ms = 300
|
||||||
emergency_flash_off_ms = 300
|
emergency_flash_off_ms = 300
|
||||||
# NORMAL_HOME 为 owner 时 RGB OFF。
|
|
||||||
#
|
|
||||||
# 每次点亮使用 v3.2.0 已验证的 selector -> R -> G -> B 完整写入顺序,
|
# 每次点亮使用 v3.2.0 已验证的 selector -> R -> G -> B 完整写入顺序,
|
||||||
# 只在亮/灭边沿访问 MCU,不做高频软件 PWM。
|
# 只在亮/灭边沿访问 MCU,不做高频软件 PWM。
|
||||||
write_delay_ms = 10
|
write_delay_ms = 10
|
||||||
@@ -199,7 +225,7 @@ write_delay_ms = 10
|
|||||||
# Raspberry Pi 4 板载 PWR / ACT LED。
|
# Raspberry Pi 4 板载 PWR / ACT LED。
|
||||||
# 本机硬件只支持开/关,不支持真正的 PWM 调暗。
|
# 本机硬件只支持开/关,不支持真正的 PWM 调暗。
|
||||||
#
|
#
|
||||||
# off = Pigway Pi Control 启动时关闭该灯。
|
# off = PIGWay Pi Control 启动时关闭该灯。
|
||||||
# 若不希望程序干预,可改为 on。
|
# 若不希望程序干预,可改为 on。
|
||||||
#
|
#
|
||||||
# 注意:
|
# 注意:
|
||||||
|
|||||||
+39
-5
@@ -2,6 +2,19 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
FORCE_I2C_CONFLICT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--force-i2c-conflict) FORCE_I2C_CONFLICT=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: sudo ./install.sh [--force-i2c-conflict]"
|
||||||
|
echo " --force-i2c-conflict Continue even when another process owns /dev/i2c-1."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) echo "ERROR: unknown option: $arg" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
for required in app/pigway_pi_control.py app/pigway_pi_control_api.py app/oled_font_5x7.bin web/index.html config/pigway-pi-control.conf systemd/pigway-pi-control.service systemd/pigway-pi-control-api.service; do
|
for required in app/pigway_pi_control.py app/pigway_pi_control_api.py app/oled_font_5x7.bin web/index.html config/pigway-pi-control.conf systemd/pigway-pi-control.service systemd/pigway-pi-control-api.service; do
|
||||||
if [ ! -f "$required" ]; then
|
if [ ! -f "$required" ]; then
|
||||||
echo "ERROR: required file missing: $required" >&2
|
echo "ERROR: required file missing: $required" >&2
|
||||||
@@ -30,12 +43,21 @@ i2c_conflict_pids() {
|
|||||||
|
|
||||||
I2C_CONFLICTS="$(i2c_conflict_pids)"
|
I2C_CONFLICTS="$(i2c_conflict_pids)"
|
||||||
if [ -n "$I2C_CONFLICTS" ]; then
|
if [ -n "$I2C_CONFLICTS" ]; then
|
||||||
echo "ERROR: /dev/i2c-1 is already in use by another process:" >&2
|
if [ "$FORCE_I2C_CONFLICT" -eq 1 ]; then
|
||||||
|
echo "WARNING: /dev/i2c-1 is already in use by another process:" >&2
|
||||||
|
else
|
||||||
|
echo "ERROR: /dev/i2c-1 is already in use by another process:" >&2
|
||||||
|
fi
|
||||||
while read -r pid; do
|
while read -r pid; do
|
||||||
ps -p "$pid" -o pid=,user=,comm=,args= >&2 || true
|
ps -p "$pid" -o pid=,user=,comm=,args= >&2 || true
|
||||||
done <<< "$I2C_CONFLICTS"
|
done <<< "$I2C_CONFLICTS"
|
||||||
echo "Stop or reconfigure the conflicting OLED/LED controller, then run the installer again." >&2
|
if [ "$FORCE_I2C_CONFLICT" -eq 1 ]; then
|
||||||
exit 1
|
echo "WARNING: continuing because --force-i2c-conflict was specified; no conflicting process will be stopped." >&2
|
||||||
|
else
|
||||||
|
echo "Stop or reconfigure the conflicting OLED/LED controller, then run the installer again." >&2
|
||||||
|
echo "If concurrent control is intentional, rerun with --force-i2c-conflict." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[2/8] 等待 APT/DPKG..."
|
echo "[2/8] 等待 APT/DPKG..."
|
||||||
@@ -81,6 +103,10 @@ path=Path(sys.argv[1])
|
|||||||
text=path.read_text()
|
text=path.read_text()
|
||||||
sections={
|
sections={
|
||||||
"led":(
|
"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),
|
("cpu_r",255),("cpu_g",0),("cpu_b",0),
|
||||||
("power_r",255),("power_g",40),("power_b",0),
|
("power_r",255),("power_g",40),("power_b",0),
|
||||||
("memory_r",255),("memory_g",0),("memory_b",255),
|
("memory_r",255),("memory_g",0),("memory_b",255),
|
||||||
@@ -107,6 +133,10 @@ for section,defaults in sections.items():
|
|||||||
if not re.search(rf"(?m)^\s*{re.escape(key)}\s*=",chunk)]
|
if not re.search(rf"(?m)^\s*{re.escape(key)}\s*=",chunk)]
|
||||||
if missing:
|
if missing:
|
||||||
note="# Added by installer; existing user values are preserved.\n"
|
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)
|
block=note+"".join(f"{key} = {value}\n" for key,value in missing)
|
||||||
text=text[:end].rstrip()+"\n"+block+"\n\n"+text[end:].lstrip("\n")
|
text=text[:end].rstrip()+"\n"+block+"\n\n"+text[end:].lstrip("\n")
|
||||||
path.write_text(text)
|
path.write_text(text)
|
||||||
@@ -139,8 +169,12 @@ check "OLED pages 0..3" grep -Fq 'cmd(0x22);cmd(0);cmd(3)' "$APP"
|
|||||||
check "UI CPU/TMP/DSK" grep -Fq 'f"TMP {temp:.1f}C"' "$APP"
|
check "UI CPU/TMP/DSK" grep -Fq 'f"TMP {temp:.1f}C"' "$APP"
|
||||||
check "UI IP4/IP6" grep -Fq 'ip_label' "$APP"
|
check "UI IP4/IP6" grep -Fq 'ip_label' "$APP"
|
||||||
check "UI right column x=72" grep -Fq 'draw_text_5x7(pix,72,y,right[row])' "$APP"
|
check "UI right column x=72" grep -Fq 'draw_text_5x7(pix,72,y,right[row])' "$APP"
|
||||||
no_other_i2c_owner() { [ -z "$(i2c_conflict_pids)" ]; }
|
if [ "$FORCE_I2C_CONFLICT" -eq 1 ]; then
|
||||||
check "No other I2C owner" no_other_i2c_owner
|
printf " %-30s WARN (forced)\n" "I2C owner conflict"
|
||||||
|
else
|
||||||
|
no_other_i2c_owner() { [ -z "$(i2c_conflict_pids)" ]; }
|
||||||
|
check "No other I2C owner" no_other_i2c_owner
|
||||||
|
fi
|
||||||
check "Service running" systemctl is-active --quiet pigway-pi-control.service
|
check "Service running" systemctl is-active --quiet pigway-pi-control.service
|
||||||
check "API/Web service running" systemctl is-active --quiet pigway-pi-control-api.service
|
check "API/Web service running" systemctl is-active --quiet pigway-pi-control-api.service
|
||||||
check "API/Web version 3.6.0" grep -Fq 'APP_VERSION="3.6.0"' /usr/local/sbin/pigway-pi-control-api
|
check "API/Web version 3.6.0" grep -Fq 'APP_VERSION="3.6.0"' /usr/local/sbin/pigway-pi-control-api
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Pigway Pi Control API and Web
|
Description=PIGWay Pi Control API and Web
|
||||||
After=network.target pigway-pi-control.service
|
After=network.target pigway-pi-control.service
|
||||||
Wants=network.target pigway-pi-control.service
|
Wants=network.target pigway-pi-control.service
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Pigway Raspberry Pi OLED/Fan/RGB Monitor
|
Description=PIGWay Raspberry Pi OLED/Fan/RGB Monitor
|
||||||
After=multi-user.target network.target
|
After=multi-user.target network.target
|
||||||
Wants=network.target
|
Wants=network.target
|
||||||
|
|
||||||
|
|||||||
+17
-5
@@ -2,13 +2,13 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Pigway Pi Control</title>
|
<title>PIGWay Pi Control</title>
|
||||||
<style>
|
<style>
|
||||||
:root{color-scheme:dark;--bg:#0b1118;--panel:#121b25;--line:#263546;--text:#e7edf5;--muted:#91a4b8;--good:#50d890;--warn:#ffbd4a;--bad:#ff6b6b;--blue:#58a6ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top right,#14263b 0,#0b1118 42%);color:var(--text);font:14px/1.45 system-ui,-apple-system,sans-serif}main{max-width:1180px;margin:auto;padding:28px 20px 60px}header{display:flex;justify-content:space-between;align-items:end;gap:18px;margin-bottom:22px}h1{font-size:27px;margin:0}h2{font-size:17px;margin:0 0 14px}.sub,.muted{color:var(--muted)}.live{display:flex;align-items:center;gap:8px}.dot{width:9px;height:9px;border-radius:50%;background:var(--bad)}.dot.ok{background:var(--good);box-shadow:0 0 12px var(--good)}nav{display:flex;gap:8px;margin:18px 0}button{border:1px solid var(--line);background:#162230;color:var(--text);border-radius:8px;padding:9px 13px;cursor:pointer}button:hover,button.active{border-color:var(--blue);background:#1b3047}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.card,.panel{background:color-mix(in srgb,var(--panel) 94%,transparent);border:1px solid var(--line);border-radius:12px;padding:16px;box-shadow:0 12px 28px #0004}.value{font-size:23px;font-weight:700;margin-top:4px}.wide{grid-column:span 2}.panel{margin-top:12px}.rows{display:grid;gap:8px}.row{display:flex;justify-content:space-between;gap:16px;padding:8px 0;border-bottom:1px solid #203040}.row:last-child{border:0}.badge{border-radius:999px;padding:3px 8px;background:#243447}.badge.good{color:var(--good)}.badge.bad{color:var(--bad)}.hidden{display:none}.log{font:12px/1.5 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;padding:8px 0;border-bottom:1px solid #203040}.config-section{margin:10px 0;border:1px solid var(--line);border-radius:10px;padding:12px}.config-section summary{cursor:pointer;font-weight:700}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:12px}.field label{display:block;color:var(--muted);font-size:12px}.field input{width:100%;margin-top:4px;border:1px solid var(--line);border-radius:7px;background:#0d151e;color:var(--text);padding:8px}.toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:12px 0}.toolbar input{min-width:280px;flex:1;border:1px solid var(--line);border-radius:7px;background:#0d151e;color:var(--text);padding:9px}.error{color:var(--bad)}@media(max-width:800px){.grid{grid-template-columns:repeat(2,1fr)}.fields{grid-template-columns:1fr}.wide{grid-column:span 2}}@media(max-width:470px){.grid{grid-template-columns:1fr}.wide{grid-column:span 1}header{align-items:start;flex-direction:column}}
|
:root{color-scheme:dark;--bg:#0b1118;--panel:#121b25;--line:#263546;--text:#e7edf5;--muted:#91a4b8;--good:#50d890;--warn:#ffbd4a;--bad:#ff6b6b;--blue:#58a6ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top right,#14263b 0,#0b1118 42%);color:var(--text);font:14px/1.45 system-ui,-apple-system,sans-serif}main{max-width:1180px;margin:auto;padding:28px 20px 60px}header{display:flex;justify-content:space-between;align-items:end;gap:18px;margin-bottom:22px}h1{font-size:27px;margin:0}h2{font-size:17px;margin:0 0 14px}.sub,.muted{color:var(--muted)}.live{display:flex;align-items:center;gap:8px}.dot{width:9px;height:9px;border-radius:50%;background:var(--bad)}.dot.ok{background:var(--good);box-shadow:0 0 12px var(--good)}nav{display:flex;gap:8px;margin:18px 0}button{border:1px solid var(--line);background:#162230;color:var(--text);border-radius:8px;padding:9px 13px;cursor:pointer}button:hover,button.active{border-color:var(--blue);background:#1b3047}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.card,.panel{background:color-mix(in srgb,var(--panel) 94%,transparent);border:1px solid var(--line);border-radius:12px;padding:16px;box-shadow:0 12px 28px #0004}.value{font-size:23px;font-weight:700;margin-top:4px}.wide{grid-column:span 2}.panel{margin-top:12px}.rows{display:grid;gap:8px}.row{display:flex;justify-content:space-between;gap:16px;padding:8px 0;border-bottom:1px solid #203040}.row:last-child{border:0}.badge{border-radius:999px;padding:3px 8px;background:#243447}.badge.good{color:var(--good)}.badge.bad{color:var(--bad)}.hidden{display:none}.log{font:12px/1.5 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;padding:8px 0;border-bottom:1px solid #203040}.config-section{margin:10px 0;border:1px solid var(--line);border-radius:10px;padding:12px}.config-section summary{cursor:pointer;font-weight:700}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:12px}.field label{display:block;color:var(--muted);font-size:12px}.field input,.field select{width:100%;margin-top:4px;border:1px solid var(--line);border-radius:7px;background:#0d151e;color:var(--text);padding:8px}.hint{color:var(--muted);font-size:11px;margin-top:4px}.toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:12px 0}.toolbar input{min-width:280px;flex:1;border:1px solid var(--line);border-radius:7px;background:#0d151e;color:var(--text);padding:9px}.error{color:var(--bad)}@media(max-width:800px){.grid{grid-template-columns:repeat(2,1fr)}.fields{grid-template-columns:1fr}.wide{grid-column:span 2}}@media(max-width:470px){.grid{grid-template-columns:1fr}.wide{grid-column:span 1}header{align-items:start;flex-direction:column}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body><main>
|
<body><main>
|
||||||
<header><div><h1>Pigway Pi Control</h1><div class="sub" id="identity">正在连接 Raspberry Pi…</div></div><div class="live"><span class="dot" id="dot"></span><span id="liveText">离线</span></div></header>
|
<header><div><h1>PIGWay Pi Control</h1><div class="sub" id="identity">正在连接 Raspberry Pi…</div></div><div class="live"><span class="dot" id="dot"></span><span id="liveText">离线</span></div></header>
|
||||||
<nav><button class="active" data-tab="overview">状态</button><button data-tab="logs">日志</button><button data-tab="config">配置</button></nav>
|
<nav><button class="active" data-tab="overview">状态</button><button data-tab="logs">日志</button><button data-tab="config">配置</button></nav>
|
||||||
<section id="overview">
|
<section id="overview">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
@@ -28,13 +28,25 @@
|
|||||||
<script>
|
<script>
|
||||||
const $=id=>document.getElementById(id), tabs=[...document.querySelectorAll('nav button')];let original={};
|
const $=id=>document.getElementById(id), tabs=[...document.querySelectorAll('nav button')];let original={};
|
||||||
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
|
const configOptions={
|
||||||
|
'led.normal_mode':[['off','全灭'],['solid','自定义常亮'],['flash','自定义闪烁'],['flow','流水灯'],['breathe','呼吸灯'],['marquee','跑马灯'],['rainbow','彩虹灯'],['colorful','炫彩灯']],
|
||||||
|
'led.normal_effect_speed':[['1','慢'],['2','中'],['3','快']],
|
||||||
|
'led.normal_effect_color':[['red','红'],['green','绿'],['blue','蓝'],['yellow','黄'],['purple','紫'],['cyan','青'],['white','白']]
|
||||||
|
};
|
||||||
|
const configHints={
|
||||||
|
'led.normal_mode':'仅在无告警时生效;告警会立即抢占。',
|
||||||
|
'led.normal_r':'常亮/闪烁红通道,0..255。','led.normal_g':'常亮/闪烁绿通道,0..255。','led.normal_b':'常亮/闪烁蓝通道,0..255。',
|
||||||
|
'led.normal_flash_on_ms':'闪烁亮灯时间,50..60000 ms。','led.normal_flash_off_ms':'闪烁灭灯时间,50..60000 ms。',
|
||||||
|
'led.normal_effect_speed':'MCU 内置效果速度。','led.normal_effect_color':'仅流水灯和呼吸灯使用;其他内置效果忽略。'
|
||||||
|
};
|
||||||
|
function configField(section,key,value){const id=`${section}.${key}`,opts=configOptions[id],hint=configHints[id]||'';const control=opts?`<select data-section="${esc(section)}" data-key="${esc(key)}">${opts.map(([v,l])=>`<option value="${v}" ${String(value)===v?'selected':''}>${l} (${v})</option>`).join('')}</select>`:`<input data-section="${esc(section)}" data-key="${esc(key)}" value="${esc(value)}">`;return `<div class="field"><label>${esc(key)}${control}</label>${hint?`<div class="hint">${esc(hint)}</div>`:''}</div>`}
|
||||||
tabs.forEach(b=>b.onclick=()=>{tabs.forEach(x=>x.classList.toggle('active',x===b));['overview','logs','config'].forEach(id=>$(id).classList.toggle('hidden',id!==b.dataset.tab));if(b.dataset.tab==='logs')loadLogs();if(b.dataset.tab==='config')loadConfig()});
|
tabs.forEach(b=>b.onclick=()=>{tabs.forEach(x=>x.classList.toggle('active',x===b));['overview','logs','config'].forEach(id=>$(id).classList.toggle('hidden',id!==b.dataset.tab));if(b.dataset.tab==='logs')loadLogs();if(b.dataset.tab==='config')loadConfig()});
|
||||||
async function get(url,opt){const r=await fetch(url,opt);const j=await r.json();if(!r.ok)throw Error(j.error||r.statusText);return j}
|
async function get(url,opt){const r=await fetch(url,opt);const j=await r.json();if(!r.ok)throw Error(j.error||r.statusText);return j}
|
||||||
function rows(target,items,empty='暂无数据'){$(target).innerHTML=items.length?items.join(''):`<div class="muted">${empty}</div>`}
|
function rows(target,items,empty='暂无数据'){$(target).innerHTML=items.length?items.join(''):`<div class="muted">${empty}</div>`}
|
||||||
async function loadStatus(){try{const s=await get('/api/v1/status'),y=s.system,n=s.network,d=s.display;$('dot').classList.add('ok');$('liveText').textContent='在线';$('identity').textContent=`v${s.version} · 运行 ${Math.floor(s.uptime_seconds/3600)} 小时 · ${new Date(s.timestamp*1000).toLocaleTimeString()}`;$('cpu').textContent=`${y.cpu_percent}%`;$('freq').textContent=`${y.cpu_freq_mhz} MHz`;$('temp').textContent=`${y.temperature_c}°C`;$('fan').textContent=`风扇 ${y.fan_name}`;$('memory').textContent=`${y.memory_percent}%`;$('disk').textContent=`${y.disk_percent}%`;$('network').textContent=`${n.kind} ${n.metric}`;$('address').textContent=`${n.interface} · ${n.ip_label} ${n.ip} · Internet ${n.internet?'OK':'DOWN'}`;$('owner').textContent=d.owner;$('rgb').textContent=`OLED ${d.oled_page} · RGB ${d.rgb_mode}`;rows('alerts',d.alerts.map(a=>`<div class="row"><span><b>${esc(a.title)}</b><br><span class="muted">${esc(a.l2)} ${esc(a.l3)}</span></span><span class="badge bad">S${a.severity} · P${a.priority}</span></div>`),'无告警');rows('watches',s.watches.map(w=>`<div class="row"><span><b>${esc(w.name)}</b><br><span class="muted">${esc(w.type)} · ${esc(w.target)}</span></span><span class="badge ${w.up?'good':'bad'}">${w.up?'UP':'DOWN'}</span></div>`))}catch(e){$('dot').classList.remove('ok');$('liveText').textContent='离线';$('identity').textContent=e.message}}
|
async function loadStatus(){try{const s=await get('/api/v1/status'),y=s.system,n=s.network,d=s.display;$('dot').classList.add('ok');$('liveText').textContent='在线';$('identity').textContent=`v${s.version} · 运行 ${Math.floor(s.uptime_seconds/3600)} 小时 · ${new Date(s.timestamp*1000).toLocaleTimeString()}`;$('cpu').textContent=`${y.cpu_percent}%`;$('freq').textContent=`${y.cpu_freq_mhz} MHz`;$('temp').textContent=`${y.temperature_c}°C`;$('fan').textContent=`风扇 ${y.fan_name}`;$('memory').textContent=`${y.memory_percent}%`;$('disk').textContent=`${y.disk_percent}%`;$('network').textContent=`${n.kind} ${n.metric}`;$('address').textContent=`${n.interface} · ${n.ip_label} ${n.ip} · Internet ${n.internet?'OK':'DOWN'}`;$('owner').textContent=d.owner;$('rgb').textContent=`OLED ${d.oled_page} · RGB ${d.rgb_mode}`;rows('alerts',d.alerts.map(a=>`<div class="row"><span><b>${esc(a.title)}</b><br><span class="muted">${esc(a.l2)} ${esc(a.l3)}</span></span><span class="badge bad">S${a.severity} · P${a.priority}</span></div>`),'无告警');rows('watches',s.watches.map(w=>`<div class="row"><span><b>${esc(w.name)}</b><br><span class="muted">${esc(w.type)} · ${esc(w.target)}</span></span><span class="badge ${w.up?'good':'bad'}">${w.up?'UP':'DOWN'}</span></div>`))}catch(e){$('dot').classList.remove('ok');$('liveText').textContent='离线';$('identity').textContent=e.message}}
|
||||||
async function loadLogs(){try{const d=await get('/api/v1/logs?limit=200');rows('logRows',d.logs.reverse().map(x=>`<div class="log">${esc(x.message)}</div>`))}catch(e){$('logRows').innerHTML=`<div class="error">${esc(e.message)}</div>`}}
|
async function loadLogs(){try{const d=await get('/api/v1/logs?limit=200');rows('logRows',d.logs.reverse().map(x=>`<div class="log">${esc(x.message)}</div>`))}catch(e){$('logRows').innerHTML=`<div class="error">${esc(e.message)}</div>`}}
|
||||||
async function loadConfig(){try{const d=await get('/api/v1/config');original=d.config;$('configRows').innerHTML=Object.entries(original).map(([section,values])=>`<details class="config-section"><summary>[${esc(section)}]</summary><div class="fields">${Object.entries(values).map(([k,v])=>`<div class="field"><label>${esc(k)}<input data-section="${esc(section)}" data-key="${esc(k)}" value="${esc(v)}"></label></div>`).join('')}</div></details>`).join('')}catch(e){$('configRows').innerHTML=`<div class="error">${esc(e.message)}</div>`}}
|
async function loadConfig(){try{const d=await get('/api/v1/config');original=d.config;$('configRows').innerHTML=Object.entries(original).map(([section,values])=>`<details class="config-section" ${section==='led'?'open':''}><summary>[${esc(section)}]</summary><div class="fields">${Object.entries(values).map(([k,v])=>configField(section,k,v)).join('')}</div></details>`).join('')}catch(e){$('configRows').innerHTML=`<div class="error">${esc(e.message)}</div>`}}
|
||||||
$('refreshLogs').onclick=loadLogs;$('token').value=localStorage.getItem('pigwayToken')||'';
|
$('refreshLogs').onclick=loadLogs;$('token').value=localStorage.getItem('pigwayToken')||'';
|
||||||
$('saveConfig').onclick=async()=>{const token=$('token').value.trim();localStorage.setItem('pigwayToken',token);const updates={};document.querySelectorAll('#configRows input').forEach(i=>{if(i.value!==original[i.dataset.section][i.dataset.key]){(updates[i.dataset.section]??={})[i.dataset.key]=i.value}});if(!Object.keys(updates).length){$('saveState').textContent='没有变化';return}try{$('saveState').textContent='正在应用…';const d=await get('/api/v1/config',{method:'PUT',headers:{'Content-Type':'application/json','Authorization':`Bearer ${token}`},body:JSON.stringify({updates})});$('saveState').textContent=`已保存,备份 ${d.backup}`;setTimeout(loadConfig,1500)}catch(e){$('saveState').textContent=e.message;$('saveState').className='error'}};
|
$('saveConfig').onclick=async()=>{const token=$('token').value.trim();localStorage.setItem('pigwayToken',token);const updates={};document.querySelectorAll('#configRows [data-section]').forEach(i=>{if(i.value!==original[i.dataset.section][i.dataset.key]){(updates[i.dataset.section]??={})[i.dataset.key]=i.value}});if(!Object.keys(updates).length){$('saveState').textContent='没有变化';return}try{$('saveState').textContent='正在应用…';const d=await get('/api/v1/config',{method:'PUT',headers:{'Content-Type':'application/json','Authorization':`Bearer ${token}`},body:JSON.stringify({updates})});$('saveState').textContent=`已保存,备份 ${d.backup}`;setTimeout(loadConfig,1500)}catch(e){$('saveState').textContent=e.message;$('saveState').className='error'}};
|
||||||
loadStatus();setInterval(loadStatus,3000);
|
loadStatus();setInterval(loadStatus,3000);
|
||||||
</script></body></html>
|
</script></body></html>
|
||||||
|
|||||||
Reference in New Issue
Block a user