feat: persist led config on debian startup
This commit is contained in:
@@ -25,6 +25,29 @@ blue:wan green:wlan mmc0:: red:power sim:en sim:en2 sim:sel sim:sel2
|
|||||||
- “识别 3 秒”闪烁指定 LED
|
- “识别 3 秒”闪烁指定 LED
|
||||||
- 无数据库、无 Node、无 pip 依赖
|
- 无数据库、无 Node、无 pip 依赖
|
||||||
|
|
||||||
|
## 持久化配置 / 开机恢复
|
||||||
|
|
||||||
|
这是 Debian 版本的一键脚本:安装后会创建 systemd 服务并开机启动:
|
||||||
|
|
||||||
|
```text
|
||||||
|
led-manager.service
|
||||||
|
```
|
||||||
|
|
||||||
|
面板里每个 LED 卡片都有:
|
||||||
|
|
||||||
|
```text
|
||||||
|
保存持久化
|
||||||
|
取消持久化
|
||||||
|
```
|
||||||
|
|
||||||
|
点击“保存持久化”后,当前 LED 的 `trigger`、`brightness`、`delay_on`、`delay_off` 会保存到:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/etc/led-manager/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
系统重启后,`led-manager.service` 会自动启动,并在启动时读取该配置恢复 LED 状态。
|
||||||
|
|
||||||
## 更新已有安装
|
## 更新已有安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+35
-34
@@ -3,6 +3,8 @@ set -eu
|
|||||||
|
|
||||||
APP_NAME="led-manager"
|
APP_NAME="led-manager"
|
||||||
APP_DIR="/opt/led-manager"
|
APP_DIR="/opt/led-manager"
|
||||||
|
CONFIG_DIR="/etc/led-manager"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||||
PORT="${PORT:-8088}"
|
PORT="${PORT:-8088}"
|
||||||
HOST="${HOST:-0.0.0.0}"
|
HOST="${HOST:-0.0.0.0}"
|
||||||
RAW_BASE="${RAW_BASE:-https://gitea.chickliu.fun/Hermes/led-manager-panel/raw/branch/main}"
|
RAW_BASE="${RAW_BASE:-https://gitea.chickliu.fun/Hermes/led-manager-panel/raw/branch/main}"
|
||||||
@@ -32,60 +34,59 @@ download() {
|
|||||||
install_python_if_possible() {
|
install_python_if_possible() {
|
||||||
if has_cmd python3; then return 0; fi
|
if has_cmd python3; then return 0; fi
|
||||||
echo "未发现 python3,尝试安装..."
|
echo "未发现 python3,尝试安装..."
|
||||||
if has_cmd opkg; then
|
if has_cmd apt-get; then
|
||||||
opkg update
|
|
||||||
opkg install python3
|
|
||||||
elif has_cmd apt-get; then
|
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y python3
|
apt-get install -y python3 ca-certificates
|
||||||
else
|
else
|
||||||
echo "未发现 opkg/apt-get,请先手动安装 python3" >&2
|
echo "Debian 系统请先安装 python3:apt update && apt install -y python3" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_openwrt_service() {
|
|
||||||
echo "检测到 OpenWrt/procd,安装 /etc/init.d/$APP_NAME"
|
|
||||||
download "$RAW_BASE/led-manager.openwrt.init" "/etc/init.d/$APP_NAME"
|
|
||||||
chmod +x "/etc/init.d/$APP_NAME"
|
|
||||||
# Patch host/port in init script if env overrides were provided.
|
|
||||||
sed -i "s/^HOST=.*/HOST=$HOST/" "/etc/init.d/$APP_NAME" || true
|
|
||||||
sed -i "s/^PORT=.*/PORT=$PORT/" "/etc/init.d/$APP_NAME" || true
|
|
||||||
"/etc/init.d/$APP_NAME" enable
|
|
||||||
"/etc/init.d/$APP_NAME" restart || "/etc/init.d/$APP_NAME" start
|
|
||||||
"/etc/init.d/$APP_NAME" status || true
|
|
||||||
}
|
|
||||||
|
|
||||||
install_systemd_service() {
|
install_systemd_service() {
|
||||||
echo "检测到 systemd,安装 led-manager.service"
|
if ! has_cmd systemctl; then
|
||||||
|
echo "未发现 systemctl,已安装文件但不能配置开机启动。手动启动:"
|
||||||
|
echo "python3 $APP_DIR/led_panel.py --host $HOST --port $PORT --config $CONFIG_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "安装 systemd 服务:$APP_NAME"
|
||||||
download "$RAW_BASE/led-manager.service" "/etc/systemd/system/$APP_NAME.service"
|
download "$RAW_BASE/led-manager.service" "/etc/systemd/system/$APP_NAME.service"
|
||||||
# Patch host/port in service if env overrides were provided.
|
# Patch host/port/config in service if env overrides were provided.
|
||||||
sed -i "s/--host [^ ]* --port [0-9]*/--host $HOST --port $PORT/" "/etc/systemd/system/$APP_NAME.service" || true
|
sed -i "s#--host [^ ]* --port [0-9]* --config [^ ]*#--host $HOST --port $PORT --config $CONFIG_FILE#" "/etc/systemd/system/$APP_NAME.service" || true
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable --now "$APP_NAME"
|
systemctl enable "$APP_NAME"
|
||||||
systemctl restart "$APP_NAME"
|
systemctl restart "$APP_NAME"
|
||||||
systemctl status "$APP_NAME" --no-pager || true
|
systemctl status "$APP_NAME" --no-pager || true
|
||||||
}
|
}
|
||||||
|
|
||||||
need_root
|
need_root
|
||||||
install_python_if_possible
|
install_python_if_possible
|
||||||
mkdir -p "$APP_DIR"
|
mkdir -p "$APP_DIR" "$CONFIG_DIR"
|
||||||
|
|
||||||
|
# Preserve existing persistent config across reinstall/update.
|
||||||
|
if [ ! -f "$CONFIG_FILE" ]; then
|
||||||
|
cat > "$CONFIG_FILE" <<'JSON'
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"leds": {}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
fi
|
||||||
|
chmod 755 "$CONFIG_DIR" || true
|
||||||
|
chmod 644 "$CONFIG_FILE" || true
|
||||||
|
|
||||||
download "$RAW_BASE/led_panel.py" "$APP_DIR/led_panel.py"
|
download "$RAW_BASE/led_panel.py" "$APP_DIR/led_panel.py"
|
||||||
chmod +x "$APP_DIR/led_panel.py"
|
chmod +x "$APP_DIR/led_panel.py"
|
||||||
|
|
||||||
if [ -x /sbin/procd ] || [ -d /etc/rc.d ] && has_cmd procd; then
|
# Debian-focused installer: always prefer systemd. OpenWrt is intentionally not handled here.
|
||||||
install_openwrt_service
|
install_systemd_service
|
||||||
elif has_cmd systemctl && [ -d /run/systemd/system ]; then
|
|
||||||
install_systemd_service
|
|
||||||
elif [ -d /etc/init.d ] && [ -f /etc/openwrt_release ]; then
|
|
||||||
install_openwrt_service
|
|
||||||
else
|
|
||||||
echo "未检测到 systemd/procd,已安装到 $APP_DIR。手动启动:"
|
|
||||||
echo "python3 $APP_DIR/led_panel.py --host $HOST --port $PORT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ipaddr="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
|
ipaddr="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
|
||||||
echo
|
echo
|
||||||
echo "安装完成。"
|
echo "安装/更新完成。"
|
||||||
|
echo "服务:systemctl status $APP_NAME"
|
||||||
|
echo "配置:$CONFIG_FILE"
|
||||||
echo "访问地址: http://${ipaddr:-设备IP}:$PORT/"
|
echo "访问地址: http://${ipaddr:-设备IP}:$PORT/"
|
||||||
|
echo "面板里点『保存持久化』后,重启系统会由 systemd 启动本项目并自动读取配置恢复 LED。"
|
||||||
echo "如需修改端口:PORT=8090 sh install.sh"
|
echo "如需修改端口:PORT=8090 sh install.sh"
|
||||||
|
|||||||
+5
-2
@@ -1,12 +1,15 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Linux LED Manager Web Panel
|
Description=Linux LED Manager Web Panel
|
||||||
After=network.target
|
Documentation=https://gitea.chickliu.fun/Hermes/led-manager-panel
|
||||||
|
After=local-fs.target network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
User=root
|
||||||
WorkingDirectory=/opt/led-manager
|
WorkingDirectory=/opt/led-manager
|
||||||
ExecStart=/usr/bin/python3 /opt/led-manager/led_panel.py --host 0.0.0.0 --port 8088
|
ExecStartPre=/bin/mkdir -p /etc/led-manager
|
||||||
|
ExecStart=/usr/bin/python3 /opt/led-manager/led_panel.py --host 0.0.0.0 --port 8088 --config /etc/led-manager/config.json
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
|
||||||
|
|||||||
+125
-7
@@ -20,6 +20,7 @@ from pathlib import Path
|
|||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
LED_ROOT = Path(os.environ.get("LED_ROOT", "/sys/class/leds"))
|
LED_ROOT = Path(os.environ.get("LED_ROOT", "/sys/class/leds"))
|
||||||
|
CONFIG_PATH = Path(os.environ.get("LED_CONFIG", "/etc/led-manager/config.json"))
|
||||||
NAME_RE = re.compile(r"^[A-Za-z0-9_.:+@=-]+$")
|
NAME_RE = re.compile(r"^[A-Za-z0-9_.:+@=-]+$")
|
||||||
|
|
||||||
CSS = r"""
|
CSS = r"""
|
||||||
@@ -65,6 +66,9 @@ async function setBrightness(name, value){ try{ const r=await api('/api/brightne
|
|||||||
async function setTrigger(name, trigger){ try{ const r=await api('/api/trigger',{name,trigger}); toast(name+' trigger='+(r.active_trigger || trigger)); setTimeout(load,300);}catch(e){toast(e.message,true);} }
|
async function setTrigger(name, trigger){ try{ const r=await api('/api/trigger',{name,trigger}); toast(name+' trigger='+(r.active_trigger || trigger)); setTimeout(load,300);}catch(e){toast(e.message,true);} }
|
||||||
async function setDelay(name, field, value){ try{ await api('/api/delay',{name,field,value:Number(value)}); toast(name+' '+field+'='+value); setTimeout(load,300);}catch(e){toast(e.message,true);} }
|
async function setDelay(name, field, value){ try{ await api('/api/delay',{name,field,value:Number(value)}); toast(name+' '+field+'='+value); setTimeout(load,300);}catch(e){toast(e.message,true);} }
|
||||||
async function identify(name){ try{ await api('/api/identify',{name}); toast('Blink '+name); setTimeout(load,3200);}catch(e){toast(e.message,true);} }
|
async function identify(name){ try{ await api('/api/identify',{name}); toast('Blink '+name); setTimeout(load,3200);}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 applySaved(){ try{ const r=await api('/api/apply-saved',{}); toast('已应用持久化配置:'+(r.applied||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=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c])); }
|
function esc(s){ return String(s).replace(/[&<>'"]/g, c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c])); }
|
||||||
function attr(s){ return esc(s); }
|
function attr(s){ return esc(s); }
|
||||||
@@ -75,10 +79,10 @@ function card(led){
|
|||||||
const triggerSelect = triggers.length ? `<select data-led="${n}" onchange="setTrigger(this.dataset.led, this.value)">${triggers.map(t=>`<option value="${attr(t.name)}" ${t.active?'selected':''}>${esc(t.name)}</option>`).join('')}</select>` : '<span class="small">无 trigger 文件</span>';
|
const triggerSelect = triggers.length ? `<select data-led="${n}" onchange="setTrigger(this.dataset.led, this.value)">${triggers.map(t=>`<option value="${attr(t.name)}" ${t.active?'selected':''}>${esc(t.name)}</option>`).join('')}</select>` : '<span class="small">无 trigger 文件</span>';
|
||||||
const delay = (led.delay_on !== null || led.delay_off !== null) ? `<div class="row"><span class="small">timer:</span><input data-led="${n}" type="number" min="0" value="${led.delay_on ?? 500}" onchange="setDelay(this.dataset.led,'delay_on',this.value)" title="delay_on ms"><input data-led="${n}" type="number" min="0" value="${led.delay_off ?? 500}" onchange="setDelay(this.dataset.led,'delay_off',this.value)" title="delay_off ms"></div>` : '';
|
const delay = (led.delay_on !== null || led.delay_off !== null) ? `<div class="row"><span class="small">timer:</span><input data-led="${n}" type="number" min="0" value="${led.delay_on ?? 500}" onchange="setDelay(this.dataset.led,'delay_on',this.value)" title="delay_on ms"><input data-led="${n}" type="number" min="0" value="${led.delay_off ?? 500}" onchange="setDelay(this.dataset.led,'delay_off',this.value)" title="delay_off ms"></div>` : '';
|
||||||
return `<div class="card">
|
return `<div class="card">
|
||||||
<div class="name"><span class="dot" style="background:${colorOf(led.name,led.brightness)};color:${colorOf(led.name,led.brightness)}"></span>${esc(led.name)} ${led.writable?'<span class="badge">可写</span>':'<span class="small">只读/权限不足</span>'}</div>
|
<div class="name"><span class="dot" style="background:${colorOf(led.name,led.brightness)};color:${colorOf(led.name,led.brightness)}"></span>${esc(led.name)} ${led.writable?'<span class="badge">可写</span>':'<span class="small">只读/权限不足</span>'} ${led.saved?'<span class="badge">已持久化</span>':''}</div>
|
||||||
<div class="meta">brightness: <b>${led.brightness}</b> / ${led.max_brightness} (${pct}%)<br>trigger: <b>${esc(led.active_trigger || '-')}</b><br>path: ${esc(led.path)}</div>
|
<div class="meta">brightness: <b>${led.brightness}</b> / ${led.max_brightness} (${pct}%)<br>trigger: <b>${esc(led.active_trigger || '-')}</b><br>path: ${esc(led.path)}</div>
|
||||||
<div class="range-line"><input data-led="${n}" type="range" min="0" max="${led.max_brightness || 1}" value="${led.brightness}" oninput="this.nextElementSibling.value=this.value" onchange="setBrightness(this.dataset.led, this.value)"><input data-led="${n}" type="number" min="0" max="${led.max_brightness || 1}" value="${led.brightness}" onchange="setBrightness(this.dataset.led, this.value)"></div>
|
<div class="range-line"><input data-led="${n}" type="range" min="0" max="${led.max_brightness || 1}" value="${led.brightness}" oninput="this.nextElementSibling.value=this.value" onchange="setBrightness(this.dataset.led, this.value)"><input data-led="${n}" type="number" min="0" max="${led.max_brightness || 1}" value="${led.brightness}" onchange="setBrightness(this.dataset.led, this.value)"></div>
|
||||||
<div class="row"><button class="primary" data-led="${n}" data-max="${led.max_brightness || 1}" onclick="setBrightness(this.dataset.led, this.dataset.max)">打开</button><button class="off" data-led="${n}" onclick="setBrightness(this.dataset.led, 0)">关闭</button><button class="warn" data-led="${n}" onclick="setTrigger(this.dataset.led, 'timer')">闪烁</button><button data-led="${n}" onclick="identify(this.dataset.led)">识别3秒</button></div>
|
<div class="row"><button class="primary" data-led="${n}" data-max="${led.max_brightness || 1}" onclick="setBrightness(this.dataset.led, this.dataset.max)">打开</button><button class="off" data-led="${n}" onclick="setBrightness(this.dataset.led, 0)">关闭</button><button class="warn" data-led="${n}" onclick="setTrigger(this.dataset.led, 'timer')">闪烁</button><button data-led="${n}" onclick="identify(this.dataset.led)">识别3秒</button><button data-led="${n}" onclick="saveLed(this.dataset.led)">保存持久化</button><button data-led="${n}" onclick="deleteSaved(this.dataset.led)">取消持久化</button></div>
|
||||||
<div class="row"><span class="small">trigger</span>${triggerSelect}</div>${delay}
|
<div class="row"><span class="small">trigger</span>${triggerSelect}</div>${delay}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -161,8 +165,97 @@ def parse_triggers(raw: Optional[str]) -> Tuple[List[Dict[str, object]], Optiona
|
|||||||
return out, active
|
return out, active
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def load_config() -> Dict[str, Dict[str, object]]:
|
||||||
|
try:
|
||||||
|
if not CONFIG_PATH.exists():
|
||||||
|
return {}
|
||||||
|
data = json.loads(CONFIG_PATH.read_text())
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return {}
|
||||||
|
leds = data.get("leds", data)
|
||||||
|
if not isinstance(leds, dict):
|
||||||
|
return {}
|
||||||
|
return {safe_name(str(k)): v for k, v in leds.items() if isinstance(v, dict)}
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(config: Dict[str, Dict[str, object]]) -> None:
|
||||||
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = CONFIG_PATH.with_suffix(CONFIG_PATH.suffix + ".tmp")
|
||||||
|
payload = {"version": 1, "leds": config}
|
||||||
|
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
||||||
|
os.replace(tmp, CONFIG_PATH)
|
||||||
|
try:
|
||||||
|
os.chmod(CONFIG_PATH, 0o644)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def current_led_config(name: str) -> Dict[str, object]:
|
||||||
|
p = led_path(name)
|
||||||
|
cfg: Dict[str, object] = {}
|
||||||
|
active = active_trigger_name(read_text(p / "trigger"))
|
||||||
|
if active:
|
||||||
|
cfg["trigger"] = active
|
||||||
|
b = read_text(p / "brightness")
|
||||||
|
if b is not None:
|
||||||
|
try: cfg["brightness"] = int(b)
|
||||||
|
except ValueError: pass
|
||||||
|
for field in ("delay_on", "delay_off"):
|
||||||
|
v = read_text(p / field)
|
||||||
|
if v is not None:
|
||||||
|
try: cfg[field] = int(v)
|
||||||
|
except ValueError: pass
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def apply_led_config(name: str, cfg: Dict[str, object]) -> Dict[str, object]:
|
||||||
|
p = led_path(name)
|
||||||
|
result: Dict[str, object] = {"name": name, "ok": True, "errors": []}
|
||||||
|
# Apply trigger first so timer/netdev files appear before setting delay fields.
|
||||||
|
if "trigger" in cfg:
|
||||||
|
try:
|
||||||
|
set_trigger(p, str(cfg["trigger"]))
|
||||||
|
except Exception as e:
|
||||||
|
result["ok"] = False
|
||||||
|
result["errors"].append(f"trigger: {e}")
|
||||||
|
for field in ("delay_on", "delay_off"):
|
||||||
|
if field in cfg and (p / field).exists():
|
||||||
|
try:
|
||||||
|
write_text(p / field, str(int(cfg[field])))
|
||||||
|
except Exception as e:
|
||||||
|
result["ok"] = False
|
||||||
|
result["errors"].append(f"{field}: {e}")
|
||||||
|
if "brightness" in cfg:
|
||||||
|
try:
|
||||||
|
max_brightness = int(read_text(p / "max_brightness") or 1)
|
||||||
|
value = max(0, min(max_brightness, int(cfg["brightness"])))
|
||||||
|
# If trigger is not explicitly set, try none for manual brightness.
|
||||||
|
if "trigger" not in cfg and (p / "trigger").exists():
|
||||||
|
try: set_trigger(p, "none")
|
||||||
|
except Exception: pass
|
||||||
|
write_text(p / "brightness", str(value))
|
||||||
|
except Exception as e:
|
||||||
|
result["ok"] = False
|
||||||
|
result["errors"].append(f"brightness: {e}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def apply_saved_config() -> List[Dict[str, object]]:
|
||||||
|
config = load_config()
|
||||||
|
results = []
|
||||||
|
for name, cfg in config.items():
|
||||||
|
try:
|
||||||
|
results.append(apply_led_config(name, cfg))
|
||||||
|
except Exception as e:
|
||||||
|
results.append({"name": name, "ok": False, "errors": [str(e)]})
|
||||||
|
return results
|
||||||
|
|
||||||
def list_leds() -> List[Dict[str, object]]:
|
def list_leds() -> List[Dict[str, object]]:
|
||||||
leds = []
|
leds = []
|
||||||
|
saved_config = load_config()
|
||||||
if not LED_ROOT.exists():
|
if not LED_ROOT.exists():
|
||||||
return leds
|
return leds
|
||||||
for p in sorted(LED_ROOT.iterdir(), key=lambda x: x.name):
|
for p in sorted(LED_ROOT.iterdir(), key=lambda x: x.name):
|
||||||
@@ -194,6 +287,7 @@ def list_leds() -> List[Dict[str, object]]:
|
|||||||
"delay_on": int(delay_on) if delay_on and delay_on.isdigit() else None,
|
"delay_on": int(delay_on) if delay_on and delay_on.isdigit() else None,
|
||||||
"delay_off": int(delay_off) if delay_off and delay_off.isdigit() else None,
|
"delay_off": int(delay_off) if delay_off and delay_off.isdigit() else None,
|
||||||
"writable": writable,
|
"writable": writable,
|
||||||
|
"saved": name in saved_config,
|
||||||
})
|
})
|
||||||
return leds
|
return leds
|
||||||
|
|
||||||
@@ -223,7 +317,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;当前 <span id='count'>-</span> 个。页面每 5 秒自动刷新。</div></div><button onclick='load()'>刷新</button></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 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")
|
||||||
@@ -239,8 +333,11 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
parsed = urllib.parse.urlparse(self.path)
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
try:
|
try:
|
||||||
data = self.read_body()
|
data = self.read_body()
|
||||||
name = safe_name(str(data.get("name", "")))
|
name = None
|
||||||
p = led_path(name)
|
p = None
|
||||||
|
if parsed.path not in ("/api/apply-saved",):
|
||||||
|
name = safe_name(str(data.get("name", "")))
|
||||||
|
p = led_path(name)
|
||||||
if parsed.path == "/api/brightness":
|
if parsed.path == "/api/brightness":
|
||||||
max_brightness = int(read_text(p / "max_brightness") or 1)
|
max_brightness = int(read_text(p / "max_brightness") or 1)
|
||||||
value = int(data.get("value", 0))
|
value = int(data.get("value", 0))
|
||||||
@@ -269,6 +366,20 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
value = max(0, int(data.get("value", 0)))
|
value = max(0, int(data.get("value", 0)))
|
||||||
write_text(p / field, str(value))
|
write_text(p / field, str(value))
|
||||||
self.send_json({"ok": True})
|
self.send_json({"ok": True})
|
||||||
|
elif parsed.path == "/api/save":
|
||||||
|
config = load_config()
|
||||||
|
config[name] = current_led_config(name)
|
||||||
|
save_config(config)
|
||||||
|
self.send_json({"ok": True, "config": config[name], "path": str(CONFIG_PATH)})
|
||||||
|
elif parsed.path == "/api/delete-saved":
|
||||||
|
config = load_config()
|
||||||
|
existed = name in config
|
||||||
|
config.pop(name, None)
|
||||||
|
save_config(config)
|
||||||
|
self.send_json({"ok": True, "deleted": existed, "path": str(CONFIG_PATH)})
|
||||||
|
elif parsed.path == "/api/apply-saved":
|
||||||
|
results = apply_saved_config()
|
||||||
|
self.send_json({"ok": True, "applied": sum(1 for r in results if r.get("ok")), "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")
|
||||||
@@ -302,15 +413,22 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global LED_ROOT
|
global LED_ROOT, CONFIG_PATH
|
||||||
ap = argparse.ArgumentParser(description="Tiny web panel for /sys/class/leds")
|
ap = argparse.ArgumentParser(description="Tiny web panel for /sys/class/leds")
|
||||||
ap.add_argument("--host", default="0.0.0.0", help="listen host, default 0.0.0.0")
|
ap.add_argument("--host", default="0.0.0.0", help="listen host, default 0.0.0.0")
|
||||||
ap.add_argument("--port", type=int, default=8088, help="listen port, default 8088")
|
ap.add_argument("--port", type=int, default=8088, help="listen port, default 8088")
|
||||||
ap.add_argument("--led-root", default=str(LED_ROOT), help="LED sysfs root, default /sys/class/leds")
|
ap.add_argument("--led-root", default=str(LED_ROOT), help="LED sysfs root, default /sys/class/leds")
|
||||||
|
ap.add_argument("--config", default=str(CONFIG_PATH), help="persistent config path, default /etc/led-manager/config.json")
|
||||||
|
ap.add_argument("--no-apply", action="store_true", help="do not apply saved config on startup")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
LED_ROOT = Path(args.led_root)
|
LED_ROOT = Path(args.led_root)
|
||||||
|
CONFIG_PATH = Path(args.config)
|
||||||
|
if not args.no_apply:
|
||||||
|
results = apply_saved_config()
|
||||||
|
if results:
|
||||||
|
print(f"Applied saved LED config: {results}", flush=True)
|
||||||
httpd = ThreadingHTTPServer((args.host, args.port), Handler)
|
httpd = ThreadingHTTPServer((args.host, args.port), Handler)
|
||||||
print(f"LED Manager Panel listening on http://{args.host}:{args.port} ; LED_ROOT={LED_ROOT}", flush=True)
|
print(f"LED Manager Panel listening on http://{args.host}:{args.port} ; LED_ROOT={LED_ROOT}; CONFIG_PATH={CONFIG_PATH}", flush=True)
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -29,7 +29,9 @@ 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')
|
||||||
|
config_path = root / 'config.json'
|
||||||
led_panel.LED_ROOT = root
|
led_panel.LED_ROOT = root
|
||||||
|
led_panel.CONFIG_PATH = config_path
|
||||||
httpd = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
httpd = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
||||||
port = httpd.server_address[1]
|
port = httpd.server_address[1]
|
||||||
t = threading.Thread(target=httpd.serve_forever, daemon=True)
|
t = threading.Thread(target=httpd.serve_forever, daemon=True)
|
||||||
@@ -44,7 +46,19 @@ def main():
|
|||||||
assert (d / 'trigger').read_text() == 'timer'
|
assert (d / 'trigger').read_text() == 'timer'
|
||||||
delay = req(port, '/api/delay', {'name':'blue:wan','field':'delay_on','value':123})
|
delay = req(port, '/api/delay', {'name':'blue:wan','field':'delay_on','value':123})
|
||||||
assert delay['ok'] and (d / 'delay_on').read_text() == '123'
|
assert delay['ok'] and (d / 'delay_on').read_text() == '123'
|
||||||
print('OK: API write regression passed')
|
saved = req(port, '/api/save', {'name':'blue:wan'})
|
||||||
|
assert saved['ok'] and config_path.exists()
|
||||||
|
cfg = json.loads(config_path.read_text())
|
||||||
|
assert cfg['leds']['blue:wan']['delay_on'] == 123
|
||||||
|
# Simulate reboot/default reset, then apply persisted config.
|
||||||
|
(d / 'brightness').write_text('0')
|
||||||
|
(d / 'trigger').write_text('[none] timer')
|
||||||
|
(d / 'delay_on').write_text('500')
|
||||||
|
applied = req(port, '/api/apply-saved', {})
|
||||||
|
assert applied['ok'] and applied['applied'] == 1
|
||||||
|
assert (d / 'brightness').read_text() == '1'
|
||||||
|
assert (d / 'delay_on').read_text() == '123'
|
||||||
|
print('OK: API write + persistence regression passed')
|
||||||
finally:
|
finally:
|
||||||
httpd.shutdown()
|
httpd.shutdown()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user