feat: focus panel on rgb leds and speed boot
This commit is contained in:
@@ -16,7 +16,7 @@ blue:wan green:wlan mmc0:: red:power sim:en sim:en2 sim:sel sim:sel2
|
|||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
- 列出 `/sys/class/leds/` 下所有 LED
|
- 默认只列出实际可控的红 / 绿 / 蓝 LED(`red:*`、`green:*`、`blue:*`),隐藏 SIM/mmc 等不可控条目
|
||||||
- 查看 brightness / max_brightness / 当前 trigger
|
- 查看 brightness / max_brightness / 当前 trigger
|
||||||
- 打开 / 关闭单个 LED
|
- 打开 / 关闭单个 LED
|
||||||
- 页面顶部一键关闭全部 LED
|
- 页面顶部一键关闭全部 LED
|
||||||
@@ -49,6 +49,28 @@ led-manager.service
|
|||||||
|
|
||||||
系统重启后,`led-manager.service` 会自动启动,并在启动时读取该配置恢复 LED 状态。
|
系统重启后,`led-manager.service` 会自动启动,并在启动时读取该配置恢复 LED 状态。
|
||||||
|
|
||||||
|
为加快开机启动,systemd 服务不再等待 `network-online.target`,只依赖本地文件系统就启动;内网地址可用后浏览器再访问即可。
|
||||||
|
|
||||||
|
## LED 过滤
|
||||||
|
|
||||||
|
默认只显示/控制红绿蓝三类实际可控灯:
|
||||||
|
|
||||||
|
```text
|
||||||
|
red,green,blue
|
||||||
|
```
|
||||||
|
|
||||||
|
如果以后要临时显示全部 `/sys/class/leds` 条目,可以这样重装/更新:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
COLORS= sh -c "$(curl -fsSL https://gitea.chickliu.fun/Hermes/led-manager-panel/raw/branch/main/install.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以指定颜色前缀,例如只显示红蓝:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
COLORS=red,blue sh -c "$(curl -fsSL https://gitea.chickliu.fun/Hermes/led-manager-panel/raw/branch/main/install.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
## 更新已有安装
|
## 更新已有安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+9
-3
@@ -7,6 +7,7 @@ CONFIG_DIR="/etc/led-manager"
|
|||||||
CONFIG_FILE="$CONFIG_DIR/config.json"
|
CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||||
PORT="${PORT:-8088}"
|
PORT="${PORT:-8088}"
|
||||||
HOST="${HOST:-0.0.0.0}"
|
HOST="${HOST:-0.0.0.0}"
|
||||||
|
COLORS="${COLORS:-red,green,blue}"
|
||||||
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}"
|
||||||
|
|
||||||
need_root() {
|
need_root() {
|
||||||
@@ -52,12 +53,15 @@ install_systemd_service() {
|
|||||||
|
|
||||||
echo "安装 systemd 服务:$APP_NAME"
|
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/config in service if env overrides were provided.
|
# Patch host/port/config/colors in service if env overrides were provided.
|
||||||
sed -i "s#--host [^ ]* --port [0-9]* --config [^ ]*#--host $HOST --port $PORT --config $CONFIG_FILE#" "/etc/systemd/system/$APP_NAME.service" || true
|
sed -i "s#--host [^ ]* --port [0-9]* --config [^ ]*#--host $HOST --port $PORT --config $CONFIG_FILE --colors $COLORS#" "/etc/systemd/system/$APP_NAME.service" || true
|
||||||
|
if ! grep -q -- '--colors' "/etc/systemd/system/$APP_NAME.service"; then
|
||||||
|
sed -i "s#\(ExecStart=.*\)#\1 --colors $COLORS#" "/etc/systemd/system/$APP_NAME.service" || true
|
||||||
|
fi
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable "$APP_NAME"
|
systemctl enable "$APP_NAME"
|
||||||
systemctl restart "$APP_NAME"
|
systemctl restart "$APP_NAME"
|
||||||
systemctl status "$APP_NAME" --no-pager || true
|
systemctl is-active "$APP_NAME" >/dev/null 2>&1 && echo "服务已启动:$APP_NAME" || systemctl status "$APP_NAME" --no-pager || true
|
||||||
}
|
}
|
||||||
|
|
||||||
need_root
|
need_root
|
||||||
@@ -89,4 +93,6 @@ echo "服务:systemctl status $APP_NAME"
|
|||||||
echo "配置:$CONFIG_FILE"
|
echo "配置:$CONFIG_FILE"
|
||||||
echo "访问地址: http://${ipaddr:-设备IP}:$PORT/"
|
echo "访问地址: http://${ipaddr:-设备IP}:$PORT/"
|
||||||
echo "面板里点『保存持久化』后,重启系统会由 systemd 启动本项目并自动读取配置恢复 LED。"
|
echo "面板里点『保存持久化』后,重启系统会由 systemd 启动本项目并自动读取配置恢复 LED。"
|
||||||
|
echo "当前只显示/控制颜色:$COLORS"
|
||||||
echo "如需修改端口:PORT=8090 sh install.sh"
|
echo "如需修改端口:PORT=8090 sh install.sh"
|
||||||
|
echo "如需显示全部 LED:COLORS= sh install.sh"
|
||||||
|
|||||||
+1
-2
@@ -1,8 +1,7 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Linux LED Manager Web Panel
|
Description=Linux LED Manager Web Panel
|
||||||
Documentation=https://gitea.chickliu.fun/Hermes/led-manager-panel
|
Documentation=https://gitea.chickliu.fun/Hermes/led-manager-panel
|
||||||
After=local-fs.target network-online.target
|
After=local-fs.target
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|||||||
+33
-4
@@ -21,6 +21,9 @@ 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"))
|
CONFIG_PATH = Path(os.environ.get("LED_CONFIG", "/etc/led-manager/config.json"))
|
||||||
|
# BOSS's device exposes more LED class entries than the physically controllable lights.
|
||||||
|
# Keep the panel focused on the real RGB lights by default; override with --colors if needed.
|
||||||
|
CONTROLLABLE_COLORS = tuple(c.strip().lower() for c in os.environ.get("LED_COLORS", "red,green,blue").split(",") if c.strip())
|
||||||
NAME_RE = re.compile(r"^[A-Za-z0-9_.:+@=-]+$")
|
NAME_RE = re.compile(r"^[A-Za-z0-9_.:+@=-]+$")
|
||||||
|
|
||||||
CSS = r"""
|
CSS = r"""
|
||||||
@@ -102,8 +105,22 @@ def safe_name(name: str) -> str:
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def led_color(name: str) -> Optional[str]:
|
||||||
|
low = name.lower()
|
||||||
|
for color in CONTROLLABLE_COLORS:
|
||||||
|
if low == color or low.startswith(color + ":") or low.startswith(color + "-") or low.startswith(color + "_"):
|
||||||
|
return color
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_controllable_led_name(name: str) -> bool:
|
||||||
|
return not CONTROLLABLE_COLORS or led_color(name) is not None
|
||||||
|
|
||||||
|
|
||||||
def led_path(name: str) -> Path:
|
def led_path(name: str) -> Path:
|
||||||
name = safe_name(name)
|
name = safe_name(name)
|
||||||
|
if not is_controllable_led_name(name):
|
||||||
|
raise ValueError("led hidden by color filter")
|
||||||
# /sys/class/leds entries are usually symlinks to /sys/devices/...
|
# /sys/class/leds entries are usually symlinks to /sys/devices/...
|
||||||
# Do NOT require the resolved target to stay under LED_ROOT, otherwise every real sysfs LED POST is rejected.
|
# Do NOT require the resolved target to stay under LED_ROOT, otherwise every real sysfs LED POST is rejected.
|
||||||
# safe_name() forbids slash and '..', so joining one basename under LED_ROOT is enough here.
|
# safe_name() forbids slash and '..', so joining one basename under LED_ROOT is enough here.
|
||||||
@@ -177,7 +194,14 @@ def load_config() -> Dict[str, Dict[str, object]]:
|
|||||||
leds = data.get("leds", data)
|
leds = data.get("leds", data)
|
||||||
if not isinstance(leds, dict):
|
if not isinstance(leds, dict):
|
||||||
return {}
|
return {}
|
||||||
return {safe_name(str(k)): v for k, v in leds.items() if isinstance(v, dict)}
|
out = {}
|
||||||
|
for k, v in leds.items():
|
||||||
|
if not isinstance(v, dict):
|
||||||
|
continue
|
||||||
|
name = safe_name(str(k))
|
||||||
|
if is_controllable_led_name(name):
|
||||||
|
out[name] = v
|
||||||
|
return out
|
||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -185,7 +209,8 @@ def load_config() -> Dict[str, Dict[str, object]]:
|
|||||||
def save_config(config: Dict[str, Dict[str, object]]) -> None:
|
def save_config(config: Dict[str, Dict[str, object]]) -> None:
|
||||||
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
tmp = CONFIG_PATH.with_suffix(CONFIG_PATH.suffix + ".tmp")
|
tmp = CONFIG_PATH.with_suffix(CONFIG_PATH.suffix + ".tmp")
|
||||||
payload = {"version": 1, "leds": config}
|
filtered = {safe_name(str(k)): v for k, v in config.items() if isinstance(v, dict) and is_controllable_led_name(safe_name(str(k)))}
|
||||||
|
payload = {"version": 1, "colors": list(CONTROLLABLE_COLORS), "leds": filtered}
|
||||||
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
||||||
os.replace(tmp, CONFIG_PATH)
|
os.replace(tmp, CONFIG_PATH)
|
||||||
try:
|
try:
|
||||||
@@ -289,6 +314,8 @@ def list_leds() -> List[Dict[str, object]]:
|
|||||||
if not p.is_dir():
|
if not p.is_dir():
|
||||||
continue
|
continue
|
||||||
name = p.name
|
name = p.name
|
||||||
|
if not is_controllable_led_name(name):
|
||||||
|
continue
|
||||||
b = read_text(p / "brightness")
|
b = read_text(p / "brightness")
|
||||||
mb = read_text(p / "max_brightness")
|
mb = read_text(p / "max_brightness")
|
||||||
tr_raw = read_text(p / "trigger")
|
tr_raw = read_text(p / "trigger")
|
||||||
@@ -443,22 +470,24 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global LED_ROOT, CONFIG_PATH
|
global LED_ROOT, CONFIG_PATH, CONTROLLABLE_COLORS
|
||||||
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("--config", default=str(CONFIG_PATH), help="persistent config path, default /etc/led-manager/config.json")
|
||||||
|
ap.add_argument("--colors", default=",".join(CONTROLLABLE_COLORS), help="comma-separated LED color prefixes to expose, default red,green,blue; empty means expose all")
|
||||||
ap.add_argument("--no-apply", action="store_true", help="do not apply saved config on startup")
|
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)
|
CONFIG_PATH = Path(args.config)
|
||||||
|
CONTROLLABLE_COLORS = tuple(c.strip().lower() for c in args.colors.split(",") if c.strip())
|
||||||
if not args.no_apply:
|
if not args.no_apply:
|
||||||
results = apply_saved_config()
|
results = apply_saved_config()
|
||||||
if results:
|
if results:
|
||||||
print(f"Applied saved LED config: {results}", flush=True)
|
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}; CONFIG_PATH={CONFIG_PATH}", flush=True)
|
print(f"LED Manager Panel listening on http://{args.host}:{args.port} ; LED_ROOT={LED_ROOT}; CONFIG_PATH={CONFIG_PATH}; COLORS={','.join(CONTROLLABLE_COLORS) or 'all'}", flush=True)
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+23
-3
@@ -2,6 +2,7 @@
|
|||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from led_panel import Handler, ThreadingHTTPServer
|
from led_panel import Handler, ThreadingHTTPServer
|
||||||
@@ -34,6 +35,16 @@ def main():
|
|||||||
(d2 / 'brightness').write_text('1')
|
(d2 / 'brightness').write_text('1')
|
||||||
(d2 / 'max_brightness').write_text('1')
|
(d2 / 'max_brightness').write_text('1')
|
||||||
(d2 / 'trigger').write_text('[none] timer')
|
(d2 / 'trigger').write_text('[none] timer')
|
||||||
|
d3 = root / 'green:wlan'
|
||||||
|
d3.mkdir()
|
||||||
|
(d3 / 'brightness').write_text('1')
|
||||||
|
(d3 / 'max_brightness').write_text('1')
|
||||||
|
(d3 / 'trigger').write_text('[none] timer')
|
||||||
|
hidden = root / 'sim:en'
|
||||||
|
hidden.mkdir()
|
||||||
|
(hidden / 'brightness').write_text('1')
|
||||||
|
(hidden / 'max_brightness').write_text('1')
|
||||||
|
(hidden / '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
|
||||||
@@ -43,7 +54,13 @@ def main():
|
|||||||
t.start()
|
t.start()
|
||||||
try:
|
try:
|
||||||
leds = req(port, '/api/leds')
|
leds = req(port, '/api/leds')
|
||||||
assert leds['ok'] and leds['leds'][0]['name'] == 'blue:wan'
|
names = [led['name'] for led in leds['leds']]
|
||||||
|
assert leds['ok'] and names == ['blue:wan', 'green:wlan', 'red:power']
|
||||||
|
try:
|
||||||
|
req(port, '/api/brightness', {'name':'sim:en','value':0})
|
||||||
|
raise AssertionError('hidden LED unexpectedly writable')
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
assert e.code == 400
|
||||||
b = req(port, '/api/brightness', {'name':'blue:wan','value':1})
|
b = req(port, '/api/brightness', {'name':'blue:wan','value':1})
|
||||||
assert b['ok'] and (d / 'brightness').read_text() == '1'
|
assert b['ok'] and (d / 'brightness').read_text() == '1'
|
||||||
tr = req(port, '/api/trigger', {'name':'blue:wan','trigger':'timer'})
|
tr = req(port, '/api/trigger', {'name':'blue:wan','trigger':'timer'})
|
||||||
@@ -64,12 +81,15 @@ def main():
|
|||||||
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'
|
||||||
all_off = req(port, '/api/all-off', {})
|
all_off = req(port, '/api/all-off', {})
|
||||||
assert all_off['ok'] and all_off['changed'] == 2
|
assert all_off['ok'] and all_off['changed'] == 3
|
||||||
assert (d / 'brightness').read_text() == '0'
|
assert (d / 'brightness').read_text() == '0'
|
||||||
assert (d2 / 'brightness').read_text() == '0'
|
assert (d2 / 'brightness').read_text() == '0'
|
||||||
|
assert (d3 / 'brightness').read_text() == '0'
|
||||||
|
assert (hidden / 'brightness').read_text() == '1'
|
||||||
assert '[none]' in (d / 'trigger').read_text() or (d / 'trigger').read_text() == 'none'
|
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'
|
assert '[none]' in (d2 / 'trigger').read_text() or (d2 / 'trigger').read_text() == 'none'
|
||||||
print('OK: API write + persistence + all-off regression passed')
|
assert '[none]' in (d3 / 'trigger').read_text() or (d3 / 'trigger').read_text() == 'none'
|
||||||
|
print('OK: RGB filter + API write + persistence + all-off regression passed')
|
||||||
finally:
|
finally:
|
||||||
httpd.shutdown()
|
httpd.shutdown()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user