feat: add all-off led action

This commit is contained in:
2026-05-15 15:57:10 +08:00
parent ac75a5f9b7
commit 4ff64b31d9
3 changed files with 46 additions and 4 deletions
+2 -1
View File
@@ -18,7 +18,8 @@ blue:wan green:wlan mmc0:: red:power sim:en sim:en2 sim:sel sim:sel2
- 列出 `/sys/class/leds/` 下所有 LED - 列出 `/sys/class/leds/` 下所有 LED
- 查看 brightness / max_brightness / 当前 trigger - 查看 brightness / max_brightness / 当前 trigger
- 打开 / 关闭 LED - 打开 / 关闭单个 LED
- 页面顶部一键关闭全部 LED
- 调整亮度 - 调整亮度
- 切换 trigger,例如 `none``timer``netdev``mmc0` 等,具体取决于内核暴露的能力 - 切换 trigger,例如 `none``timer``netdev``mmc0` 等,具体取决于内核暴露的能力
- 设置 `timer` trigger 的 `delay_on` / `delay_off` - 设置 `timer` trigger 的 `delay_on` / `delay_off`
+32 -2
View File
@@ -69,6 +69,7 @@ async function identify(name){ try{ await api('/api/identify',{name}); toast('Bl
async function saveLed(name){ try{ await api('/api/save',{name}); toast('已保存 '+name+',重启后自动恢复'); setTimeout(load,300);}catch(e){toast(e.message,true);} } async function saveLed(name){ try{ await api('/api/save',{name}); toast('已保存 '+name+',重启后自动恢复'); setTimeout(load,300);}catch(e){toast(e.message,true);} }
async function deleteSaved(name){ try{ await api('/api/delete-saved',{name}); toast('已取消持久化 '+name); setTimeout(load,300);}catch(e){toast(e.message,true);} } async function deleteSaved(name){ try{ await api('/api/delete-saved',{name}); toast('已取消持久化 '+name); setTimeout(load,300);}catch(e){toast(e.message,true);} }
async function applySaved(){ try{ const r=await api('/api/apply-saved',{}); toast('已应用持久化配置:'+(r.applied||0)+''); setTimeout(load,300);}catch(e){toast(e.message,true);} } async function applySaved(){ try{ const r=await api('/api/apply-saved',{}); toast('已应用持久化配置:'+(r.applied||0)+''); setTimeout(load,300);}catch(e){toast(e.message,true);} }
async function turnOffAll(){ try{ const r=await api('/api/all-off',{}); const failed=(r.results||[]).filter(x=>!x.ok); toast(failed.length ? ('已关闭 '+(r.changed||0)+' 个,失败 '+failed.length+'') : ('已关闭全部 LED'+(r.changed||0)+''), failed.length>0); setTimeout(load,300);}catch(e){toast(e.message,true);} }
function colorOf(n,b){ const s=n.toLowerCase(); if(Number(b)<=0) return '#475569'; if(s.includes('red')) return '#ef4444'; if(s.includes('green')) return '#22c55e'; if(s.includes('blue')) return '#3b82f6'; if(s.includes('wlan')) return '#22c55e'; if(s.includes('wan')) return '#3b82f6'; if(s.includes('power')) return '#ef4444'; if(s.includes('sim')) return '#f59e0b'; return '#5eead4'; } function colorOf(n,b){ const s=n.toLowerCase(); if(Number(b)<=0) return '#475569'; if(s.includes('red')) return '#ef4444'; if(s.includes('green')) return '#22c55e'; if(s.includes('blue')) return '#3b82f6'; if(s.includes('wlan')) return '#22c55e'; if(s.includes('wan')) return '#3b82f6'; if(s.includes('power')) return '#ef4444'; if(s.includes('sim')) return '#f59e0b'; return '#5eead4'; }
function esc(s){ return String(s).replace(/[&<>'"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c])); } function esc(s){ return String(s).replace(/[&<>'"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c])); }
function attr(s){ return esc(s); } function attr(s){ return esc(s); }
@@ -253,6 +254,32 @@ def apply_saved_config() -> List[Dict[str, object]]:
results.append({"name": name, "ok": False, "errors": [str(e)]}) results.append({"name": name, "ok": False, "errors": [str(e)]})
return results return results
def turn_off_all_leds() -> List[Dict[str, object]]:
results = []
for led in list_leds():
name = str(led["name"])
result: Dict[str, object] = {"name": name, "ok": True, "errors": []}
try:
p = led_path(name)
if (p / "trigger").exists():
try:
set_trigger(p, "none")
except Exception as e:
result["ok"] = False
result["errors"].append(f"trigger: {e}")
write_text(p / "brightness", "0")
try:
result["brightness"] = int(read_text(p / "brightness") or 0)
except ValueError:
result["brightness"] = read_text(p / "brightness")
except Exception as e:
result["ok"] = False
result["errors"].append(str(e))
results.append(result)
return results
def list_leds() -> List[Dict[str, object]]: def list_leds() -> List[Dict[str, object]]:
leds = [] leds = []
saved_config = load_config() saved_config = load_config()
@@ -317,7 +344,7 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
parsed = urllib.parse.urlparse(self.path) parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/": if parsed.path == "/":
body = f"""<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>LED Manager</title><style>{CSS}</style></head><body><div class='container'><div class='header'><div><h1>LED Manager Panel</h1><div class='sub'>管理 <code>{html.escape(str(LED_ROOT))}</code> 下的 LED;配置保存到 <code>{html.escape(str(CONFIG_PATH))}</code>;当前 <span id='count'>-</span> 个。页面每 5 秒自动刷新。</div></div><div class='row' style='margin-top:0'><button onclick='applySaved()'>应用已保存配置</button><button onclick='load()'>刷新</button></div></div><div id='root' class='grid'></div></div><div id='toast' class='toast'></div><script>{JS}</script></body></html>""" body = f"""<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>LED Manager</title><style>{CSS}</style></head><body><div class='container'><div class='header'><div><h1>LED Manager Panel</h1><div class='sub'>管理 <code>{html.escape(str(LED_ROOT))}</code> 下的 LED;配置保存到 <code>{html.escape(str(CONFIG_PATH))}</code>;当前 <span id='count'>-</span> 个。页面每 5 秒自动刷新。</div></div><div class='row' style='margin-top:0'><button class='off' onclick='turnOffAll()'>关闭全部灯</button><button onclick='applySaved()'>应用已保存配置</button><button onclick='load()'>刷新</button></div></div><div id='root' class='grid'></div></div><div id='toast' class='toast'></div><script>{JS}</script></body></html>"""
data = body.encode("utf-8") data = body.encode("utf-8")
self.send_response(200) self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Type", "text/html; charset=utf-8")
@@ -335,7 +362,7 @@ class Handler(BaseHTTPRequestHandler):
data = self.read_body() data = self.read_body()
name = None name = None
p = None p = None
if parsed.path not in ("/api/apply-saved",): if parsed.path not in ("/api/apply-saved", "/api/all-off"):
name = safe_name(str(data.get("name", ""))) name = safe_name(str(data.get("name", "")))
p = led_path(name) p = led_path(name)
if parsed.path == "/api/brightness": if parsed.path == "/api/brightness":
@@ -380,6 +407,9 @@ class Handler(BaseHTTPRequestHandler):
elif parsed.path == "/api/apply-saved": elif parsed.path == "/api/apply-saved":
results = apply_saved_config() results = apply_saved_config()
self.send_json({"ok": True, "applied": sum(1 for r in results if r.get("ok")), "results": results}) self.send_json({"ok": True, "applied": sum(1 for r in results if r.get("ok")), "results": results})
elif parsed.path == "/api/all-off":
results = turn_off_all_leds()
self.send_json({"ok": all(r.get("ok") for r in results), "changed": sum(1 for r in results if r.get("brightness") == 0), "results": results})
elif parsed.path == "/api/identify": elif parsed.path == "/api/identify":
old_trigger = read_text(p / "trigger") old_trigger = read_text(p / "trigger")
old_brightness = read_text(p / "brightness") old_brightness = read_text(p / "brightness")
+12 -1
View File
@@ -29,6 +29,11 @@ def main():
(d / 'trigger').write_text('[none] timer') (d / 'trigger').write_text('[none] timer')
(d / 'delay_on').write_text('500') (d / 'delay_on').write_text('500')
(d / 'delay_off').write_text('500') (d / 'delay_off').write_text('500')
d2 = root / 'red:power'
d2.mkdir()
(d2 / 'brightness').write_text('1')
(d2 / 'max_brightness').write_text('1')
(d2 / 'trigger').write_text('[none] timer')
config_path = root / 'config.json' config_path = root / 'config.json'
led_panel.LED_ROOT = root led_panel.LED_ROOT = root
led_panel.CONFIG_PATH = config_path led_panel.CONFIG_PATH = config_path
@@ -58,7 +63,13 @@ def main():
assert applied['ok'] and applied['applied'] == 1 assert applied['ok'] and applied['applied'] == 1
assert (d / 'brightness').read_text() == '1' assert (d / 'brightness').read_text() == '1'
assert (d / 'delay_on').read_text() == '123' assert (d / 'delay_on').read_text() == '123'
print('OK: API write + persistence regression passed') all_off = req(port, '/api/all-off', {})
assert all_off['ok'] and all_off['changed'] == 2
assert (d / 'brightness').read_text() == '0'
assert (d2 / 'brightness').read_text() == '0'
assert '[none]' in (d / 'trigger').read_text() or (d / 'trigger').read_text() == 'none'
assert '[none]' in (d2 / 'trigger').read_text() or (d2 / 'trigger').read_text() == 'none'
print('OK: API write + persistence + all-off regression passed')
finally: finally:
httpd.shutdown() httpd.shutdown()