From 645c461d2aa90cc2f0a87eeef2699458ac959cf7 Mon Sep 17 00:00:00 2001 From: biss Date: Tue, 8 Sep 2026 20:01:03 +0800 Subject: [PATCH] 3.0.0 --- .gitea/workflows/build.yml | 160 +++++++++++++ LICENSE | 17 ++ Makefile | 71 ++++++ README.md | 171 ++++++++++++++ .../resources/view/led-scheduler/main.js | 150 ++++++++++++ root/etc/config/led-scheduler | 5 + root/etc/hotplug.d/ntp/90-led-scheduler | 16 ++ root/etc/init.d/led-scheduler | 37 +++ root/usr/bin/led-control | 223 ++++++++++++++++++ .../luci/menu.d/luci-app-led-scheduler.json | 13 + .../rpcd/acl.d/luci-app-led-scheduler.json | 19 ++ root/usr/share/ucitrack/led-scheduler.json | 4 + 12 files changed, 886 insertions(+) create mode 100644 .gitea/workflows/build.yml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 htdocs/luci-static/resources/view/led-scheduler/main.js create mode 100644 root/etc/config/led-scheduler create mode 100644 root/etc/hotplug.d/ntp/90-led-scheduler create mode 100644 root/etc/init.d/led-scheduler create mode 100644 root/usr/bin/led-control create mode 100644 root/usr/share/luci/menu.d/luci-app-led-scheduler.json create mode 100644 root/usr/share/rpcd/acl.d/luci-app-led-scheduler.json create mode 100644 root/usr/share/ucitrack/led-scheduler.json diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..8e85665 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,160 @@ +name: Build OpenWrt packages + +on: + push: + branches: + - main + tags: + - "v*" + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - openwrt: "25.12.0" + format: "apk" + sdk_file: "openwrt-sdk-25.12.0-x86-64_gcc-14.3.0_musl.Linux-x86_64.tar.zst" + sdk_sha256: "9f371906ce6d2f95418f69fac06df58bf9df0ffc4abed6206c30f1b547fcfc12" + sdk_url: "https://downloads.openwrt.org/releases/25.12.0/targets/x86/64/openwrt-sdk-25.12.0-x86-64_gcc-14.3.0_musl.Linux-x86_64.tar.zst" + + - openwrt: "24.10.8" + format: "ipk" + sdk_file: "openwrt-sdk-24.10.8-x86-64_gcc-13.3.0_musl.Linux-x86_64.tar.zst" + sdk_sha256: "ac4a0405d2eea821b06f93c14ba13ffa90ad0457648903df7dde02570027ab21" + sdk_url: "https://downloads.openwrt.org/releases/24.10.8/targets/x86/64/openwrt-sdk-24.10.8-x86-64_gcc-13.3.0_musl.Linux-x86_64.tar.zst" + + - openwrt: "23.05.5" + format: "ipk" + sdk_file: "openwrt-sdk-23.05.5-x86-64_gcc-12.3.0_musl.Linux-x86_64.tar.xz" + sdk_sha256: "9e5b6813a3f810123391f5f3ea10dfd1c14be1a880f410c70ec4424bbd1573d2" + sdk_url: "https://downloads.openwrt.org/releases/23.05.5/targets/x86/64/openwrt-sdk-23.05.5-x86-64_gcc-12.3.0_musl.Linux-x86_64.tar.xz" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install build dependencies + shell: bash + run: | + set -eux + + if command -v sudo >/dev/null 2>&1; then + SUDO=sudo + else + SUDO= + fi + + $SUDO apt-get update + $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y \ + build-essential \ + clang \ + flex \ + bison \ + gawk \ + gettext \ + git \ + libncurses-dev \ + libssl-dev \ + python3 \ + python3-pyelftools \ + python3-setuptools \ + rsync \ + swig \ + unzip \ + wget \ + xz-utils \ + zlib1g-dev \ + zstd + + - name: Download and verify SDK + shell: bash + run: | + set -eux + wget -O "${{ matrix.sdk_file }}" "${{ matrix.sdk_url }}" + echo "${{ matrix.sdk_sha256 }} ${{ matrix.sdk_file }}" | sha256sum -c - + + - name: Extract SDK + shell: bash + run: | + set -eux + rm -rf _sdk + mkdir _sdk + + case "${{ matrix.sdk_file }}" in + *.tar.zst) + tar --zstd -xf "${{ matrix.sdk_file }}" --strip-components=1 -C _sdk + ;; + *.tar.xz) + tar -xJf "${{ matrix.sdk_file }}" --strip-components=1 -C _sdk + ;; + *) + echo "Unsupported SDK archive" + exit 1 + ;; + esac + + - name: Prepare LuCI feed + shell: bash + working-directory: _sdk + run: | + set -eux + ./scripts/feeds update luci + ./scripts/feeds install luci-base + + - name: Add package source + shell: bash + run: | + set -eux + PKG="_sdk/package/luci-app-led-scheduler" + rm -rf "$PKG" + mkdir -p "$PKG" + + cp -a \ + Makefile \ + LICENSE \ + README.md \ + htdocs \ + root \ + "$PKG/" + + - name: Configure SDK + shell: bash + working-directory: _sdk + run: | + set -eux + make defconfig + + - name: Build + shell: bash + working-directory: _sdk + run: | + set -eux + make -j"$(nproc)" package/luci-app-led-scheduler/compile V=s + + - name: Collect package + shell: bash + run: | + set -eux + mkdir -p artifacts + + find _sdk/bin -type f \ + -name "luci-app-led-scheduler*.${{ matrix.format }}" \ + -exec cp -v {} artifacts/ \; + + test -n "$(find artifacts -type f -name "luci-app-led-scheduler*.${{ matrix.format }}" -print -quit)" + + echo "Built for OpenWrt ${{ matrix.openwrt }}:" + ls -lh artifacts/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: luci-app-led-scheduler-openwrt-${{ matrix.openwrt }}-${{ matrix.format }} + path: artifacts/* + if-no-files-found: error diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8c1899 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4c4f5a3 --- /dev/null +++ b/Makefile @@ -0,0 +1,71 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-led-scheduler +PKG_VERSION:=0.3.0 +PKG_RELEASE:=1 +PKG_LICENSE:=MIT + +LUCI_TITLE:=LuCI support for LED Scheduler +LUCI_DESCRIPTION:=Schedule LEDs configured by OpenWrt system LED settings. +LUCI_DEPENDS:=+luci-base +rpcd-mod-file +LUCI_PKGARCH:=all + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +# Support both old opkg/IPK staging and new apk staging variables. +[ -n "$${IPKG_INSTROOT}$${PKG_INSTROOT}" ] && exit 0 + +rm -f /tmp/luci-indexcache.* +rm -rf /tmp/luci-modulecache/ + +chmod 0755 \ + /etc/init.d/led-scheduler \ + /usr/bin/led-control \ + /etc/hotplug.d/ntp/90-led-scheduler \ + 2>/dev/null || true + +# OpenWrt 23.05 uses /etc/config/ucitrack, while 24.10+ uses +# /usr/share/ucitrack/*.json. Install the legacy trigger dynamically only +# when the old ucitrack configuration exists. +if [ -f /etc/config/ucitrack ]; then + uci -q delete ucitrack.led_scheduler >/dev/null 2>&1 || true + uci -q set ucitrack.led_scheduler='led-scheduler' + uci -q set ucitrack.led_scheduler.init='led-scheduler' + uci -q commit ucitrack +fi + +/etc/init.d/led-scheduler enable 2>/dev/null || true +/etc/init.d/ucitrack restart 2>/dev/null || true +/etc/init.d/rpcd reload 2>/dev/null || true +/etc/init.d/led-scheduler restart 2>/dev/null || true + +exit 0 +endef + +define Package/$(PKG_NAME)/prerm +#!/bin/sh +[ -n "$${IPKG_INSTROOT}$${PKG_INSTROOT}" ] && exit 0 + +chmod 0755 \ + /etc/init.d/led-scheduler \ + /usr/bin/led-control \ + /etc/hotplug.d/ntp/90-led-scheduler \ + 2>/dev/null || true + +/usr/bin/led-control clear-cron >/dev/null 2>&1 || true +/usr/bin/led-control on >/dev/null 2>&1 || true +/etc/init.d/led-scheduler disable >/dev/null 2>&1 || true + +# Remove the legacy OpenWrt 23.05 ucitrack entry on uninstall. +if [ -f /etc/config/ucitrack ]; then + uci -q delete ucitrack.led_scheduler >/dev/null 2>&1 || true + uci -q commit ucitrack + /etc/init.d/ucitrack restart >/dev/null 2>&1 || true +fi + +exit 0 +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/README.md b/README.md new file mode 100644 index 0000000..f77ab96 --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +# luci-app-led-scheduler 0.3.0 + +## 0.3.0 + +- 增加 OpenWrt 23.05 / 24.10 兼容。 +- OpenWrt 25.12 构建 `.apk`。 +- OpenWrt 24.10 / 23.05 构建 `.ipk`。 +- 包仍为 `all` 架构,因此同一 OpenWrt 系列的产物不绑定 x86、ARM、MIPS 等 CPU 架构。 +- 同时兼容两代 ucitrack: + - OpenWrt 24.10 / 25.12:`/usr/share/ucitrack/*.json` + - OpenWrt 23.05:`/etc/config/ucitrack` +- LuCI 通知兼容: + - 24.10+ 使用 `addTimeLimitedNotification()` + - 23.05 自动退回 `addNotification()` + JavaScript 定时关闭。 +- 仓库新增 `.gitea/workflows/build.yml`,一次 CI 同时生成 APK 与 IPK。 + +### CI 构建矩阵 + +| OpenWrt | 包格式 | SDK target | Package arch | +| --- | --- | --- | --- | +| 25.12.0 | `.apk` | x86/64 | `all` | +| 24.10.8 | `.ipk` | x86/64 | `all` | +| 23.05.5 | `.ipk` | x86/64 | `all` | + +这里故意使用 x86/64 SDK:本项目没有原生二进制,`LUCI_PKGARCH:=all`, +因此 CI 不需要为每一种 CPU 架构重复构建。 + + +## 0.2.4 + +- 修复无 RTC / 开机时间尚未完成 NTP 同步时,重启后 LED 可能错误保持关闭的问题。 +- 开机先恢复 OpenWrt 原生 LED 状态。 +- 只有系统时间可信时才立即按计划判断。 +- 新增 `/etc/hotplug.d/ntp/90-led-scheduler`,在 NTP `step` / `stratum` 事件后重新执行 `led-control apply`。 +- 白天重启不会因为假时间误关灯;夜间重启会在 NTP 校时后按真实时间自动熄灯。 + + +## 0.2.3 + +- 安装/升级阶段主动执行 `chmod 0755`,避免自定义解压/构建流程丢失 Unix 可执行位后导致 `/etc/init.d/led-scheduler: Permission denied`。 +- 同时确保 `/usr/bin/led-control` 具备执行权限。 +- 保留 0.2.2 的 ucitrack 自动注册和限时通知修复。 + + +## 0.2.2 + +- 修复首次安装后 `ucitrack` 未重新扫描,导致 LuCI 保存配置但 cron 不自动更新的问题。 +- 安装/升级后按顺序执行: + 1. `led-scheduler enable` + 2. `ucitrack restart` + 3. `rpcd reload` + 4. `led-scheduler restart` +- 手动控制成功通知 3 秒后自动消失。 +- 操作失败通知 6 秒后自动消失。 +- Makefile 补全 OpenWrt buildroot signature。 + + +这个版本只负责 LED 的“定时”,不再自己维护 WAN/LAN 的正常工作方式。 + +## 设计 + +OpenWrt 原生页面: + +```text +系统 -> LED 配置 +``` + +继续负责: + +- WAN / LAN 对应哪个 sysfs LED +- trigger(例如 netdev) +- 网口绑定、Link/TX/RX 等正常行为 + +本插件: + +```text +系统 -> LED 定时 +``` + +只负责: + +```text +23:00 -> 临时关闭所选 LED +07:00 -> /etc/init.d/led restart +``` + +恢复后 LED 会重新按照 `/etc/config/system` 中的原生配置运行。 + +## 默认配置 + +```uci +config scheduler 'main' + option enabled '0' + option off_time '23:00' + option on_time '07:00' + option all_system_leds '1' +``` + +默认不启用定时。 + +## 页面功能 + +- 启用 LED 定时 +- 关闭时间 / 恢复时间 +- 控制全部“系统 -> LED 配置”中的 LED +- 或选择部分 LED +- 立即关闭 +- 立即恢复 +- 按当前时间应用 +- 重新同步 cron +- 显示系统 LED 的名称、sysfs、trigger + +## 直接复制到路由器测试 + +```sh +scp -r root/* root@192.168.2.1:/ +scp -r htdocs/* root@192.168.2.1:/www/ +``` + +然后: + +```sh +ssh root@192.168.2.1 +chmod +x /usr/bin/led-control /etc/init.d/led-scheduler +/etc/init.d/led-scheduler enable +rm -f /tmp/luci-indexcache.* +rm -rf /tmp/luci-modulecache/ +/etc/init.d/rpcd restart +/etc/init.d/led-scheduler restart +``` + +重新登录 LuCI,进入: + +```text +系统 -> LED 定时 +``` + +## SSH 调试 + +```sh +/usr/bin/led-control list +/usr/bin/led-control selected +/usr/bin/led-control status +/usr/bin/led-control off +/usr/bin/led-control on +/usr/bin/led-control sync +``` + +R2S Plus 当前预计: + +```text +WAN wan_led netdev +LAN lan_led netdev +``` + +## 编译 + +放入自定义 feed 后: + +```sh +./scripts/feeds update myfeed +./scripts/feeds install -p myfeed luci-app-led-scheduler +make defconfig +make package/feeds/myfeed/luci-app-led-scheduler/compile V=s +``` + +输出为: + +```text +luci-app-led-scheduler-0.3.0-r1.apk +``` diff --git a/htdocs/luci-static/resources/view/led-scheduler/main.js b/htdocs/luci-static/resources/view/led-scheduler/main.js new file mode 100644 index 0000000..d62e637 --- /dev/null +++ b/htdocs/luci-static/resources/view/led-scheduler/main.js @@ -0,0 +1,150 @@ +'use strict'; +'require view'; +'require form'; +'require fs'; +'require ui'; +'require uci'; + +function validateTime(section_id, value) { + if (!/^\d{2}:\d{2}$/.test(value)) + return _('时间格式必须为 HH:MM,例如 23:00'); + var parts = value.split(':'); + var hour = Number(parts[0]); + var minute = Number(parts[1]); + if (hour < 0 || hour > 23 || minute < 0 || minute > 59) + return _('请输入有效时间,例如 07:30'); + return true; +} + +function showTimedNotification(message, style, timeout) { + var content = E('p', {}, message); + + /* + * OpenWrt 25.12 LuCI provides addTimeLimitedNotification(). + * Keep a fallback for older/custom FriendlyWrt builds. + */ + if (typeof ui.addTimeLimitedNotification === 'function') + return ui.addTimeLimitedNotification(null, content, timeout, style); + + var node = ui.addNotification(null, content, style); + + window.setTimeout(function() { + if (!node || !node.parentNode) + return; + + node.classList.add('fade-out'); + node.classList.remove('fade-in'); + + window.setTimeout(function() { + if (node.parentNode) + node.parentNode.removeChild(node); + }, 300); + }, timeout); + + return node; +} + +function runControl(action, successMessage) { + return fs.exec('/usr/bin/led-control', [ action ]).then(function(res) { + if (!res || res.code !== 0) { + var message = (res && res.stderr) ? res.stderr.trim() : _('未知错误'); + throw new Error(message || _('命令执行失败')); + } + + showTimedNotification(successMessage, 'info', 3000); + }).catch(function(err) { + showTimedNotification( + _('操作失败:%s').format(err.message || String(err)), + 'error', + 6000 + ); + }); +} + +return view.extend({ + load: function() { + return Promise.all([ uci.load('system'), uci.load('led-scheduler') ]); + }, + + render: function() { + var m, s, o; + var leds = []; + + uci.sections('system', 'led', function(section) { + if (!section.sysfs) return; + leds.push({ + name: section.name || section.sysfs, + sysfs: section.sysfs, + trigger: section.trigger || _('未指定') + }); + }); + leds.sort(function(a, b) { return a.name.localeCompare(b.name); }); + + m = new form.Map('led-scheduler', _('LED 定时'), + _('这里只负责“什么时候关灯、什么时候恢复”。LED 平时的触发器、网口联动等行为仍由“系统 → LED 配置”管理。')); + + s = m.section(form.NamedSection, 'main', 'scheduler', _('定时设置')); + s.addremove = false; + + o = s.option(form.Flag, 'enabled', _('启用 LED 定时')); + o.default = '0'; o.rmempty = false; + o.description = _('关闭此开关并保存应用后,会立即恢复 OpenWrt 原生 LED 配置。'); + + o = s.option(form.Value, 'off_time', _('关闭时间')); + o.default = '23:00'; o.placeholder = '23:00'; o.rmempty = false; o.validate = validateTime; + + o = s.option(form.Value, 'on_time', _('恢复时间')); + o.default = '07:00'; o.placeholder = '07:00'; o.rmempty = false; o.validate = validateTime; + o.description = _('支持跨午夜,例如 23:00 关闭、07:00 恢复。'); + + o = s.option(form.Flag, 'all_system_leds', _('控制“系统 → LED 配置”中的全部 LED')); + o.default = '1'; o.rmempty = false; + + o = s.option(form.MultiValue, 'led', _('控制对象')); + o.depends('all_system_leds', '0'); o.rmempty = true; + if (leds.length) { + leds.forEach(function(led) { + o.value(led.sysfs, _('%s(%s,触发器:%s)').format(led.name, led.sysfs, led.trigger)); + }); + o.description = _('这些项目直接读取自 /etc/config/system,不会复制或修改原有 LED 行为。'); + } else { + o.description = _('当前没有从“系统 → LED 配置”中读取到 LED 项目。'); + } + + s = m.section(form.NamedSection, 'main', 'scheduler', _('手动控制')); + s.addremove = false; + o = s.option(form.Button, '_off_now', _('立即关闭')); + o.inputstyle = 'remove'; + o.description = _('临时将所选 LED 的 trigger 设为 none,并将亮度设为 0。'); + o.onclick = function() { return runControl('off', _('所选 LED 已关闭。')); }; + + o = s.option(form.Button, '_on_now', _('立即恢复')); + o.inputstyle = 'apply'; + o.description = _('重新加载 OpenWrt 原生 LED 配置,恢复 WAN/LAN 等原有触发方式。'); + o.onclick = function() { return runControl('on', _('OpenWrt 原生 LED 配置已恢复。')); }; + + o = s.option(form.Button, '_apply_now', _('按当前时间应用')); + o.inputstyle = 'action'; + o.onclick = function() { return runControl('apply', _('已按当前时间应用 LED 状态。')); }; + + o = s.option(form.Button, '_sync_cron', _('重新同步定时任务')); + o.inputstyle = 'action'; + o.description = _('正常情况下保存并应用会自动同步,无需手动点击。'); + o.onclick = function() { return runControl('sync', _('定时任务已同步。')); }; + + s = m.section(form.TypedSection, '_native_led_info', _('已读取的系统 LED')); + s.anonymous = true; s.addremove = false; + o = s.option(form.DummyValue, '_info'); + o.rawhtml = true; + o.cfgvalue = function() { + if (!leds.length) return '' + _('没有检测到系统 LED 配置。') + ''; + function esc(v) { return String(v).replace(/[&<>"']/g, function(c) { return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } + var rows = leds.map(function(led) { + return '' + esc(led.name) + '' + esc(led.sysfs) + '' + esc(led.trigger) + ''; + }).join(''); + return '' + rows + '
' + _('名称') + '' + _('LED 名称') + '' + _('触发器') + '
'; + }; + + return m.render(); + } +}); diff --git a/root/etc/config/led-scheduler b/root/etc/config/led-scheduler new file mode 100644 index 0000000..16e6d0f --- /dev/null +++ b/root/etc/config/led-scheduler @@ -0,0 +1,5 @@ +config scheduler 'main' + option enabled '0' + option off_time '23:00' + option on_time '07:00' + option all_system_leds '1' diff --git a/root/etc/hotplug.d/ntp/90-led-scheduler b/root/etc/hotplug.d/ntp/90-led-scheduler new file mode 100644 index 0000000..33ebbb3 --- /dev/null +++ b/root/etc/hotplug.d/ntp/90-led-scheduler @@ -0,0 +1,16 @@ +#!/bin/sh + +case "$ACTION" in + step|stratum) + [ -x /usr/bin/led-control ] || exit 0 + + year="$(date +%Y 2>/dev/null)" + [ -n "$year" ] || exit 0 + [ "$year" -ge 2024 ] 2>/dev/null || exit 0 + + /usr/bin/led-control apply >/dev/null 2>&1 + logger -t led-scheduler "schedule reapplied after NTP event: $ACTION" + ;; +esac + +exit 0 diff --git a/root/etc/init.d/led-scheduler b/root/etc/init.d/led-scheduler new file mode 100644 index 0000000..2728654 --- /dev/null +++ b/root/etc/init.d/led-scheduler @@ -0,0 +1,37 @@ +#!/bin/sh /etc/rc.common + +START=99 +STOP=10 + +time_is_sane() { + local year + year="$(date +%Y 2>/dev/null)" + [ -n "$year" ] || return 1 + [ "$year" -ge 2024 ] 2>/dev/null +} + +start() { + /usr/bin/led-control sync >/dev/null 2>&1 + + # Keep LEDs in their normal OpenWrt state until the clock is trustworthy. + /usr/bin/led-control on >/dev/null 2>&1 + + if time_is_sane; then + /usr/bin/led-control apply >/dev/null 2>&1 + else + logger -t led-scheduler "boot clock not synchronized; waiting for NTP" + fi +} + +reload() { + start +} + +restart() { + start +} + +stop() { + /usr/bin/led-control clear-cron >/dev/null 2>&1 + /usr/bin/led-control on >/dev/null 2>&1 +} diff --git a/root/usr/bin/led-control b/root/usr/bin/led-control new file mode 100644 index 0000000..86e4aee --- /dev/null +++ b/root/usr/bin/led-control @@ -0,0 +1,223 @@ +#!/bin/sh + +CONFIG="led-scheduler" +SECTION="main" +LED_ROOT="/sys/class/leds" +CRON_FILE="/etc/crontabs/root" +CRON_BEGIN="# BEGIN luci-app-led-scheduler" +CRON_END="# END luci-app-led-scheduler" + +log() { logger -t led-scheduler "$*"; } +uci_get() { uci -q get "${CONFIG}.${SECTION}.$1" 2>/dev/null; } + +valid_led_name() { + case "$1" in + ""|*/*|*".."*|*[!A-Za-z0-9:_.-]*) return 1 ;; + esac + return 0 +} + +available_led() { + valid_led_name "$1" || return 1 + [ -d "${LED_ROOT}/$1" ] +} + +configured_leds() { + local section sysfs + uci -q show system 2>/dev/null | + sed -n 's/^system\.\([^=]*\)=led$/\1/p' | + while IFS= read -r section; do + sysfs="$(uci -q get "system.${section}.sysfs" 2>/dev/null)" + [ -n "$sysfs" ] || continue + available_led "$sysfs" || continue + printf '%s\n' "$sysfs" + done | + awk '!seen[$0]++' +} + +selected_leds() { + local all selected led + all="$(uci_get all_system_leds)" + [ -n "$all" ] || all="1" + if [ "$all" = "1" ]; then + configured_leds + return 0 + fi + selected="$(uci_get led)" + for led in $selected; do + available_led "$led" || continue + printf '%s\n' "$led" + done | awk '!seen[$0]++' +} + +restore_native() { + if [ -x /etc/init.d/led ]; then + /etc/init.d/led restart >/dev/null 2>&1 + return $? + fi + echo "OpenWrt LED service /etc/init.d/led not found" >&2 + return 1 +} + +turn_off() { + local led path + restore_native >/dev/null 2>&1 || true + selected_leds | while IFS= read -r led; do + available_led "$led" || continue + path="${LED_ROOT}/${led}" + [ -w "${path}/trigger" ] && printf '%s\n' none > "${path}/trigger" 2>/dev/null + [ -w "${path}/brightness" ] && printf '%s\n' 0 > "${path}/brightness" 2>/dev/null + done + log "selected LEDs turned off" +} + +turn_on() { + if restore_native; then + log "native OpenWrt LED configuration restored" + return 0 + fi + log "failed to restore native LED configuration" + return 1 +} + +validate_time() { + local t="$1" h m + case "$t" in [0-9][0-9]:[0-9][0-9]) ;; *) return 1 ;; esac + h="${t%:*}"; m="${t#*:}" + h="${h#0}"; m="${m#0}" + [ -n "$h" ] || h=0; [ -n "$m" ] || m=0 + [ "$h" -ge 0 ] 2>/dev/null || return 1 + [ "$h" -le 23 ] 2>/dev/null || return 1 + [ "$m" -ge 0 ] 2>/dev/null || return 1 + [ "$m" -le 59 ] 2>/dev/null || return 1 + return 0 +} + +time_parts() { + local t="$1" h m + h="${t%:*}"; m="${t#*:}" + h="${h#0}"; m="${m#0}" + [ -n "$h" ] || h=0; [ -n "$m" ] || m=0 + printf '%s %s\n' "$h" "$m" +} + +strip_cron_block() { + local tmp="/tmp/led-scheduler-cron.$$" + mkdir -p "$(dirname "$CRON_FILE")" + touch "$CRON_FILE" + awk -v begin="$CRON_BEGIN" -v end="$CRON_END" ' + $0 == begin { skip=1; next } + $0 == end { skip=0; next } + !skip { print } + ' "$CRON_FILE" > "$tmp" + cat "$tmp" > "$CRON_FILE" + rm -f "$tmp" +} + +reload_cron() { [ -x /etc/init.d/cron ] && /etc/init.d/cron restart >/dev/null 2>&1; } + +clear_cron() { + strip_cron_block + reload_cron + log "cron entries removed" +} + +sync_cron() { + local enabled off_time on_time off_h off_m on_h on_m + enabled="$(uci_get enabled)"; [ -n "$enabled" ] || enabled="0" + off_time="$(uci_get off_time)"; on_time="$(uci_get on_time)" + [ -n "$off_time" ] || off_time="23:00" + [ -n "$on_time" ] || on_time="07:00" + strip_cron_block + if [ "$enabled" != "1" ]; then reload_cron; log "scheduler disabled; cron entries removed"; return 0; fi + validate_time "$off_time" || { echo "Invalid off_time: $off_time" >&2; return 1; } + validate_time "$on_time" || { echo "Invalid on_time: $on_time" >&2; return 1; } + set -- $(time_parts "$off_time"); off_h="$1"; off_m="$2" + set -- $(time_parts "$on_time"); on_h="$1"; on_m="$2" + { + printf '%s\n' "$CRON_BEGIN" + printf '%s %s * * * /usr/bin/led-control off\n' "$off_m" "$off_h" + printf '%s %s * * * /usr/bin/led-control on\n' "$on_m" "$on_h" + printf '%s\n' "$CRON_END" + } >> "$CRON_FILE" + reload_cron + log "cron synchronized: off=${off_time}, on=${on_time}" +} + +minutes_from_time() { + local t="$1" h m + set -- $(time_parts "$t"); h="$1"; m="$2" + echo $((h * 60 + m)) +} + +apply_current_state() { + local enabled off_time on_time now_h now_m now off on + enabled="$(uci_get enabled)" + if [ "$enabled" != "1" ]; then turn_on; return $?; fi + off_time="$(uci_get off_time)"; on_time="$(uci_get on_time)" + [ -n "$off_time" ] || off_time="23:00" + [ -n "$on_time" ] || on_time="07:00" + validate_time "$off_time" || return 1 + validate_time "$on_time" || return 1 + off="$(minutes_from_time "$off_time")"; on="$(minutes_from_time "$on_time")" + if [ "$off" -eq "$on" ]; then turn_on; return $?; fi + now_h="$(date +%H)"; now_m="$(date +%M)" + now_h="${now_h#0}"; now_m="${now_m#0}" + [ -n "$now_h" ] || now_h=0; [ -n "$now_m" ] || now_m=0 + now=$((now_h * 60 + now_m)) + if [ "$off" -lt "$on" ]; then + if [ "$now" -ge "$off" ] && [ "$now" -lt "$on" ]; then turn_off; else turn_on; fi + else + if [ "$now" -ge "$off" ] || [ "$now" -lt "$on" ]; then turn_off; else turn_on; fi + fi +} + +list_configured() { + local section name sysfs trigger + uci -q show system 2>/dev/null | + sed -n 's/^system\.\([^=]*\)=led$/\1/p' | + while IFS= read -r section; do + name="$(uci -q get "system.${section}.name" 2>/dev/null)" + sysfs="$(uci -q get "system.${section}.sysfs" 2>/dev/null)" + trigger="$(uci -q get "system.${section}.trigger" 2>/dev/null)" + [ -n "$sysfs" ] || continue + printf '%s %s %s\n' "${name:-$sysfs}" "$sysfs" "${trigger:-unknown}" + done +} + +status() { + echo "enabled=$(uci_get enabled)" + echo "off_time=$(uci_get off_time)" + echo "on_time=$(uci_get on_time)" + echo "all_system_leds=$(uci_get all_system_leds)" + echo "selected sysfs LEDs:" + selected_leds | sed 's/^/ - /' +} + +usage() { + cat <<'EOF' +Usage: led-control COMMAND + +Commands: + list List LEDs defined by OpenWrt System -> LED Configuration + selected List LEDs currently selected by LED Scheduler + off Turn selected LEDs off + on Restore OpenWrt native LED configuration + apply Apply the correct state for the current time + sync Synchronize cron entries from UCI configuration + clear-cron Remove this application's cron entries + status Show scheduler configuration +EOF +} + +case "$1" in + list) list_configured ;; + selected) selected_leds ;; + off) turn_off ;; + on) turn_on ;; + apply) apply_current_state ;; + sync) sync_cron ;; + clear-cron) clear_cron ;; + status) status ;; + *) usage; exit 1 ;; +esac diff --git a/root/usr/share/luci/menu.d/luci-app-led-scheduler.json b/root/usr/share/luci/menu.d/luci-app-led-scheduler.json new file mode 100644 index 0000000..2444584 --- /dev/null +++ b/root/usr/share/luci/menu.d/luci-app-led-scheduler.json @@ -0,0 +1,13 @@ +{ + "admin/system/led-scheduler": { + "title": "LED 定时", + "order": 91, + "action": { + "type": "view", + "path": "led-scheduler/main" + }, + "depends": { + "acl": [ "luci-app-led-scheduler" ] + } + } +} diff --git a/root/usr/share/rpcd/acl.d/luci-app-led-scheduler.json b/root/usr/share/rpcd/acl.d/luci-app-led-scheduler.json new file mode 100644 index 0000000..00ab849 --- /dev/null +++ b/root/usr/share/rpcd/acl.d/luci-app-led-scheduler.json @@ -0,0 +1,19 @@ +{ + "luci-app-led-scheduler": { + "description": "Grant access to LED Scheduler", + "read": { + "uci": [ + "led-scheduler", + "system" + ] + }, + "write": { + "file": { + "/usr/bin/led-control": [ "exec" ] + }, + "uci": [ + "led-scheduler" + ] + } + } +} diff --git a/root/usr/share/ucitrack/led-scheduler.json b/root/usr/share/ucitrack/led-scheduler.json new file mode 100644 index 0000000..4b9c285 --- /dev/null +++ b/root/usr/share/ucitrack/led-scheduler.json @@ -0,0 +1,4 @@ +{ + "config": "led-scheduler", + "init": "led-scheduler" +}