diff --git a/honk/Makefile b/honk/Makefile new file mode 100644 index 000000000..4cd2b195d --- /dev/null +++ b/honk/Makefile @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: GPL-2.0-only + +include $(TOPDIR)/rules.mk + +PKG_NAME:=honk +PKG_VERSION:=0.0.1_beta81 +PKG_RELEASE:=1 + +# Raw upstream release tag. It keeps the dots in qualifiers (e.g. v0.0.1.beta.49) +# because that is what the GitHub release asset name uses. +HONK_RELEASE_TAG:=v0.0.1.beta.81 + +# SHA256 of each release tarball, kept in sync by scripts/update_honk_version.sh. +HONK_HASH_X86_64:=88e5c3b29200c6ad729cc89636e14a452d974108cd08852c9c1e44074d23e10e +HONK_HASH_AARCH64:=b160a3c8674556222b2b29ce6c975bc346f7ce3cddeb049474c6ca9e0a8191a6 + +PKG_LICENSE:=AGPL-3.0-only +PKG_LICENSE_FILE:=LICENSE +PKG_MAINTAINER:=luci-app-honk maintainers + +ifeq ($(ARCH),x86_64) + HONK_TARGET:=x86_64-unknown-linux-musl + HONK_HASH:=$(HONK_HASH_X86_64) +else ifeq ($(ARCH),aarch64) + HONK_TARGET:=aarch64-unknown-linux-musl + HONK_HASH:=$(HONK_HASH_AARCH64) +endif + +# Default to the stock-malloc build: lower RSS high-water on small gateways. +# Use an empty suffix to switch back to the mimalloc build (faster QUIC). +HONK_SUFFIX:=-stock +HONK_ASSET:=honk-core-$(HONK_RELEASE_TAG)-$(HONK_TARGET)$(HONK_SUFFIX).tar.gz + +PKG_SOURCE:=$(HONK_ASSET) +PKG_SOURCE_URL:=https://github.com/daeuniverse/honk/releases/download/$(HONK_RELEASE_TAG)/ +PKG_HASH:=skip + +include $(INCLUDE_DIR)/package.mk + +define Package/honk + SECTION:=net + CATEGORY:=Network + SUBMENU:=Web Servers/Proxies + TITLE:=honk - eBPF transparent proxy engine (dae-compatible) + URL:=https://github.com/daeuniverse/honk + DEPENDS:=@(x86_64||aarch64) \ + +ca-bundle +ip-full \ + +kmod-sched-core +kmod-sched-bpf +kmod-veth +kmod-nft-queue \ + +v2ray-geoip +v2ray-geosite \ + +@KERNEL_XDP_SOCKETS \ + +HONK_USE_VMLINUX_BTF:vmlinux-btf +endef + +define Package/honk/config + choice + prompt "BTF source for CO-RE" + default HONK_USE_KERNEL_BTF + depends on PACKAGE_honk + + config HONK_USE_KERNEL_BTF + bool "Use kernel BTF (integrated)" + depends on KERNEL_DEBUG_INFO_BTF + + config HONK_USE_VMLINUX_BTF + bool "Use vmlinux-btf package" + endchoice +endef + +define Package/honk/conffiles +/etc/honk/ +/etc/config/honk +endef + +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) + $(TAR) -xzf $(DL_DIR)/$(PKG_SOURCE) -C $(PKG_BUILD_DIR) --strip-components=1 +endef + +define Build/Compile + true +endef + +define Package/honk/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/honk-core $(1)/usr/bin/honk-core + + $(INSTALL_DIR) $(1)/etc/honk/config.d + $(INSTALL_CONF) $(CURDIR)/files/config.dae $(1)/etc/honk/config.dae + $(INSTALL_CONF) $(CURDIR)/files/config.d/node.dae $(1)/etc/honk/config.d/node.dae + $(INSTALL_CONF) $(CURDIR)/files/config.d/route.dae $(1)/etc/honk/config.d/route.dae + $(INSTALL_CONF) $(CURDIR)/files/config.d/dns.dae $(1)/etc/honk/config.d/dns.dae + + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) $(CURDIR)/files/honk.config $(1)/etc/config/honk + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) $(CURDIR)/files/honk.init $(1)/etc/init.d/honk +endef + +$(eval $(call BuildPackage,honk)) diff --git a/honk/files/config.d/dns.dae b/honk/files/config.d/dns.dae new file mode 100644 index 000000000..8693bfd37 --- /dev/null +++ b/honk/files/config.d/dns.dae @@ -0,0 +1,23 @@ +# dns.dae +dns { + bind: 'tcp+udp://[::]:53530' + client_subnet: auto + #ipversion_prefer: 4 + upstream { + local: 'udp://127.0.0.1:53' -> direct + remote: 'tcp+udp://dns.google:53' + } + routing { + request { + qtype(https) -> reject + qname(geosite: gfw) -> remote + fallback: local + } + response { + upstream(remote) -> accept + qname(geosite: private) -> accept + ip(geoip: cn) -> accept + fallback: remote + } + } +} diff --git a/honk/files/config.d/node.dae b/honk/files/config.d/node.dae new file mode 100644 index 000000000..848dcb48f --- /dev/null +++ b/honk/files/config.d/node.dae @@ -0,0 +1,35 @@ +# node.dae +node { + node1: 'xxx' + node2: 'xxx' +} + +subscription { + # 短形式:默认 86400s 刷新一次 + sub: 'http://sub' + + # 短形式 + UA:仍然只能默认 86400s + sub_ua: 'http://sub'(honk/1.0 like) + + # 块形式:可以自定义定时刷新周期 + sub2: { + url: 'http://sub' + ua: 'honk/1.0' + interval: '10000s' # 每 10000 秒刷新一次 + } + + # 禁用定期刷新 + sub3: { + url: 'http://sub' + interval: 0 + } +} + +group { + proxy { + filter: subtag(sub) && !name(keyword: 'ExpireAt:') + filter: name(node1, node2) + policy: score + } +} + diff --git a/honk/files/config.d/route.dae b/honk/files/config.d/route.dae new file mode 100644 index 000000000..31106c7f5 --- /dev/null +++ b/honk/files/config.d/route.dae @@ -0,0 +1,27 @@ +# route.dae +routing { + pname(dnsmasq, zerotier-one) -> direct(must) + dip(224.0.0.0/3, 'ff00::/8', geoip: private) -> direct + domain(geosite: category-bank-cn, geosite: boc@!cn, geosite: synology) -> direct + + # block quic, if u node has poor quic support, enable this line + # l4proto(udp) && dport(443) -> block + + domain(geosite: spotify) -> proxy + domain(geosite: category-doh) -> proxy + domain(geosite: tiktok) -> proxy + domain(geosite: category-ai-!cn, geosite: github) -> proxy + domain(geosite: gfw) -> proxy + + dip(geoip: telegram) -> proxy + + dip(geoip: cn) -> direct + # geoip:cn always using chiness asn, can use domain rule to override + # domain(geosite: cn) -> direct + + # using proxy for ssh, http, https, and common ports, avoid (bt, or other p2p) + # dport(22, 80, 443, 8080, 6443, 9090) -> proxy + + # fallback: direct + fallback: proxy +} diff --git a/honk/files/config.dae b/honk/files/config.dae new file mode 100644 index 000000000..4115010bb --- /dev/null +++ b/honk/files/config.dae @@ -0,0 +1,47 @@ +# config.dae +# Load all honk files placed in ./config.d/. +include { + config.d/*.dae +} + +global { + log_level: warn + check_interval: 600s + check_tolerance: 30ms + lan_interface: br-lan + wan_interface: auto + auto_config_kernel_parameter: true + data_dir: '/etc/honk' + log_file: '/var/log/honk/honk.log' + store_subscribe: true + # if u not using dns bind, set dial_mode to domain++ prefer + dial_mode: domain + bootstrap_resolver: '223.5.5.5:53' + tcp_check_url: 'https://www.gstatic.com/generate_204' + tcp_check_http_method: HEAD + udp_check_dns: 'dns.google.com:53', '8.8.8.8', '2001:4860:4860::8888' +} + +# using for enable experimental features +# native-api, clash-api, cache-file +#experimental { +# native_api { +# enabled: true +# listen: '0.0.0.0:9527' +# secret: 'honk' +# ui: '/etc/honk/dashboard' +# } +# clash_api { +# external_controller: '0.0.0.0:9090' +# external_ui: '/etc/honk/zashboard' +# secret: '' +# default_mode: 'Rule' +# } +# cache_file { +# enabled: true +# path: '/etc/honk/cache.db' +# cache_id: '' +# store_fakeip: false #in this time, this field will be ignored +# } +#} + diff --git a/honk/files/honk.config b/honk/files/honk.config new file mode 100644 index 000000000..8b33dcf88 --- /dev/null +++ b/honk/files/honk.config @@ -0,0 +1,7 @@ + +config honk 'config' + option enabled '0' + option advanced '0' + option dashboard 'doona' + option config_file '/etc/honk/config.dae' + diff --git a/honk/files/honk.init b/honk/files/honk.init new file mode 100644 index 000000000..0e6789b52 --- /dev/null +++ b/honk/files/honk.init @@ -0,0 +1,112 @@ +#!/bin/sh /etc/rc.common + +USE_PROCD=1 +START=96 +STOP=10 + +extra_command "hot_reload" "Hot-reload service" + +CONF="honk" +PROG="/usr/bin/honk-core" +# honk-core hardcodes dae-compatible datapath names (dae0/dae0peer + daens); +# these are the real interface/netns names, not leftover dae config. +HONK_HOST_IFACE="dae0" +HONK_PEER_IFACE="dae0peer" +HONK_NETNS_NAME="daens" +HONK_NETNS_PATH="/run/netns/$HONK_NETNS_NAME" +RESOLV_HONK="/tmp/resolv.conf.honk" + +set_chmod_640() { + mkdir -p /etc/honk/config.d + chmod -R 640 /etc/honk/ 2>/dev/null || true +} + +cleanup_netns() { + ip netns del "$HONK_NETNS_NAME" 2>/dev/null || true + umount "$HONK_NETNS_PATH" 2>/dev/null || true + rm -f "$HONK_NETNS_PATH" 2>/dev/null || true + ip link del "$HONK_HOST_IFACE" 2>/dev/null || true + ip link del "$HONK_PEER_IFACE" 2>/dev/null || true +} + +hijack_resolv_conf() { + grep -q ' /tmp/resolv\.conf ' /proc/mounts && return 0 + + grep -E '^(search|domain) ' /tmp/resolv.conf > "$RESOLV_HONK" 2>/dev/null + + local dns_list LOGICAL_WAN + . /lib/functions/network.sh + network_find_wan LOGICAL_WAN 2>/dev/null + if [ -n "$LOGICAL_WAN" ]; then + dns_list=$(ubus call "network.interface.$LOGICAL_WAN" status 2>/dev/null | \ + jsonfilter -e '@["dns-server"][*]' 2>/dev/null) + fi + + if [ -z "$dns_list" ] && [ -f "/tmp/resolv.conf.d/resolv.conf.auto" ]; then + dns_list=$(awk '/^nameserver/{print $2}' "/tmp/resolv.conf.d/resolv.conf.auto") + fi + + [ -z "$dns_list" ] && dns_list="119.29.29.29 223.5.5.5" + + for dns in $dns_list; do + echo "nameserver $dns" >> "$RESOLV_HONK" + done + + mount --bind "$RESOLV_HONK" /tmp/resolv.conf + mount -o remount,ro,bind /tmp/resolv.conf +} + +restore_resolv_conf() { + grep -q ' /tmp/resolv\.conf ' /proc/mounts || return 0 + mount -o remount,rw,bind /tmp/resolv.conf 2>/dev/null + umount /tmp/resolv.conf 2>/dev/null + rm -f "$RESOLV_HONK" +} + +start_service() { + local enabled config_file + + config_load "$CONF" + config_get_bool enabled "config" "enabled" "0" + [ "$enabled" -eq "1" ] || return 1 + + config_get config_file "config" "config_file" "/etc/honk/config.dae" + + set_chmod_640 + cleanup_netns + hijack_resolv_conf + + procd_open_instance "$CONF" + procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" + procd_set_param command "$PROG" --config "$config_file" + procd_set_param netdev "$HONK_HOST_IFACE" "$HONK_PEER_IFACE" + procd_set_param limits core="unlimited" + procd_set_param limits nofile="1000000 1000000" + procd_set_param term_timeout 8 + procd_set_param respawn 3600 5 5 + procd_set_param stderr 1 + procd_close_instance +} + +service_stopped() { + restore_resolv_conf + cleanup_netns +} + +reload_service() { + stop + start +} + +service_triggers() { + procd_add_reload_trigger "$CONF" +} + +hot_reload() { + set_chmod_640 + if /etc/init.d/honk running; then + "$PROG" reload + else + /etc/init.d/honk restart + fi +} diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile index 02fab38d9..eb01f21c6 100644 --- a/luci-app-fwlive/Makefile +++ b/luci-app-fwlive/Makefile @@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter +coreutils-timeout LUCI_PKGARCH:=all PKG_VERSION:=0.1.46 -PKG_RELEASE:=42 +PKG_RELEASE:=43 # Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI). ifdef SOURCE_DATE_EPOCH diff --git a/luci-app-fwlive/root/usr/libexec/fwlive-is-firewall-event.awk b/luci-app-fwlive/root/usr/libexec/fwlive-is-firewall-event.awk index 325a30938..d782ffb1e 100755 --- a/luci-app-fwlive/root/usr/libexec/fwlive-is-firewall-event.awk +++ b/luci-app-fwlive/root/usr/libexec/fwlive-is-firewall-event.awk @@ -197,7 +197,7 @@ function summary_json(out) { out = out ",\"top_drops\":" summary_top(summary_drop_counts, 3) out = out ",\"top_rules\":" summary_top(summary_rule_counts, 3) "}" # LC_ALL=C makes this a byte budget; utf8_prefix keeps field cuts valid. - if (length(out) > 256) return "{\"scope\":\"top of shown sample\",\"truncated\":true}" + if (length(out) > 1024) return "{\"scope\":\"top of shown sample\",\"truncated\":true}" return out } BEGIN { diff --git a/luci-app-fwlive/root/usr/libexec/fwlive-log-filter.sh b/luci-app-fwlive/root/usr/libexec/fwlive-log-filter.sh index 6e11a3f88..6e95c18ca 100755 --- a/luci-app-fwlive/root/usr/libexec/fwlive-log-filter.sh +++ b/luci-app-fwlive/root/usr/libexec/fwlive-log-filter.sh @@ -6,9 +6,10 @@ # Log messages are treated as data (jsonfilter + awk stdin); never interpolated # into shell command strings. Usage: ubus call log read '...' | fwlive-log-filter.sh # -# Performance: one jsonfilter for @.log[*] plus one awk classify. The same -# awk pass counts enumerated entries for messages_received; process count is -# constant per poll, not O(entries). +# Performance: one jsonfilter for @.log[*] plus one awk classify. An empty +# @.log array pays a second jsonfilter -t to tell healthy empty from a +# parse failure. The same awk pass counts enumerated entries for +# messages_received; process count is not O(entries). # # Entry point (pipeline). The classifier sibling is sourced and must not set # strict mode itself. @@ -37,10 +38,12 @@ fi # (or paused FETCH_LINES_MAX poll) can exceed that and make jsonfilter fail # while this script still returns a valid empty result. # A valid input with no log entries is not fatal; the classifier closes JSON -# and reports messages_received:0. A jsonfilter failure is different: -# it must remain an error so rpcd does not record a fast healthy sample. Keep -# jsonfilter output in a secure temporary file before classification so a -# partial pipeline cannot produce malformed JSON on failure. +# and reports messages_received:0. OpenWrt jsonfilter exits 1 when @.log[*] +# matches nothing, including a real empty array, so that rc alone is not a +# parse failure. A missing log key or malformed JSON must remain an error so +# rpcd does not record a fast healthy sample. Keep jsonfilter output in a +# secure temporary file before classification so a partial pipeline cannot +# produce malformed JSON on failure. # This script runs from rpcd as root and reopens the path for classification. # Match _fwlive_mktemp: only use a verified, sticky dump directory. The # production rpcd caller passes /tmp; the explicit argument lets host tests @@ -50,17 +53,40 @@ if [ ! -d "$FILTER_TMP_DIR" ] || [ -L "$FILTER_TMP_DIR" ] || ! [ -k "$FILTER_TMP printf '%s' '{"log":[],"error":"filter_tempfile_failed"}' exit 1 fi -_filter_tmp=$(mktemp "$FILTER_TMP_DIR/fwlive-filter.XXXXXX") || { +_filter_in=$(mktemp "$FILTER_TMP_DIR/fwlive-filter-in.XXXXXX") || { printf '%s' '{"log":[],"error":"filter_tempfile_failed"}' exit 1 } -trap 'rm -f "$_filter_tmp"' 0 -trap 'rm -f "$_filter_tmp"; exit 129' 1 -trap 'rm -f "$_filter_tmp"; exit 130' 2 -trap 'rm -f "$_filter_tmp"; exit 131' 3 -trap 'rm -f "$_filter_tmp"; exit 143' 15 -if ! jsonfilter -e '@.log[*]' >"$_filter_tmp" 2>/dev/null; then +_filter_tmp=$(mktemp "$FILTER_TMP_DIR/fwlive-filter.XXXXXX") || { + rm -f "$_filter_in" + printf '%s' '{"log":[],"error":"filter_tempfile_failed"}' + exit 1 +} +trap 'rm -f "$_filter_in" "$_filter_tmp"' 0 +trap 'rm -f "$_filter_in" "$_filter_tmp"; exit 129' 1 +trap 'rm -f "$_filter_in" "$_filter_tmp"; exit 130' 2 +trap 'rm -f "$_filter_in" "$_filter_tmp"; exit 131' 3 +trap 'rm -f "$_filter_in" "$_filter_tmp"; exit 143' 15 +cat >"$_filter_in" || { printf '%s' '{"log":[],"error":"filter_failed"}' exit 1 +} +_jf_rc=0 +jsonfilter -e '@.log[*]' <"$_filter_in" >"$_filter_tmp" 2>/dev/null || _jf_rc=$? +if [ "$_jf_rc" -ne 0 ]; then + # Empty @.log array: extract rc=1 and empty stdout on OpenWrt jsonfilter. + # Confirm the member is an array before treating it as a healthy empty poll. + if [ ! -s "$_filter_tmp" ]; then + _log_type=$(jsonfilter -t '@.log' <"$_filter_in" 2>/dev/null) || _log_type= + if [ "$_log_type" = "array" ]; then + : >"$_filter_tmp" + else + printf '%s' '{"log":[],"error":"filter_failed"}' + exit 1 + fi + else + printf '%s' '{"log":[],"error":"filter_failed"}' + exit 1 + fi fi _fwlive_filter_json_reply <"$_filter_tmp" diff --git a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive index 92df592e5..0ef051e09 100755 --- a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive +++ b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive @@ -263,8 +263,8 @@ EOF # One awk pass: prefixcomment. Last quoted log prefix / comment on # the line, including \" units (same capture as the former sed). -# Raw tabs in prefix/comment become the two-character sequence \t so the -# separator tab is unique. Backslashes are not re-encoded. +# Raw tabs become \t and backslashes become \\ so the separator tab is +# unique. Walk bytes: BusyBox awk ≥1.37 gsub cannot emit those sequences. nft_dump_fields() { LC_ALL=C awk ' function last_at(s, needle, p, i, found) { @@ -276,6 +276,22 @@ nft_dump_fields() { } return found } + function tsv_escape(s, out, i, n, c) { + # BusyBox awk ≥1.37 treats gsub replacement "\\t" as a tab + # (and "\\\\" as one backslash). Walk bytes instead. + out = "" + n = length(s) + for (i = 1; i <= n; i++) { + c = substr(s, i, 1) + if (c == "\\") + out = out "\\\\" + else if (c == "\t") + out = out "\\t" + else + out = out c + } + return out + } function quoted_at(s, needle, i, rest, out, c, n, esc, j) { i = last_at(s, needle) if (i == 0) @@ -307,10 +323,8 @@ nft_dump_fields() { comment = quoted_at($0, "comment \"") if (prefix == "" && comment == "") next - gsub(/\\/, "\\\\", prefix) - gsub(/\t/, "\\t", prefix) - gsub(/\\/, "\\\\", comment) - gsub(/\t/, "\\t", comment) + prefix = tsv_escape(prefix) + comment = tsv_escape(comment) print prefix "\t" comment } ' || return 1 diff --git a/luci-app-meow/luci-app-meow/Makefile b/luci-app-meow/Makefile similarity index 95% rename from luci-app-meow/luci-app-meow/Makefile rename to luci-app-meow/Makefile index 35f9429b8..79d3ec293 100644 --- a/luci-app-meow/luci-app-meow/Makefile +++ b/luci-app-meow/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-meow PKG_VERSION:=0.21.2 -PKG_RELEASE:=1 +PKG_RELEASE:=2 LUCI_TITLE:=LuCI support for meow (panel + settings) LUCI_DEPENDS:=+luci-base +meow diff --git a/luci-app-meow/README.md b/luci-app-meow/README.md deleted file mode 100644 index 0181c3015..000000000 --- a/luci-app-meow/README.md +++ /dev/null @@ -1,10 +0,0 @@ -本仓库完全自包含:init 脚本、UCI 默认配置、默认 YAML、LuCI JS/JSON 均已收录 -(取自 meow-rs 仓库 `openwrt/` 目录),CI 里不需要再从上游仓库复制任何文件; -构建时唯一的外部依赖是 `meow/Makefile` 声明的官方 release tarball -(`PKG_SOURCE_URL` + `PKG_HASH` 校验)。 - -## 参考 - -- meow-rs: https://github.com/meow-rs/meow-rs -- OpenWrt feeds 文档: https://openwrt.org/docs/guide-developer/feeds -- OpenWrt apk 包管理器(25.12+): https://openwrt.org/docs/guide-user/additional-software/apk diff --git a/luci-app-meow/luci-app-meow/htdocs/luci-static/resources/view/meow/config.js b/luci-app-meow/htdocs/luci-static/resources/view/meow/config.js similarity index 100% rename from luci-app-meow/luci-app-meow/htdocs/luci-static/resources/view/meow/config.js rename to luci-app-meow/htdocs/luci-static/resources/view/meow/config.js diff --git a/luci-app-meow/luci-app-meow/htdocs/luci-static/resources/view/meow/log.js b/luci-app-meow/htdocs/luci-static/resources/view/meow/log.js similarity index 100% rename from luci-app-meow/luci-app-meow/htdocs/luci-static/resources/view/meow/log.js rename to luci-app-meow/htdocs/luci-static/resources/view/meow/log.js diff --git a/luci-app-meow/luci-app-meow/htdocs/luci-static/resources/view/meow/panel.js b/luci-app-meow/htdocs/luci-static/resources/view/meow/panel.js similarity index 100% rename from luci-app-meow/luci-app-meow/htdocs/luci-static/resources/view/meow/panel.js rename to luci-app-meow/htdocs/luci-static/resources/view/meow/panel.js diff --git a/luci-app-meow/luci-app-meow/htdocs/luci-static/resources/view/meow/settings.js b/luci-app-meow/htdocs/luci-static/resources/view/meow/settings.js similarity index 100% rename from luci-app-meow/luci-app-meow/htdocs/luci-static/resources/view/meow/settings.js rename to luci-app-meow/htdocs/luci-static/resources/view/meow/settings.js diff --git a/luci-app-meow/luci-app-meow/root/usr/share/luci/menu.d/luci-app-meow.json b/luci-app-meow/root/usr/share/luci/menu.d/luci-app-meow.json similarity index 100% rename from luci-app-meow/luci-app-meow/root/usr/share/luci/menu.d/luci-app-meow.json rename to luci-app-meow/root/usr/share/luci/menu.d/luci-app-meow.json diff --git a/luci-app-meow/luci-app-meow/root/usr/share/rpcd/acl.d/luci-app-meow.json b/luci-app-meow/root/usr/share/rpcd/acl.d/luci-app-meow.json similarity index 100% rename from luci-app-meow/luci-app-meow/root/usr/share/rpcd/acl.d/luci-app-meow.json rename to luci-app-meow/root/usr/share/rpcd/acl.d/luci-app-meow.json diff --git a/luci-app-meow/scripts/update_meow_version.sh b/luci-app-meow/scripts/update_meow_version.sh deleted file mode 100644 index 12a48c153..000000000 --- a/luci-app-meow/scripts/update_meow_version.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -# scripts/update_meow_version.sh — 跟随 meow-rs 上游 release 自动更新 feed -# 行为: -# 1) 通过 GitHub API 获取 meow-rs 最新 release tag(形如 v0.21.2) -# 2) 下载对应 aarch64 musl tarball,运行时计算真实 SHA256(不硬编码) -# 3) 用 sed 更新 meow/Makefile 的 PKG_VERSION 与 PKG_HASH(保留 Tab 缩进) -# 以及 luci-app-meow/Makefile 的 PKG_VERSION(两个包版本保持一致) -# 4) 已是最新版本时无修改、退出码 0(CI 判定为无变化) -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -cd "$REPO_ROOT" - -UPSTREAM="meow-rs/meow-rs" -TARBALL_PREFIX="aarch64-unknown-linux-musl.tar.gz" # 静态 musl 二进制包 -MEOW_MK="meow/Makefile" -LUCI_MK="luci-app-meow/Makefile" - -# ---- 1. 获取最新 release tag ---- -TAG="$(curl -fsSL --retry 3 \ - "https://api.github.com/repos/${UPSTREAM}/releases/latest" \ - | grep -oP '"tag_name":\s*"\K[^"]+')" -[ -n "${TAG}" ] || { echo "ERROR: 无法获取上游 release tag" >&2; exit 1; } -VERSION="${TAG#v}" # PKG_VERSION 不带 v 前缀(源码 URL 用 v$(PKG_VERSION) 拼接) - -CURRENT="$(grep '^PKG_VERSION:=' "${MEOW_MK}" | cut -d= -f2)" -if [ "${VERSION}" = "${CURRENT}" ]; then - echo "meow 已是最新版本 ${TAG},无需更新。" - exit 0 -fi - -# ---- 2. 下载并计算真实 SHA256 ---- -URL="https://github.com/${UPSTREAM}/releases/download/${TAG}/meow-${TAG}-${TARBALL_PREFIX}" -TMP="$(mktemp)" -trap 'rm -f "${TMP}"' EXIT -curl -fsSL --retry 3 -o "${TMP}" "${URL}" -HASH="$(sha256sum "${TMP}" | cut -d' ' -f1)" -echo "上游 ${TAG} tarball SHA256 = ${HASH}" - -# ---- 3. 更新 Makefile(sed 保留行内 Tab;两个包版本同步) ---- -sed -i "s|^PKG_VERSION:=.*|PKG_VERSION:=${VERSION}|" "${MEOW_MK}" "${LUCI_MK}" -sed -i "s|^PKG_HASH:=.*|PKG_HASH:=${HASH}|" "${MEOW_MK}" - -# ---- 4. 完整性自检:PKG_SOURCE/URL 拼接 + 哈希回验 ---- -SOURCE_NAME="meow-v${VERSION}-${TARBALL_PREFIX}" -[ -f "${TMP}" ] && [ "$(sha256sum "${TMP}" | cut -d' ' -f1)" = "${HASH}" ] \ - || { echo "ERROR: 哈希回验失败" >&2; exit 1; } -echo "已更新 ${MEOW_MK}: PKG_VERSION=${VERSION}, PKG_HASH=${HASH}" -echo "已更新 ${LUCI_MK}: PKG_VERSION=${VERSION}" -echo "源码包名(由 \$(PKG_VERSION) 拼接): ${SOURCE_NAME}" diff --git a/luci-app-meow/meow/Makefile b/meow/Makefile similarity index 96% rename from luci-app-meow/meow/Makefile rename to meow/Makefile index 0c23e808f..27a898246 100644 --- a/luci-app-meow/meow/Makefile +++ b/meow/Makefile @@ -10,7 +10,7 @@ PKG_LICENSE_FILES:=LICENSE PKG_SOURCE:=meow-v$(PKG_VERSION)-aarch64-unknown-linux-musl.tar.gz PKG_SOURCE_URL:=https://github.com/meow-rs/meow-rs/releases/download/v$(PKG_VERSION)/ -PKG_HASH:=7966b35ee2101e32406009f074f97cbbcc8017489daa02cd3fc5b91b4dbcbfeb +PKG_HASH:=skip PKG_BUILD_DIR:=$(BUILD_DIR)/meow-v$(PKG_VERSION)-aarch64-unknown-linux-musl include $(INCLUDE_DIR)/package.mk diff --git a/luci-app-meow/meow/files/config.yaml b/meow/files/config.yaml similarity index 100% rename from luci-app-meow/meow/files/config.yaml rename to meow/files/config.yaml diff --git a/luci-app-meow/meow/files/firewall.sh b/meow/files/firewall.sh similarity index 100% rename from luci-app-meow/meow/files/firewall.sh rename to meow/files/firewall.sh diff --git a/luci-app-meow/meow/files/gateway.nft.template b/meow/files/gateway.nft.template similarity index 100% rename from luci-app-meow/meow/files/gateway.nft.template rename to meow/files/gateway.nft.template diff --git a/luci-app-meow/meow/files/meow.config b/meow/files/meow.config similarity index 100% rename from luci-app-meow/meow/files/meow.config rename to meow/files/meow.config diff --git a/luci-app-meow/meow/files/meow.init b/meow/files/meow.init similarity index 100% rename from luci-app-meow/meow/files/meow.init rename to meow/files/meow.init diff --git a/luci-app-meow/meow/files/uci-defaults/99-meow-firewall b/meow/files/uci-defaults/99-meow-firewall similarity index 100% rename from luci-app-meow/meow/files/uci-defaults/99-meow-firewall rename to meow/files/uci-defaults/99-meow-firewall diff --git a/openwrt-thermal-guard/.gitattributes b/openwrt-thermal-guard/.gitattributes deleted file mode 100644 index 6313b56c5..000000000 --- a/openwrt-thermal-guard/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text=auto eol=lf diff --git a/openwrt-thermal-guard/.gitignore b/openwrt-thermal-guard/.gitignore deleted file mode 100644 index 8335c478d..000000000 --- a/openwrt-thermal-guard/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# editor and OS noise -*.sync-conflict-* -.stversions/ -.stfolder/ -*~ diff --git a/openwrt-thermal-guard/CHANGELOG.md b/openwrt-thermal-guard/CHANGELOG.md deleted file mode 100644 index f7f9c8335..000000000 --- a/openwrt-thermal-guard/CHANGELOG.md +++ /dev/null @@ -1,591 +0,0 @@ -# Changelog - -All notable changes to `thermal-guard` and `luci-app-thermal-guard`. -Versions follow `Major.Minor.Patch`; both packages are released -together and carry the same number. - -## 2.0.0 - -### Breaking changes - -- **Breaking: `trip_dt` is no longer a UCI option.** The daemon used to write - it into `/etc/config/thermal-guard` from the running process, next to the - operator's own edits. The device tree trips that `trip_active` was made for - now live in `/etc/thermal-guard/trip_base`, written on the first cycle with - `trip_active` set and kept across a sysupgrade. Without `trip_active` the file - is neither read nor written. The package moves an existing `trip_dt` there - once (`/etc/uci-defaults/90-thermal-guard-trip-base`) and removes the option; - a leftover `trip_dt` is ignored. The daemon no longer writes UCI. -- **Breaking: `fan_cooling_device` and `fan_thermal_zone` are gone.** Both - named a numbered path (`cooling_device0`, `thermal_zone0`), and those numbers - can change between boots, which is what the detection by type avoids. The - fan is found by `fan_cooling_device_type`, its zone by the link to it. A - value left in an old configuration is ignored like any unknown option. - -### Notes for operators - -- **Syslog filters may need adjusting.** Every line now carries a - `daemon.*` priority instead of `user.notice`: `crit`, `warning`, `notice`, - `info` as listed under Changed. A `logread` filter or remote syslog rule - written for `user.notice` no longer matches. -- **New dependency `jsonfilter`.** `base-files` already ships it on every - OpenWrt and ImmortalWrt image, so nothing new gets installed there. -- **Update both packages together.** The config page's answer to a changed - firmware runs `thermal-guard trips-keep`, which needs the daemon and the - rpcd ACL of this release; the ACL grants exactly that command line. - -### Added - -- **`thermal-guard trips-keep`** records the device tree trips of the running - boot as the ones `trip_active` was made for. The config page runs it for both - answers to the new firmware notice; the ACL grants exactly this command line. - -### Changed - -- **README: a section on known limitations**: ModemManager and `at_lock`, - notifications without `curl`, boards that report no temperature, and - `wifi_off` on radios netifd does not manage. - -- **Without `curl`, a notification address with a query string or user info - is refused.** `uclient-fetch` and `wget` take the address on the command - line, where every local process can read it in `/proc`, so a token in - `?token=...` or `user:pass@host` would leak. The check runs when the - configuration loads and clears `notify_url`, so nothing piles up in the - outbox; the notify hook still works. An address without `http://` or - `https://`, or with characters `curl` would misread, and a malformed - `notify_header` are now rejected at load for the same reason. Each reason is - logged once, the address itself never. A secret in the path of the address - (Slack, Discord, a private ntfy topic) cannot be detected and stays visible - without `curl`; the README says so. -- **A notification post without `curl` has a time limit.** `uclient-fetch` and - `wget` run under `timeout` (`HTTP_TIMEOUT`, 20 s by default) with `-T 15` for - the network wait, so a hung DNS lookup or server costs one message instead of - blocking the loop. The `curl` path keeps its `-m 15`. -- **`wifi_off` is read back.** It was the only built in stage action that - trusted the exit code of `wifi down`. Now the command is sent up to five times - with 5 s in between (`WIFI_SETTLE`), and the action succeeds only when - `ubus call network.wireless status` reports no radio up; the way back after a - reset waits until every radio that is not disabled is up. Checked against the - real netifd answers of a Banana Pi R3 Mini (`mt_wifi`) and a GL-X3000, kept as - test fixtures. -- **Time limits for `wifi`, `ifdown`, `ifup` and every hook.** The commands get - 15 s (`CMD_TIMEOUT`), the `notify`, `stage1` and `stage2` hooks 15 s - (`HOOK_TIMEOUT`), the `modem-temp` hook 5 s. A hook that ignores TERM is killed - 5 s later. Before, a hung command or hook held the whole loop. -- **The config page checks what the daemon will use.** The order of the - thresholds is checked against the values in the form, not the saved ones, - and an empty field counts as the daemon's default; before, `cpu_warn` 85 with - `cpu_crit` left empty passed and made the daemon fall back to all defaults. - A threshold like `85abc` is refused. `interval` takes whole numbers of 5 and - more only. `fan_governor` is a list of the four governors the daemon accepts. -- **`fan_governor` only takes a kernel governor that regulates:** - `step_wise`, `fair_share`, `bang_bang` or `power_allocator`. Before, any - string went to the zone's `policy` on reset, `user_space` included, which - leaves nothing regulating the fan. Anything else now means `step_wise` and is - logged once. -- **Syslog lines carry a priority** instead of the default `user.notice`: - `daemon.crit` for stages, restored stages, a stage that cannot be persisted - and failed stage actions (also a modem or interface that does not come back - after a reset), `daemon.warning` for rejected options, missing tools, a - sensor that does not read and trips that could not be written, - `daemon.notice` for cooled down, reset, `trips-keep` and new firmware trips - that need a decision, `daemon.info` for the rest. The `blind:` message for a - CPU sensor that stops reading ran through the stage 0 path and would have - been `crit`; it is a `warning` and is still notified. The event log format - is unchanged. -- **netifd's answers are read with `jsonfilter`** instead of `sed` and `awk` on - the text of `ubus` output, which depended on its indentation. Every `ubus - call` has a time limit of 5 s (`UBUS_TIMEOUT`): netifd answers late while - `wifi down` runs, and the readback must not hang on exactly what it checks. - A radio counts as enabled unless netifd reports it `disabled`; the Wi-Fi log - line counts only those. `jsonfilter` is now in `DEPENDS`; `base-files` - already pulls it into every image. -- **The configuration is read with one `config_load` per cycle** instead of - one `uci -q get` per option, about 40 processes every 20 s. The checks on - the values are unchanged. -- **The detected fan pwm output is logged once as `fan pwm output detected at - '...'`.** Before it read `fan pwm path '' unusable`, although nothing was - configured. A configured `pwm_path` that stopped working is still reported - as unusable, once per replacement. - -### Fixed - -- **Four help texts on the config page say what the service does** (English - and German): `fan_governor` applies only when no earlier control mode was - recorded; `hold_minutes` starts stage 2 of 3 when the temperature has not - dropped and stage 3 of 3 only while it still rises; the `zone_path` reason - names the fan's zone, which is no longer configured; `uplink_interface` - decides whether a message goes out or is queued, not whether the modem is - switched off, and is the default for the interface action. - -- **README, hooks README and the comments in `/etc/config/thermal-guard` - describe what the code does**: which stage actions can keep a connection - off after a reboot, what a pwm reset writes back, when `test` shows the - escalation over time, what a missing hook means per hook, what - `fan_cooling_device_type` is used for, and that `fan_governor` applies only - when no earlier governor was recorded. - -- **`thermal-guard test` reports the fan mode the daemon would use.** It took - `fan_mode` as configured, so `auto` without a cooling device or `pwm` - without `pwm_full` and `pwm_idle` claimed "forcing fan to full speed", while - the daemon leaves the fan alone in both cases. It now runs the same fan - detection, which only reads sysfs. - -- **A `cpu_temp_path` set in its `/sys/devices` form no longer shows the - processor among the other sensors.** Paths were compared as written, and - the sensor loops walk the `/sys/class` links. Such a path is now resolved - and compared resolved; the default and detected paths are in class form and - cost nothing extra. - -- **`... cannot send a header` is logged once per fetcher.** Without `curl` it - came with every message and with every retry of the outbox, two minutes - apart. It is said again when the header was empty in between or another - fetcher takes over, and forgotten once `curl` is there. - -- **Trip messages are said once each.** They shared one memory, which every - successful trip write cleared, so `device tree trips are now ...` came back - after each change of the trips. Each message now has its own, cleared when - its cause is gone: the zone found, `trip_active` valid again, the firmware - trips recorded, a write that worked. - -- **The 1.2.0 entry on notifications overstated the config file.** Only `curl` - reads `notify_url` and `notify_header` from a file; `uclient-fetch` and - `wget` take the address on their command line. Since this release such an - address is refused when it carries a query string or user info (see - Changed). - -- **`at_lock` is checked where it is used, not only when the configuration - loads.** 1.2.0 said every UCI path is checked again at its use site; for - `at_lock` that was not so, `tg_at` handed it to `flock` as it was. Now a path - the guard refuses falls back to `/var/lock/modem-at.lock` there as well. - -- **The processor no longer shows up among the other sensors under another - name.** It was left out only when its sensor was called `cpu_thermal`. A - processor zone with another type, or a hwmon input such as `coretemp`, was - listed a second time. Now its own sensor is skipped by path, and where it is - a thermal zone, the hwmon node the kernel mirrors it as is skipped by the - zone's type. The other inputs of the same chip stay listed. - -- **A `modem-temp` hook stopped at its time limit gives no reading.** Its - output was used whatever the exit status, so a hook that printed a value - and then hung still counted. Now the output is dropped when `timeout` - reports the limit (124) or the hook died of a signal (above 128: the KILL - for a hook that ignored TERM, or a BusyBox `timeout`, which reports 143), - as the hooks README already said. - -- **A reading missing at stage entry no longer counts as 0.** The reference - for "no drop" and "still climbing" was stored as 0 when that reading was - missing, and a state file from before the references existed loaded them as - 0. Any later reading then counted as a rise, and stage 3 of 3 could start - without one. A missing reference now stays empty, the comparison counts only - channels with a reading and a reference, and the first reading after the gap - becomes the reference, so the channel compares again as "risen since it came - back". - -- **`thermal-guard test` shows what the readings would trigger, and only on - stdout.** It ran one cycle, while every stage needs two readings in a row, so - it never showed a stage. What it could do was write a `cooled down` line - (notice) into syslog and the event log when a stage was active. Now it runs - two cycles at the same moment and prints each message it would send as - `would notify (): ...`; syslog, the event log and the outbox stay - untouched. - -- **Without `ip`, messages were never delivered from stage 2 of 3 on.** The - uplink check found no route, so every message went to the outbox, and the - outbox waited for a route it could never see. Nothing was logged. Now a - missing `ip` means the uplink is unknown and the message goes out at once, - with `no ip command, cannot tell whether another uplink exists, sending at - once` logged once as a warning. A missing `flock` is logged once as well - (`no flock command, AT queries run without the shared lock`); the queries - still run. - -- **A stage restored after a reboot reports an action that fails.** The - restore reapplied the stage actions without looking at the result, so a - modem that stayed on or Wi-Fi that stayed up went unreported. Now each - failure is logged as crit (`stage N action '...' failed after the reboot`), - and the one restore message, sent after the actions, names what failed. The - event log gets `restoring stage N after a reboot` before the actions start, - so a daemon that dies halfway leaves a trace. -- **Handing a pwm fan back is read back.** Reset wrote the saved mode and duty - cycle without checking them. Both are read back now and a mismatch is logged - as crit. In automatic mode the duty cycle is no longer written at all: the - chip sets it itself, the value from before the takeover means nothing there, - and some drivers refuse it. - -- **The status page no longer says a board without a sensor takes no action.** - The note read "The service stays idle and takes no action", while the modem - thresholds keep working as long as the modem reports a temperature. It now - says that the processor is not watched and the modem thresholds still apply - (English and German). - -- **`wifi_off` no longer counts as done when netifd manages no radio.** An - empty `network.wireless` status used to confirm the action: nothing was up, - because nothing was there, and the stage reported Wi-Fi off on a device whose - radios netifd does not see. Now the action fails (`netifd manages no Wi-Fi - radio, wifi_off switched nothing off`, crit) and is notified like any failed - action. Bringing Wi-Fi back after a reset still passes, there is nothing to - bring back. The daemon also warns once when it loads a configuration that - uses `wifi_off` on such a device, so the mistake shows before the box runs - hot; a ubus error there is left to the action. - -- **`thermal-guard selftest` says why it stops** when it cannot create its - scratch directory, instead of ending with 1 and no word. The - `cooling-levels` case skips on a host without `hexdump` rather than failing. - -- **Handing the zone back on reset is checked.** `tg_fan_release` wrote the - governor to the zone's `policy` without reading it back. A governor the - kernel was built without, such as `power_allocator` on the Filogic kernel, - was refused by the kernel, and the zone stayed on `user_space` with the fan - at full speed and nobody told. Now a governor missing from - `available_policies` becomes `step_wise` (logged as warning), and a zone that - still reports another policy afterwards is logged as crit. - -- **A deleted option kept its last value until the daemon restarted.** - Removing `notify_url` went on sending notifications, removing a threshold - kept the old threshold. Every option now returns to its default within one - cycle. - -- **A persisted stage could be lost to a power cut, and a failed persist went - unnoticed.** Writing `/etc/thermal-guard/state` on a stage change and removing - it on reset now end with `sync`. A persist that cannot be written, for example - on a full or read only overlay, is logged once (`could not persist stage N to - ..., it will not survive a reboot`) instead of being treated as done. The - per cycle path writes only to `/var/run` and does not sync. - -## 1.5.0 - -### Added - -- **Wi-Fi temperatures with the proprietary MediaTek driver.** `mt_wifi`, used - by the MTK vendor images of the Banana Pi R3 Mini, registers no hwmon sensor, - so the status page showed no Wi-Fi readings although the LuCI overview did. - The extra sensors now also read `CurrentTemperature` from - `iwpriv stat` for `ra0`, `rax0` and the other `mt_wifi` - interfaces, only while they are up and bounded by `timeout`, the same way - ImmortalWrt's `autocore` does it. Shown as Wi-Fi 1, Wi-Fi 2, for display only. - -## 1.4.0 - -Where the kernel drives the fan, thermal-guard can now lower its trip points -for a hot modem instead of competing with it. Package release `1.4.0-r2`, -deployed and checked on the GL-X3000 and both Banana Pi R3 Mini. - -### Added - -- **Trip management (`trip_boost`, `trip_active`, `trip_boost_offset`, - `trip_dt`).** With `trip_boost 'modem'` the daemon lowers the active trips of - the thermal zone that drives the fan by `(modem - offset) - cpu`, rounded up - to 5 K and capped at 40 K, so the kernel regulator spins the fan up for a hot - modem it cannot see. Trips only ever go below the baseline, `hot` and - `critical` are never written, a missing reading holds the trips and raising - them needs three cool readings in a row. Stopping the daemon leaves them where - they are. Nothing is written unless `trip_boost` or `trip_active` is set, so - boards with a kernel-driven fan such as the GL-X3000 keep their trips. New - module `/usr/share/thermal-guard/trips.sh`, selftest cases against a planted - sysfs tree. -- **`THERMAL_ROOT`** environment variable, default `/sys/class/thermal`, for - tests only. UCI cannot change it. -- **`/var/run/thermal-guard/modem-temp`**: one line ` - `, written atomically after every good modem reading. Other programs - read the value here instead of querying the modem over AT themselves. A - missing reading leaves the file alone, so its age shows the outage. -- Deployment findings for 1.2.0 on real hardware, one section per device, - taken into the development repository unchanged. - -### Changed - -- **AT queries have a time limit.** `timeout 10 flock timeout 5 - sms_tool …`: the outer limit covers the wait for the lock, which BusyBox - `flock` cannot bound itself, the inner one the query. Before, a hung modem or - a program sitting on the lock stopped the whole loop, stages and trip - management included. A timeout counts as a missing reading. New dependency - `coreutils-timeout`; the selftest fails where `timeout` is missing. -- **Web interface.** System → Thermal Guard, tab Fan: `trip_boost`, the - modem allowance and the switching temperatures (`trip_active`, checked with - the same rules as the service), the state of the trip management, the - `cooling-levels` from the device tree with their duty cycle, and after an - image with other device tree trips a notice with "use the firmware values" and - "keep my values". The status page shows the current trips and by how much - they are lowered. 31 new strings, German catalogue complete. -- Status file: `trip_state`, `trip_reason` (a fixed code the page translates), - `trip_zone`, `trip_base`, `trip_now`, `trip_dt`, `trip_delta`, - `trip_dt_changed`, `trip_limit`, `fan_levels`. -- Docs: README covers trip management, `modem-temp` and the dependency. - The internal deployment documents follow the new layout of the build - repositories. -- `luci-app-thermal-guard` 1.4.0, same number as the daemon. -- **`/lib/upgrade/keep.d/thermal-guard`** keeps `/etc/thermal-guard/` across a - `sysupgrade`. Before, only `/etc/config/thermal-guard` survived: the hooks - an operator placed over ssh, `matrix.env` for `matrix-notify`, and the - persisted stages were lost with every flash. Stages persisted at the time of - a flash now come back after it, the same as after a reboot; clear them before - flashing if the cause is fixed. -- The shipped configuration leaves `trip_boost` unset, which means off. An - image can then switch it on for its board at first boot with a uci-defaults - script that only sets missing options, without overriding an own setting. -- Integration tests for trip management, the hung modem and the held lock run - the daemon against a planted sysfs tree. The suite now takes about two - minutes. -- README: why this package exists, and screenshots of the status page, the - overview block and the configuration tabs on a Banana Pi R3 Mini and a - GL-X3000, in `screenshots/`. -- CI installs shellcheck 0.11.0 from its release, checked against a pinned - SHA256, instead of the Ubuntu package. Ubuntu 24.04 ships 0.9.0, which does - not know the busybox dialect, so the lint job failed on its first run on - GitHub without looking at a single line. - -### Fixed - -- **A package upgrade left the old daemon running.** procd keeps an instance - whose command line did not change, and `thermal-guard daemon` never does, so - after `apk add` the previous version went on running while `thermal-guard -V` - already showed the new one. Found on the GL-X3000 with 1.4.0-r1. The package - now restarts the service after an upgrade on a running system, only if it was - running. Package release `1.4.0-r2`. - -## 1.3.2 - -### Fixed - -- **`thermal-guard selftest` failed on boards with a fan chip.** The case - "nothing to read, nothing shown" assumed no hwmon fan exists; on the GL-X3000 - the real `pwm-fan` is found by name and read, which is correct. The case now - runs only where no fan chip is present. - -## 1.3.1 - -### Fixed - -- **`thermal-guard selftest` worked in the live state directory.** It saved - test stages into `/var/run/thermal-guard/state` under a running daemon, which - then saw a changed `GEN` and dropped its decision ("reset during this - cycle"), placed test hooks there, and wrote its test messages to syslog. The - 1.3.0 cases also emptied the event log, the incident history the status page - shows. The deployment runbook runs the selftest on every board. It now works - in a temporary directory and logs nothing. Found on the GL-MT300N-V2 right - after installing 1.3.0. - -## 1.3.0 - -The daemon no longer touches a fan it has not been told about. Found in the -first buildroot build: a package install starts the service at once, and on a -board without a configuration the shipped `auto` could resolve to `pwm` with an -assumed polarity of `255`/`0`, which on an inverted board stops the fan in the -stage meant to force it. - -### Changed - -- **`fan_mode` defaults to `none`**, in the shipped configuration and in the - daemon when the option is missing. Most boards with a fan already regulate it, - and this package is a protection layer, not a regulator. -- **`pwm_full` and `pwm_idle` have no default.** `fan_mode=pwm` needs both; with - either missing the daemon logs it once and leaves the fan alone. The - configuration page requires both when `pwm` is selected. -- **`auto` never resolves to `pwm`**, only to `cooling_device` or `none`. The - cooling device carries its polarity in the device tree; a raw pwm output - needs it from the operator. -- An upgrade keeps the existing configuration. Affected is only a board that set - `auto` without a cooling device, or `pwm` without both values; the deployment - runbook lists the cases. - -### Added - -- **Stage actions are read back instead of assumed.** `modem_radio_off` asks the - modem `AT+CFUN?` afterwards and resends up to three times, because a - collision on the AT port swallows a command without an error. A modem that - does not confirm fails the stage, which is notified. Switching it back on at - a reset is checked the same way. -- **`modem_radio_off_at`**: `AT+CFUN=4` (flight mode, default) or `AT+CFUN=0` - (minimum function) for a modem that does not take 4. Nothing else is - accepted, because the configuration page can write every option and a free - AT command would let it reset or disable the modem. -- **`interface_down` as a stage action.** Takes `action_interface` down, or - `uplink_interface` when that is unset, and waits for netifd to report it - down. A reset brings it back up. Interface names are checked as UCI section - names, in the daemon and on the page, since they reach `ifdown` and a ubus - path; `uplink_interface` is now checked the same way. -- **The fan is shown even where another program drives it.** `fan_mode` says - what the daemon may write, the new `fan_view` in the status file what it - reads. Under `none` it reads the raw pwm output where `pwm_full` and - `pwm_idle` are set, otherwise the cooling device, otherwise the raw output - without a percentage. The raw output comes first because the kernel derives - a cooling device's `cur_state` assuming ascending `cooling-levels`; the Banana - Pi R3 Mini's descend (`<255 96 0>`), so full speed reads as state 0. The status - page and the overview show the fan whenever `fan_view` names something. - -### Fixed - -- **A reset only undid `modem_radio_off` at stage 2 of 3 and `wifi_off` at - stage 3**, so either action configured for the other stage stayed applied - after a clear. Each stage's configured action is now reversed. -- **The overview block showed a pwm fan as "fan step 140 of 1"** on an inverted - board, because it used the raw value against `pwm_full` as the maximum. It - now shows the percentage the daemon computes, and `fan_max` is only reported - for pwm when both polarity values are set. -- **Stage 1 of 3 promised a fan at full speed under `fan_mode=none`**, in the - notification, on the status page and in two descriptions on the configuration - page, and logged "could not force the fan" as if the stage had failed. It now - says the fan is left to its own controller and logs nothing about it. - -### Translations - -- German: new `Leave the fan alone` (replaces `No fan`) and `Stage 1 of 3 active: - warning, fan left to its own controller`; reworded the descriptions of the fan - mode, `pwm_full`, `pwm_idle`, `cpu_warn` and the Actions tab. -- German: `Switch the modem radio off` (replaces `Switch the modem off - (AT+CFUN=4)`, since the level is now configurable), and new `Take a network - interface down`, `network interface down`, `How the modem is switched off` - with its description, `Flight mode (AT+CFUN=4)`, `Minimum function - (AT+CFUN=0)`, `Interface to take down` with its description. - -## 1.2.0 - -Hardening pass over the whole package, plus the corrections that came out of -testing on a Banana Pi R3 Mini, a GL-X3000 and a GL-MT300N-V2. - -### Fixed - -- **One-shot commands narrated the configuration into the event log.** Every - entry point runs the same validation, so `status`, `reset` and `test` each - repeated whatever it found: three `thermal-guard status` calls on a board - without a sensor produced three identical lines. The event log keeps the last - 50 entries, so a monitoring check calling `status` in a loop flushed the - record of an actual incident out of it within the hour. Only the daemon - reports these now. Found during deployment on a GL-MT300N-V2. -- **A reset could be undone by the cycle it interrupted.** Reading the modem - over AT takes seconds, and a reset landing in that window was overwritten - when the cycle saved. In the worst shape the fan was forced again right after - being handed back, leaving it at full speed while the state said idle, with - no way to clear it: the release was guarded by `stage >= 0`. Resets are now - counted in `GEN` and the count is checked before acting and before saving, - and `thermal-guard reset` releases the fan unconditionally. -- **Turning `enabled` off removed the guard instead of pausing it.** - `start_service` registered no instance, so stages stayed applied, the fan - stayed forced and the status file froze with its last values while the page - kept showing an active stage. The instance now always runs and the daemon - pauses itself. -- **The fan was addressed by hwmon number.** `hwmon2` today is `hwmon1` after - an NVMe moves or a kernel changes, and the daemon would then write a duty - cycle to a different chip and report success. The fan chip is looked up by - its hwmon name. -- **`tg_fan_release` did nothing in pwm mode** unless `fan_release_command` was - set, so the fan stayed on manual full speed after a reset. -- **A failing stage action reported success.** `tg_run_action` ended in an - unconditional `return 0`, so a modem that could not be reached still counted - as switched off. It now returns what the action returned, an unknown action - name is logged, and a failed stage is notified. -- **`tg_cfg` skipped path validation and sensor detection** entirely when `uci` - was absent. -- CI checked `70_thermal-guard.js`, which was renamed to `11_` some releases - ago, so the `luci-syntax` job had been failing on a missing file. -- The rpcd ACL named the status file and the reset command but not the ubus - `file` object they are reached through, nor the `/tmp/run` path that `/var` - resolves to. Both reads and the clear button were denied for every session - that was not root. -- **Installing `luci-app-thermal-guard` as a package did not load its ACL** - (`1.2.0-r2`). The package's own `postinst` replaced the one `luci.mk` - provides, which is the one that reloads rpcd, so the pages were refused until - rpcd restarted. The own `postinst` is gone; the default drops the menu caches - as well. Found in the first buildroot build of either package. - -### Added - -- **Protection stages survive a reboot.** A stage change is mirrored to - `/etc/thermal-guard/state` and reapplied at startup. The state used to live - only in `/var/run`, which is tmpfs, so any reboot silently cleared the - stages and the box came back with the modem enabled and the same broken fan. -- **`pwm_idle`**, the raw value meaning slowest, as the counterpart to - `pwm_full`. The pair carries the polarity, which several boards run inverted - (`1` full speed, `255` stopped). The fan percentage on the status page is - computed from it instead of assuming a rising scale. -- **A SoC sensor is looked for** when `cpu_temp_path` does not read, covering - ramips and x86 boards that report through hwmon rather than a thermal zone. -- **"This device reports no temperature at all"** as a state of its own, told - apart from a wrong path and shown on the status page. Some boards, the - GL-MT300N-V2 among them, expose no sensor whatsoever. -- **A second fan controller is detected.** When a value the daemon wrote has - been replaced, it logs which `fan_mode` avoids the fight, reasserts and - reports it to the page. -- The status file carries `now`, `interval`, `enabled`, `no_sensor`, - `fan_percent`, `fan_foreign` and `cpu_src`, which lets the page tell running, - paused and stopped apart instead of showing a stale snapshot as current. -- `tests/run-integration.sh`, exercising reset, the reboot restore, a garbled - state file and the collectd filter through the real scripts. - -### Changed - -- **Notifications go out over HTTP.** `notify_url` is posted to with `curl` - where present and `uclient-fetch` otherwise; `notify_header` carries an - authorisation token where `curl` is installed. Both go through a config file - rather than the command line, because `/proc/*/cmdline` is readable by every - local process, and both are rejected if they contain anything that could - turn that config file into further directives. -- **Custom code lives in `/etc/thermal-guard/hooks/`** as executables, not in - UCI as strings. The rpcd ACL grants write access to every option in this - configuration, so a shell command stored there was a way for anyone who - could reach the configuration page to run code as root. `sh -c` no longer - appears in the package at all. -- Paths taken from UCI are checked against the subtree they belong to, in - `tg_cfg` and again at each use site, and the config view validates the same - prefixes. -- `thermal-guard-collectd` passes status values through an integer filter - before building a `PUTVAL` line. -- A cooling device that no thermal zone binds is usable; the zone is only - needed to take the device away from a governor. `cooling_device` is preferred - where available because `max_state` means maximum cooling whichever way the - device tree runs its `cooling-levels`, so that mode cannot get the polarity - wrong. -- Reload is a no-op. `tg_cfg` runs every cycle, so a changed option is live - within one interval, and restarting would drop a stage action in flight. -- The stage banner names the configured action instead of the default, so a - box with `stage1_action=wifi_off` no longer claims the modem was switched - off. -- German catalogue regenerated: 124 entries, 34 of them new, 11 stale ones - removed. - -### Documented - -- **What this package is for.** It is a protection layer, not a fan controller, - and where a board already has one `fan_mode=none` is the normal setting - rather than a workaround. The built-in fan handling is for boards with - nothing else. -- **Sharing the modem's AT port.** Overlapping AT queries return nothing - useful and neither side reports a fault. Deploying next to a fan controller - that queried the modem without a lock garbled that controller's reading, - collapsed its control temperature and left the fan at its minimum on a board - at 68 °C. The symptom was at the fan, the cause at the serial port. -- **`fan_mode=pwm` is not a regulator**, now a warning in the deployment - runbook. Below the thresholds it writes nothing to the fan. Pausing the Banana - Pi's `fan-control` in favour of it left `pwm1` at a stale minimum while the - SoC climbed from 58 to 68 °C. -- **Deployment runbook, gaps found on hardware.** Check for an installed - package (`apk info -e` / `opkg status`) and remove it through the package - manager before deleting files by hand; the Banana Pi carried 1.1.0 as apk - packages. The migration now lists the options this repository removed in - 1.2.0 (`notify_command` and the other `*_command` options) with their - replacements, not only the BananaWRT variant's. `pwm_idle='255'` is set on - the Banana Pi, and the runbook separates what a missing value does: - `fan_percent` pinned at 100 % under `fan_mode=pwm`, empty under - `fan_mode=none`. -- **Whether there is a kernel backstop at all.** Preferring `cooling_device` - used to be justified by the kernel continuing to regulate while the guard is - idle. That only holds where a thermal zone actually binds the cooling device, - which several boards do not do, and the README now says how to check. - -### Removed - -- `notify_command`, `stage1_command`, `stage2_command`, `modem_temp_command` - and `fan_release_command`. The first is replaced by `notify_url`, the next - three by hooks of the same name, and the last by recording what the pwm chip - was set to before the takeover and writing it back on release. - -## 1.1.0 - -- Configuration page under System, clear button on the status page -- Other board temperature sensors reported alongside SoC and modem -- Fan mode no longer reset to `auto` on every cycle - -## 1.0.0 - -- Staged overheating protection: force the fan, modem radio off, Wi-Fi off -- Status page, overview block and collectd feed diff --git a/openwrt-thermal-guard/LICENSE b/openwrt-thermal-guard/LICENSE deleted file mode 100644 index 9efa6fbc9..000000000 --- a/openwrt-thermal-guard/LICENSE +++ /dev/null @@ -1,338 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, see . - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Moe Ghoul, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/openwrt-thermal-guard/README.md b/openwrt-thermal-guard/README.md deleted file mode 100644 index fbcb2a81c..000000000 --- a/openwrt-thermal-guard/README.md +++ /dev/null @@ -1,458 +0,0 @@ -# thermal-guard - -Overheating protection for OpenWrt routers with staged mitigation. - -![Status page on a Banana Pi R3 Mini](screenshots/status-bpi-r3-mini.png) - -## Why this exists - -The idea for this project came after a fan malfunction on my Banana Pi R3 Mini -damaged its Fibocom FM350-GL 5G module beyond repair. An inexpensive part -failed and took a much pricier one with it. Since one of my BPI R3 Minis runs -in a motorhome, I did not want to go through that again and set out to build -something that prevents it. This package is the result. - -A better fan controller would not have helped: the one on the board was -convinced it had the fan at full speed. What was missing was something that -ignores what the fan is supposed to be doing and only looks at the temperature. - -**This is not a fan controller.** OpenWrt has several of those and they do the -job better: they map temperature onto a curve and keep the box in its normal -band. This one does nothing at all until a threshold is crossed, and then does -something drastic that does not undo itself. Thermostat and thermal fuse, two -layers rather than two opinions. - -So where a fan controller is already installed, leave it in charge: `fan_mode` -is `none` by default, and that is the regular configuration for such a board, -not a workaround. The built-in fan handling exists for boards that have nothing else, -and "force it to maximum" is a poor substitute for a curve. What this package -adds is the layer above: the one that assumes the controller can fail, or can -be wrong, and acts on the temperature rather than on what the fan reports. - -A fan that stops is rarely noticed until something dies. `thermal-guard` watches -SoC and modem temperature and, when the box runs hot, escalates in three stages: - -| Stage | Default action | Triggered by | -|---|---|---| -| 1 of 3 | Warning; fan forced to full speed where `fan_mode` lets the daemon drive it | `cpu_warn` or `modem_warn` reached | -| 2 of 3 | Modem radio off (`AT+CFUN=4`) | `cpu_crit`/`modem_crit`, or stage 1 held for `hold_minutes` without any drop | -| 3 of 3 | Wi-Fi off | `cpu_emergency`, or stage 2 held for `hold_minutes` while still climbing | - -The web interface counts stages from one, the daemon and the status file count -them from zero, so stage 1 of 3 is `stage=0` in `thermal-guard status`. What -stages 2 and 3 do is configurable; the table shows the defaults. Each built in -action is read back rather than assumed: the modem is asked for its state -(`AT+CFUN?`), `interface_down` waits for netifd to report the interface down, -`wifi_off` waits for netifd to report no enabled radio up. An action that does not -confirm counts as failed, and the failure is notified. A `command` hook counts -by its exit code. - -Three design decisions worth knowing before you deploy it: - -* **Stages never roll back on their own.** Cooling down is reported once, the - stage stays. A fan that failed once will fail again, and a silent recovery - hides the fault. Clearing is an operator decision: `thermal-guard reset`. -* **A reboot does not clear them either.** A stage change is mirrored to - `/etc/thermal-guard/state`, and the daemon reapplies it at startup. The box - did not get a new fan while it was down, so coming back up with the modem - enabled would just repeat the incident. The restore is logged and notified. -* **A crash leaves the fan running.** The daemon only hands the fan back on an - explicit reset, so dying mid-incident is the safe direction. - -If a restored stage ever locks you out, reach the box over a wired port or -serial and run `thermal-guard reset`. Only a device that reached a stage whose -action is `wifi_off` or `interface_down` (stage 3 of 3 with the defaults) comes -back from a reboot with that connection still off, and such a device has a fan -and a modem, so it has wired ports too. - -Every transition needs two consecutive readings above the threshold, which keeps -sensor glitches from triggering anything. A missing reading never blocks an -escalation: at a threshold it simply does not count, on the hold path it counts -as no drop, so it never delays stage 2 of 3, and a reference missing at stage -entry is taken from the first reading after the gap. - -Escalating from stage 1 of 3 on time alone, when the temperature has not dropped, is -the part that earns its keep. The guard never asks whether the fan turned; it -asks whether the box got cooler. That covers a fan that failed, a fan cable -that came off, and the case that prompted this package: a fan controller whose -pwm polarity was configured the wrong way round, which stopped the fan while -reporting that it had gone to full speed. No software can tell those apart on -a board without a tachometer, and none of them have to be told apart, because -the answer is the same. Set `hold_minutes` with that in mind: it is how long a -stage is given to prove it worked before the next one starts. - -## Screenshots - -The status page on two boards: a Banana Pi R3 Mini with a Fibocom modem, an NVMe -drive and the MediaTek vendor Wi-Fi driver, where another program drives the fan, -and a GL-iNet GL-X3000 with a Quectel modem, where the kernel does. Wi-Fi 1 and 2 -come from `iwpriv` on the one and from hwmon on the other. - - - - - - -
Status page, Banana Pi R3 MiniStatus page, GL-X3000
- -The same readings in a block on the LuCI overview page: - - - - - - -
Overview block, Banana Pi R3 MiniOverview block, GL-X3000
- -
-Configuration page, System → Thermal Guard - -General - -![General tab](screenshots/settings-general.png) - -Thresholds for the three stages - -![Thresholds tab](screenshots/settings-thresholds.png) - -Fan, including the switching temperatures of a kernel-driven fan - -![Fan tab](screenshots/settings-fan.png) - -Modem, Fibocom on the Banana Pi and Quectel on the GL-X3000 - -![Modem tab, Fibocom](screenshots/settings-modem-fibocom.png) -![Modem tab, Quectel](screenshots/settings-modem-quectel.png) - -Actions and notifications - -![Actions tab](screenshots/settings-actions.png) - -
- -## Install - -```sh -opkg install thermal-guard luci-app-thermal-guard # or: apk add ... -/etc/init.d/thermal-guard enable -/etc/init.d/thermal-guard start -``` - -Two LuCI pages: **Status → Thermal Guard** shows the temperatures, the fan and -the current stage, and clears the stages; **System → Thermal Guard** edits the -configuration. A changed option is picked up within one interval, and a -deleted one is back at its default by then, so Apply needs no restart. - -The package depends on `coreutils-timeout`: every AT query runs under a time -limit, so a hung modem costs one reading instead of the whole loop. It also -depends on `jsonfilter` to read netifd's answers; `base-files` ships it on every -image anyway. Optional runtime helpers, none of them a hard dependency: -`sms_tool` for the modem AT commands, `flock` for the AT lock, `ip` for the -uplink check, and `curl` for notification headers. Without `flock` the AT -queries run without the lock; without `ip` the daemon cannot tell whether a -route exists and sends every message at once instead of queueing it. Each is -logged once as a warning. - -## Configuration - -`/etc/config/thermal-guard`, section `main`. Defaults leave the fan alone and -read a Quectel or Fibocom modem; driving the fan is an explicit choice. - -| Option | Default | Meaning | -|---|---|---| -| `enabled` | `1` | Pauses the checks when `0`; stages and the fan stay until a reset | -| `interval` | `20` | Seconds between readings, minimum 5 | -| `cpu_warn` / `cpu_crit` / `cpu_emergency` | `70` / `80` / `90` | SoC thresholds in °C, must be ordered | -| `modem_warn` / `modem_crit` | `80` / `88` | Modem thresholds in °C | -| `hold_minutes` | `3` | How long a stage must persist before time alone escalates | -| `hysteresis` | `10` | Degrees below the warn thresholds that count as cooled down | -| `extra_sensors` | `1` | `0` hides the board's other sensors (Wi-Fi, ethernet phys, NVMe) from the status page | -| `cpu_temp_path` | `/sys/class/thermal/thermal_zone0/temp` | Sysfs source under `/sys/class/thermal/` or `/sys/class/hwmon/`, degrees or millidegrees. A sensor is looked for when this does not read | -| `fan_mode` | `none` | `cooling_device`, `pwm`, `auto`. `auto` takes the cooling device if there is one, otherwise it stays `none`; it never picks `pwm` | -| `fan_cooling_device_type` | `pwm-fan` | Type string used to find the cooling device | -| `fan_governor` | `step_wise` | Governor the zone is handed back to on reset when no earlier one was saved: `step_wise`, `fair_share`, `bang_bang` or `power_allocator`. Anything else, `user_space` included, means `step_wise`, and so does a governor the kernel was built without | -| `pwm_path`, `pwm_enable_path` | unset | `pwm` mode only; found by hwmon name when left empty | -| `pwm_full`, `pwm_idle` | unset | Raw values for fastest and slowest, both required for `pwm`. Without both the fan is left alone | -| `modem_source` | `auto` | `quectel`, `fibocom`, `file`, `command`, `none` | -| `modem_at_port` | `auto` | Serial port for AT commands | -| `modem_temp_file` | unset | Source for `file` mode; `command` mode reads the `modem-temp` hook | -| `at_lock` | `/var/lock/modem-at.lock` | Shared lock. Every other AT user on this port has to take the same one, see below | -| `trip_boost` | `off` | `modem` lowers the fan's trip points while the modem runs hotter than the CPU, see below | -| `trip_boost_offset` | `15` | Degrees the modem may run above the CPU before the trips go down, 0 to 40 | -| `trip_active` | unset | List, baseline of the fan's active trips in °C, ascending, one per trip. Unset means the device tree values | -| `stage1_action` / `stage2_action` | `modem_radio_off` / `wifi_off` | Also `interface_down`, `command`, which runs a hook, or `none` | -| `modem_radio_off_at` | `AT+CFUN=4` | Or `AT+CFUN=0` for a modem without flight mode. Nothing else is accepted | -| `action_interface` | unset | Interface `interface_down` takes down; `uplink_interface` when unset | -| `notify_url` | unset | The warning text is posted here | -| `notify_header` | unset | Extra header for the post, needs `curl` | -| `uplink_interface` | unset | Modem interface name, lets the daemon tell whether another uplink exists | - -### Fan modes - -`cooling_device` sets the cooling device to its maximum step, taking the thermal -zone off its governor first if one drives it. Prefer this mode wherever the -board offers it: `max_state` means maximum cooling whichever way the device tree -runs its `cooling-levels`, so this mode cannot get the polarity wrong. - -On reset the zone gets back the governor it had before, or `fan_governor` when -none was recorded. A governor the running kernel does not list in the zone's -`available_policies` is replaced by `step_wise`. The policy is read back -afterwards: a zone that still reports another one is logged as `crit`, since -then nothing regulates the fan. - -A cooling device that no thermal zone binds still works, there is simply nothing -to take over and nothing to hand back. Whether one is bound is worth knowing, -because it decides whether there is a kernel backstop underneath at all: - -```sh -ls /sys/class/thermal/thermal_zone*/cdev* 2>/dev/null || echo "no zone drives any cooling device" -``` - -Where nothing is printed, the device tree defines no `cooling-maps`, the zone's -trip points fire into nothing, and the only regulation on the board is whatever -runs in userspace. That is worth fixing in the device tree, because it is the -difference between a fan controller that can fail safely and one whose failure -nobody catches until this package escalates. - -`pwm` writes the duty cycle to sysfs directly. Use it on boards where the fan is -not wired into a thermal zone. Before taking the chip over, the daemon records -its mode and duty cycle. Reset writes the mode back, and the duty cycle where -the chip was in manual mode; in automatic mode the chip sets it itself. Nothing -has to be configured for the way out. Leaving `pwm_path` empty is fine: the fan chip is -looked up by its hwmon name, which survives the renumbering that happens when -another sensor appears. - -Polarity is not something the daemon can work out, and getting it wrong stops -the fan in an emergency instead of speeding it up. Several boards, the Banana Pi -R3 Mini among them, run the signal inverted: `1` is full speed and `255` stops -the fan. So there is no default: `pwm` needs both `pwm_full` and `pwm_idle`, and -with either missing the daemon logs it once and leaves the fan alone. For the -same reason `auto` never resolves to `pwm`. Confirm the values by ear rather -than by reading the number. - -If another service already controls the fan, keep `fan_mode` at `none`. Stage -1 of 3 is then a warning only. Setting a fan mode anyway works: the daemon notices when a value it wrote has been replaced, says so in the log and -reasserts, but two controllers writing one file is worth avoiding, and the -other one almost certainly has the better curve. Nothing is lost: the stage -that forces the fan is the one a working controller has already covered, and -the stages that matter come after it. - -### Trip management - -Where the kernel drives the fan through a thermal zone, it only compares the -CPU against the zone's trip points; a modem running hotter than the CPU does not -reach it. With `trip_boost 'modem'` the daemon lowers the zone's active trips by - -``` -delta = (modem - trip_boost_offset) - cpu, 0 if negative, - rounded up to 5 K, at most 40 K -``` - -which is the same as regulating on `max(cpu, modem - offset)`, but leaves the -regulator in the kernel. The zone is the one whose `cdevN` points at the fan's -cooling device, the trips are the `active` ones bound to it, whatever their -numbers are. `hot` and `critical` are never written. - -The trips only ever go below the baseline, never above it. A wrong modem reading -can therefore only make the fan louder, not slower. A missing reading keeps the -trips where they are, raising them again takes three cool readings in a row, and -not at all while a stage is in force. Stopping the daemon leaves them as they -are; setting `trip_boost 'off'` with `trip_active` unset hands the zone its -device tree values back once and leaves it alone from then on. - -Nothing is written unless `trip_boost` or `trip_active` is set, so a board whose -fan the kernel already drives keeps its trips as they are. - -A new firmware image can bring other trips in its device tree. The first cycle -with `trip_active` set records the device tree values of that boot in -`/etc/thermal-guard/trip_base`, which survives a sysupgrade. When a later boot -finds other values, the log says so, the status file carries -`trip_dt_changed=1`, and **System → Thermal Guard** offers to use the new values -or to keep your own. `trip_active` stays in force until you decide. Both -choices run `thermal-guard trips-keep`, which records the values of the running -boot. Without `trip_active` the file is neither read nor written. - -### Sharing the modem with another program - -A modem answers two overlapping AT conversations with nothing useful, and the -program that gets the nothing rarely says so. This daemon takes `at_lock` around -every query. **Anything else on the box that talks to the same port has to take -the same lock**, or both readings become unreliable. ModemManager cannot, see -the known limitations below. - -That is not a theoretical concern. On a Banana Pi R3 Mini this package was -installed next to a fan controller that queried the modem without a lock. The -collisions garbled the controller's modem reading, its control temperature -collapsed, and it set the fan to its minimum while the board sat at 68 °C. -Nothing in either program reported a fault: the fan controller believed it was -regulating correctly, and this daemon believed the fan was somebody else's -business. The symptom appeared at the fan, the cause was at the serial port. - -If the other program cannot be changed, give it the modem and set -`modem_source` to `none` here, but understand the cost. A modem die runs -appreciably hotter than the SoC, commonly by 10 to 20 K, so protecting it -through the processor thresholds alone means acting late for the part that is -actually at risk. Prefer fixing the lock. - -Programs that only need the temperature do not have to ask the modem at all. -After every good reading the daemon writes one line to -`/var/run/thermal-guard/modem-temp`: - -``` -47 1790150400 at -``` - -degrees, Unix time of the reading, source (`at`, `file` or `command`). The file -is replaced atomically and not touched when a reading fails, so a reader that -finds it older than three intervals should treat the value as unknown. - -### Notifications - -Set `notify_url` and the message is posted there as the request body. That -covers ntfy, Gotify, Matrix, Slack and most other things you would want to be -woken by, and it needs no shell command in the configuration to do it. - -``` -option notify_url 'https://ntfy.sh/my-topic' -``` - -`curl` is used when it is installed, otherwise `uclient-fetch` from the OpenWrt -base, or `wget`. Only `curl` can send `notify_header`, and only `curl` reads the -address and the header from a file (mode 600) instead of the command line, where -`/proc` shows them to every local process. - -Without `curl` the daemon therefore refuses an address that carries a query -string or user info (`?token=...`, `user:pass@host`). It logs the reason once -and sends nothing over HTTP; the notify hook below still works. A secret in the -path of the address, as in Slack or Discord webhooks or a private ntfy topic, -cannot be told apart from an ordinary path and is sent as it is. Without `curl` -it is visible in the process list while the message goes out. Install `curl` -for such services. - -For a delivery HTTP cannot express, put an executable at -`/etc/thermal-guard/hooks/notify`; it gets the text on stdin. With both set, -the URL is tried first and the hook is the fallback. - -From stage 2 of 3 on the daemon checks whether a route exists that does not run -through the modem; if not, the message is queued and retried, because that -stage is about to switch the modem off. - -### Hooks - -`/etc/thermal-guard/hooks/` holds executables the daemon runs for `notify`, -for `stage1` and `stage2` when the action is `command`, and for `modem-temp` -when `modem_source` is `command`. See the `README` installed in that directory. -The whole of `/etc/thermal-guard/` is listed in `/lib/upgrade/keep.d/`, so -hooks and any files they need, persisted stages and `trip_base` survive a -`sysupgrade`. - -Every hook runs under a time limit: 15 s for `notify`, `stage1` and `stage2`, -5 s for `modem-temp`, which runs every cycle. A hook still running then gets -TERM, and KILL 5 s later, and counts as failed. - -They are files rather than options because the rpcd ACL that lets the web -interface edit this configuration does not grant write or execute access to -that directory. Custom code therefore has to be put there by someone with -shell access, and editing the configuration cannot introduce any. - -## Commands - -```sh -thermal-guard status # current stage, readings, fan state -thermal-guard reset # clear all stages, hand the fan back, radio on -thermal-guard trips-keep # record this boot's device tree trips in trip_base -thermal-guard test 85 70 # two cycles with these readings, print what they would trigger, change nothing -thermal-guard selftest # run the decision logic against its test cases -``` - -`test` runs its two cycles at the same moment, because every stage needs two -readings in a row. The hold time does not pass in between: from an idle state, -stage 3 of 3 shows only through `cpu_emergency`, and the escalation over time -cannot be tried this way. `test 75 66` staying at stage 1 of 3 says nothing -about it. On a box already in a stage, its hold timers apply. - -`/var/run/thermal-guard.status` holds a machine readable snapshot. Everything -else reads that file, so only the daemon talks to the modem, and `thermal-guard -reset`, which switches the radio back on under `at_lock`. - -## Where the readings show up - -* **Status -> Thermal Guard** is the monitor page: temperatures, fan state, stage. -* **Status -> Overview** gets a short temperature block, installed with the LuCI app. -* **Syslog**, tag `thermal-guard`, facility `daemon`: `crit` for a stage, a - restored stage, a stage that cannot be persisted and every stage action that - failed, the way back after a reset included; `warning` for a rejected option, - a missing tool, a sensor that does not read and trips that could not be - written; `notice` for cooled down, reset, `trips-keep` and new firmware trips - that need a decision; `info` for the rest. `logread -e thermal-guard` shows - them all, a remote syslog can filter on the priority. The event log in - `/var/run/thermal-guard/log` keeps the same lines without it. -* **Other sensors** of the board (`extra_sensors`, on by default): every hwmon and - thermal zone temperature besides the processor, such as Wi-Fi chips, ethernet - phys and NVMe drives. The proprietary MediaTek Wi-Fi driver `mt_wifi` exposes - none of those; its radios are read with `iwpriv stat` instead, the - way ImmortalWrt's own overview does it, and only while the interface is up. - These readings are shown only, they never trigger a stage. -* **RRD graphs** through collectd. The package ships an exec plugin feed, enable it - in `/etc/collectd.conf`: - - ``` - LoadPlugin exec - - Exec "nobody:nogroup" "/usr/libexec/thermal-guard-collectd" - - ``` - - Needs `collectd-mod-exec` and `collectd-mod-rrdtool`. It emits - `temperature-soc`, `temperature-modem`, `gauge-fan_step` and `gauge-stage` - under the plugin name `thermal_guard`. - -## Hardware - -Developed against two boards, which is where the two fan modes come from: - -| Board | Fan | Modem | -|---|---|---| -| GL.iNet GL-X3000 (MT7981) | kernel thermal zone, `cooling_device` | Quectel RM520N-GL | -| Banana Pi R3 Mini (MT7986) | sysfs PWM, `pwm` | Fibocom FM350-GL | - -Nothing in the package is board specific, any OpenWrt device with a readable -thermal zone should work. - -## Known limitations - -**ModemManager does not know `at_lock`.** It keeps the modem's AT ports open -and sends its own commands without taking the lock this daemon uses. The daemon -cannot detect that. Where ModemManager manages the modem, read the temperature -another way (`modem_source` `file` or `command`) or set it to `none`; the -section on sharing the modem says what `none` costs. - -**Without `curl`, notifications are limited.** `uclient-fetch` and `wget` cannot -send `notify_header`. The message goes out without it, a webhook that needs it -refuses the message, and it stays in the outbox, retried every two minutes. -Both tools also take the address on their command line, where `/proc` shows it -to every local process while the message goes out. An address with a query -string or user info is refused for that reason and logged once; a secret in the -path of the address cannot be told apart and is sent as it is. - -**A board that reports no temperature gets no processor protection.** When -neither a thermal zone nor a hwmon sensor can be read, the daemon logs `this -device reports no temperature at all, nothing to watch here` once and the -status page says so. No stage can start from the processor side. The modem -thresholds still apply as long as the modem gives a reading. - -**`wifi_off` sees only what netifd manages.** The action runs `wifi down` and -confirms it through `ubus call network.wireless status`. Radios that netifd -does not manage are neither switched by `wifi` nor seen by the check. When -netifd lists no radio, the action fails and is notified, and the daemon warns -once when it loads a configuration that uses `wifi_off`. When netifd does not -answer, the action fails as well. - -**Two cooling devices of the same type cannot be told apart.** The daemon -takes the first one of `fan_cooling_device_type` whose `max_state` is above 0. -If your board has such a setup, please report it. - -## License - -GPL-2.0-only. The escalation logic started out as a device specific script for -the two boards above and was generalised for this package. diff --git a/openwrt-thermal-guard/luci-app-thermal-guard/Makefile b/openwrt-thermal-guard/luci-app-thermal-guard/Makefile deleted file mode 100644 index 1d304247a..000000000 --- a/openwrt-thermal-guard/luci-app-thermal-guard/Makefile +++ /dev/null @@ -1,26 +0,0 @@ -# -# Copyright (C) 2026 SyntaxError NullPointer -# -# This is free software, licensed under the GNU General Public License v2. -# See /LICENSE for more information. -# - -include $(TOPDIR)/rules.mk - -PKG_VERSION:=2.0.0 -PKG_RELEASE:=1 - -PKG_LICENSE:=GPL-2.0-only -PKG_MAINTAINER:=SyntaxError NullPointer <287466119+SyntaxError-NullPointer@users.noreply.github.com> - -LUCI_TITLE:=LuCI support for thermal-guard -LUCI_DESCRIPTION:=Monitor and configuration pages for thermal-guard: temperatures, fan, protection stages and thresholds. -LUCI_DEPENDS:=+luci-base +thermal-guard -LUCI_PKGARCH:=all - -include $(TOPDIR)/feeds/luci/luci.mk - -# No own postinst: luci.mk's default drops the menu caches and reloads rpcd, -# without which the new ACL is not loaded and the pages are refused. - -# call BuildPackage - OpenWrt buildroot signature diff --git a/openwrt-thermal-guard/luci-app-thermal-guard/htdocs/luci-static/resources/view/status/include/11_thermal-guard.js b/openwrt-thermal-guard/luci-app-thermal-guard/htdocs/luci-static/resources/view/status/include/11_thermal-guard.js deleted file mode 100644 index a5818f86b..000000000 --- a/openwrt-thermal-guard/luci-app-thermal-guard/htdocs/luci-static/resources/view/status/include/11_thermal-guard.js +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -// Adds a temperature row to Status -> Overview. Reads the status file the daemon -// writes, so the overview never talks to the modem itself. -'use strict'; -'require baseclass'; -'require fs'; - -var STATUS_FILE = '/var/run/thermal-guard.status'; - -function parseStatus(text) { - var st = {}; - (text || '').split('\n').forEach(function(line) { - var i = line.indexOf('='); - if (i > 0) - st[line.substring(0, i)] = line.substring(i + 1).trim(); - }); - return st; -} - -function num(v) { - var n = parseInt(v, 10); - return isNaN(n) ? null : n; -} - -// Sensor names come straight from the kernel. Translate the ones people actually -// recognise, leave the rest as reported rather than guessing. -function sensorLabel(name) { - var m = name.match(/^(?:mt79\d\d|mtwifi)_phy(\d)/); - if (m) - return _('Wi-Fi %d').format(parseInt(m[1], 10) + 1); - if (name.indexOf('mdio_bus') === 0 || name.indexOf('phy') === 0) - return _('Ethernet'); - if (name === 'nvme') - return _('SSD'); - return name; -} - -function extraSensors(st) { - var out = []; - for (var k in st) { - if (k.indexOf('sensor.') === 0 && st[k] !== '') - out.push([ sensorLabel(k.substring(7)), parseInt(st[k], 10) ]); - } - return out.sort(function(a, b) { return a[0] > b[0] ? 1 : -1; }); -} - -function temp(value, warn, crit) { - if (value === null) - return E('em', {}, _('no reading')); - - var colour = ''; - if (crit !== null && value >= crit) - colour = 'color:#f44336;font-weight:bold'; - else if (warn !== null && value >= warn) - colour = 'color:#ff9800;font-weight:bold'; - - return E('span', { 'style': colour }, '%d °C'.format(value)); -} - -return baseclass.extend({ - title: _('Temperatures'), - - load: function() { - return fs.read(STATUS_FILE).catch(function() { return null; }); - }, - - render: function(text) { - if (text === null) - return null; - - var st = parseStatus(text); - var stage = num(st.stage); - var rows = [ - [ _('Processor'), temp(num(st.cpu_c), num(st.cpu_warn), num(st.cpu_crit)) ] - ]; - - if (st.modem_src || st.modem_c) - rows.push([ _('Modem'), temp(num(st.modem_c), num(st.modem_warn), num(st.modem_crit)) ]); - - extraSensors(st).forEach(function(s) { - rows.push([ s[0], temp(s[1], null, null) ]); - }); - - // fan_view is what can be read, even where fan_mode leaves the fan to - // another controller. Older status files only carry fan_mode. - var fanView = st.fan_view || st.fan_mode; - if (fanView && fanView !== 'none') { - var state = num(st.fan_state), max = num(st.fan_max), rpm = num(st.fan_rpm), - percent = num(st.fan_percent); - var label = (percent !== null) - ? '%d %%'.format(percent) - : (state === null ? '-' - : (max ? _('fan step %d of %d').format(state, max) : String(state))); - if (rpm !== null && rpm > 0) - label += ' (%d rpm)'.format(rpm); - rows.push([ _('Fan'), label ]); - } - - if (stage !== null && stage >= 0) - rows.push([ - _('Thermal Guard'), - E('span', { 'style': 'color:#f44336;font-weight:bold' }, - _('protection stage %d active').format(stage + 1)) - ]); - - return E('table', { 'class': 'table' }, rows.map(function(row) { - return E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td left', 'width': '33%' }, row[0]), - E('td', { 'class': 'td left' }, row[1]) - ]); - })); - } -}); diff --git a/openwrt-thermal-guard/luci-app-thermal-guard/htdocs/luci-static/resources/view/thermal-guard/config.js b/openwrt-thermal-guard/luci-app-thermal-guard/htdocs/luci-static/resources/view/thermal-guard/config.js deleted file mode 100644 index 3fcf4b600..000000000 --- a/openwrt-thermal-guard/luci-app-thermal-guard/htdocs/luci-static/resources/view/thermal-guard/config.js +++ /dev/null @@ -1,431 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -'use strict'; -'require view'; -'require form'; -'require uci'; -'require fs'; -'require ui'; - -var STATUS_FILE = '/var/run/thermal-guard.status'; - -function parseStatus(text) { - var st = {}; - (text || '').split('\n').forEach(function(line) { - var i = line.indexOf('='); - if (i > 0) - st[line.substring(0, i)] = line.substring(i + 1).trim(); - }); - return st; -} - -// The daemon reports why it cannot manage the trips as a fixed code. -function tripReasonText(code) { - switch (code) { - case 'no_cdev': return _('the kernel knows no fan on this device'); - case 'no_zone': return _('no thermal zone drives the fan'); - case 'zone_path': return _('the fan\'s thermal zone is not under /sys/class/thermal'); - case 'no_active_trip': return _('the fan is not bound to any switching temperature'); - case 'not_writable': return _('the switching temperatures cannot be changed'); - default: return code || _('unknown reason'); - } -} - -function tripList(v) { - return (v || '').split('/').filter(function(x) { return x !== ''; }); -} - -// The daemon's defaults, from tg_cfg_default_thresholds. An empty field means -// the daemon uses these, so the order is checked against them as well. -var THRESHOLD_DEFAULTS = { - cpu_warn: 70, cpu_crit: 80, cpu_emergency: 90, modem_warn: 80, modem_crit: 88 -}; - -// The value the daemon will use: the default when empty, NaN when it is not -// a whole number. -function thresholdValue(option, value) { - if (value == null || value === '') - return THRESHOLD_DEFAULTS[option]; - return /^[0-9]+$/.test(value) ? parseInt(value, 10) : NaN; -} - -// Thresholds have to stay ordered, otherwise the service falls back to its -// defaults and the protection silently stops doing what the form says. The -// neighbours are read from the form, not from the saved configuration, so a -// change made next to this one counts. -function checkOrder(section, section_id, option, value, below, above, belowLabel, aboveLabel) { - var v = thresholdValue(option, value); - if (isNaN(v)) - return _('Enter a whole number'); - - if (below) { - var b = thresholdValue(below, section.formvalue(section_id, below)); - if (!isNaN(b) && v <= b) - return _('Must be above %s (%d)').format(belowLabel, b); - } - if (above) { - var a = thresholdValue(above, section.formvalue(section_id, above)); - if (!isNaN(a) && v >= a) - return _('Must be below %s (%d)').format(aboveLabel, a); - } - return true; -} - -// Mirrors the guards in backends.sh. The service rejects anything outside these -// subtrees anyway, so catching it here saves a silent fallback after Apply. -function checkPathPrefix(value, prefixes) { - if (!value) - return true; - if (value.charAt(0) !== '/' || value.indexOf('..') >= 0) - return _('Must be an absolute path without ".."'); - for (var i = 0; i < prefixes.length; i++) - if (value.indexOf(prefixes[i]) === 0) - return true; - return _('Path must start with %s').format(prefixes.join(', ')); -} - -return view.extend({ - load: function() { - return fs.read(STATUS_FILE).catch(function() { return null; }); - }, - - render: function(statusText) { - var m, s, o, - st = parseStatus(statusText), - tripDt = tripList(st.trip_dt), - tripLimit = parseInt(st.trip_limit, 10); - - m = new form.Map('thermal-guard', _('Thermal Guard'), - _('Monitors the temperature of the main processor and the modem. If the device gets too hot, it intervenes in up to three protection stages. Stages stay active until you clear them on the status page.')); - - s = m.section(form.NamedSection, 'main', 'thermal-guard'); - s.addremove = false; - s.tab('general', _('General')); - s.tab('thresholds', _('Thresholds'), - _('Temperatures at which each protection stage starts. What actually happens at stage 2 and 3 is set on the Actions tab.')); - s.tab('fan', _('Fan')); - s.tab('modem', _('Modem')); - s.tab('actions', _('Actions'), - _('What happens when a stage starts. Stage 1 of 3 warns and, if this service drives the fan (Fan tab), sets it to full speed. That cannot be changed.')); - - // ---- general ---- - o = s.taboption('general', form.Flag, 'enabled', _('Enabled'), - _('Switching this off pauses the temperature checks. Protection stages that are already active stay applied and the fan is not handed back, so a broken fan cannot be hidden by a checkbox. Use Clear on the status page for that.')); - o.default = '1'; - o.rmempty = false; - - o = s.taboption('general', form.Value, 'interval', _('Interval'), - _('Seconds between two temperature readings. Values below five seconds are treated as five.')); - o.datatype = 'and(uinteger,min(5))'; - o.placeholder = '20'; - - o = s.taboption('general', form.Value, 'cpu_temp_path', _('Processor temperature source'), - _('System file the processor temperature is read from. The default fits most devices; change it only if your device reports its temperature elsewhere.')); - o.placeholder = '/sys/class/thermal/thermal_zone0/temp'; - o.validate = function(id, v) { - return checkPathPrefix(v, ['/sys/class/thermal/', '/sys/class/hwmon/', '/sys/devices/']); - }; - - // ---- thresholds ---- - o = s.taboption('thresholds', form.Value, 'cpu_warn', _('Processor warning (stage 1 of 3)'), - _('Stage 1 of 3 starts when either temperature reaches its warning level: a warning, and full fan speed if this service drives the fan.')); - o.placeholder = '70'; - o.validate = function(id, v) { - return checkOrder(this.section, id, 'cpu_warn', v, null, 'cpu_crit', null, _('Processor critical')); - }; - - o = s.taboption('thresholds', form.Value, 'cpu_crit', _('Processor critical (stage 2 of 3)')); - o.placeholder = '80'; - o.validate = function(id, v) { - return checkOrder(this.section, id, 'cpu_crit', v, 'cpu_warn', 'cpu_emergency', _('Processor warning'), _('Processor emergency')); - }; - - o = s.taboption('thresholds', form.Value, 'cpu_emergency', _('Processor emergency (stage 3 of 3)')); - o.placeholder = '90'; - o.validate = function(id, v) { - return checkOrder(this.section, id, 'cpu_emergency', v, 'cpu_crit', null, _('Processor critical'), null); - }; - - o = s.taboption('thresholds', form.Value, 'modem_warn', _('Modem warning (stage 1 of 3)')); - o.placeholder = '80'; - o.validate = function(id, v) { - return checkOrder(this.section, id, 'modem_warn', v, null, 'modem_crit', null, _('Modem critical')); - }; - - o = s.taboption('thresholds', form.Value, 'modem_crit', _('Modem critical (stage 2 of 3)')); - o.placeholder = '88'; - o.validate = function(id, v) { - return checkOrder(this.section, id, 'modem_crit', v, 'modem_warn', null, _('Modem warning'), null); - }; - - o = s.taboption('thresholds', form.Value, 'hold_minutes', _('Hold time'), - _('Minutes a stage has to last before the next one starts on time alone: stage 2 of 3 if the temperature has not dropped, stage 3 of 3 if it is still rising.')); - o.datatype = 'uinteger'; - o.placeholder = '3'; - - o = s.taboption('thresholds', form.Value, 'hysteresis', _('Hysteresis'), - _('How many degrees the temperature has to fall below the warning level before the device counts as cooled down.')); - o.datatype = 'uinteger'; - o.placeholder = '10'; - - // ---- fan ---- - o = s.taboption('fan', form.ListValue, 'fan_mode', _('Fan control method'), - _('Leave the fan to the regulator the device already has, unless it has none. Automatic uses the kernel fan control when the device offers it and otherwise leaves the fan alone. It never picks direct control, which needs the fan\'s polarity.')); - o.value('none', _('Leave the fan alone')); - o.value('auto', _('Automatic')); - o.value('cooling_device', _('Kernel fan control')); - o.value('pwm', _('Direct fan control (PWM)')); - o.default = 'none'; - - o = s.taboption('fan', form.Value, 'fan_cooling_device_type', _('Fan identifier'), - _('Name the kernel uses for the fan. Change it only if the fan is not found automatically.')); - o.placeholder = 'pwm-fan'; - o.depends('fan_mode', 'auto'); - o.depends('fan_mode', 'cooling_device'); - - o = s.taboption('fan', form.ListValue, 'fan_governor', _('Control mode to restore'), - _('Control mode handed back to the kernel when the stages are cleared and no earlier control mode was recorded. The default fits almost every device.')); - // The daemon's list, kernel names and not translated. Not user_space: - // nothing would regulate the fan after a reset. - [ 'step_wise', 'fair_share', 'bang_bang', 'power_allocator' ].forEach(function(g) { - o.value(g); - }); - o.default = 'step_wise'; - o.depends('fan_mode', 'auto'); - o.depends('fan_mode', 'cooling_device'); - - o = s.taboption('fan', form.Value, 'pwm_path', _('Fan speed file'), - _('Only needed for direct fan control.')); - o.placeholder = '/sys/class/hwmon/hwmon0/pwm1'; - o.depends('fan_mode', 'pwm'); - o.validate = function(id, v) { return checkPathPrefix(v, ['/sys/class/hwmon/', '/sys/devices/']); }; - - o = s.taboption('fan', form.Value, 'pwm_enable_path', _('Fan control file'), - _('Only needed for direct fan control. Switches the fan chip to manual control.')); - o.placeholder = '/sys/class/hwmon/hwmon0/pwm1_enable'; - o.depends('fan_mode', 'pwm'); - o.validate = function(id, v) { return checkPathPrefix(v, ['/sys/class/hwmon/', '/sys/devices/']); }; - - o = s.taboption('fan', form.Value, 'pwm_full', _('Value for full speed'), - _('Required for direct fan control. The raw value that makes this fan spin fastest. Some boards wire the signal inverted, where 1 is full speed and 255 stops the fan. Without this value and the slowest one, the fan is left alone.')); - o.datatype = 'uinteger'; - o.depends('fan_mode', 'pwm'); - o.rmempty = false; - - o = s.taboption('fan', form.Value, 'pwm_idle', _('Value for the slowest setting'), - _('Required for direct fan control. The opposite end of the same scale. Together with full speed it states the polarity, and it shows the fan speed as a percentage. Never written to the hardware.')); - o.datatype = 'uinteger'; - o.depends('fan_mode', 'pwm'); - o.rmempty = false; - - // ---- switching temperatures of the kernel fan control ---- - o = s.taboption('fan', form.DummyValue, '_trip_state', _('Kernel fan control')); - o.cfgvalue = function() { - switch (st.trip_state) { - case 'active': - return _('Managed by this service, switching temperatures now %s °C').format(tripList(st.trip_now).join(', ')); - case 'inactive': - return _('Not possible on this device: %s').format(tripReasonText(st.trip_reason)); - default: - return _('Not managed by this service'); - } - }; - - o = s.taboption('fan', form.ListValue, 'trip_boost', _('Speed the fan up for the modem'), - _('Where the kernel drives the fan, its switching temperatures are lowered while the modem runs hotter than the processor, so the fan speeds up for the modem as well. They only ever go down, never above the base values. A missing modem reading keeps them where they are.')); - o.value('off', _('Off')); - o.value('modem', _('Follow the modem temperature')); - o.default = 'off'; - - o = s.taboption('fan', form.Value, 'trip_boost_offset', _('Modem allowance'), - _('Degrees the modem may run above the processor before the switching temperatures are lowered.')); - o.datatype = 'range(0,40)'; - o.placeholder = '15'; - o.depends('trip_boost', 'modem'); - - o = s.taboption('fan', form.DynamicList, 'trip_active', _('Fan switching temperatures'), - tripDt.length - ? _('Base values in degrees at which the kernel moves the fan one step up, lowest first, one value per step. Empty uses the values of the installed firmware: %s °C.').format(tripDt.join(', ')) - : _('Base values in degrees at which the kernel moves the fan one step up, lowest first, one value per step. Empty uses the values of the installed firmware.')); - // Same rules as the service, which falls back to the firmware values - // on anything it rejects. - o.validate = function(section_id, value) { - var list = L.toArray(this.formvalue(section_id)), prev = null; - for (var i = 0; i < list.length; i++) { - var raw = String(list[i]).trim(), v = parseInt(raw, 10); - if (isNaN(v) || String(v) !== raw) - return _('Enter a whole number'); - if (v < 20 || v > 95) - return _('Must be between 20 and 95'); - if (prev !== null && v <= prev) - return _('Each value must be higher than the one before'); - if (!isNaN(tripLimit) && v >= tripLimit) - return _('Must be below %d, the temperature at which the kernel itself intervenes').format(tripLimit); - prev = v; - } - if (list.length && tripDt.length && list.length !== tripDt.length) - return _('Needs exactly %d values, one per fan step').format(tripDt.length); - return true; - }; - - if (st.trip_dt_changed === '1' && tripDt.length) { - o = s.taboption('fan', form.DummyValue, '_trip_dt_hint', _('New firmware values')); - o.cfgvalue = function() { - return _('The installed firmware now uses %s °C. Your own base values were set for an earlier firmware and still apply.').format(tripDt.join(', ')); - }; - - // The daemon keeps the firmware values trip_active was made for in - // a file of its own; trips-keep records the ones of this boot. - // Resolves to true when that worked. - var keepTrips = function() { - var failed = function(why) { - ui.addNotification(null, E('p', {}, _('Recording the firmware values failed: %s').format(why)), 'error'); - return false; - }; - return fs.exec('/usr/sbin/thermal-guard', [ 'trips-keep' ]).then(function(res) { - if (res.code !== 0) - return failed(res.stderr || res.stdout || res.code); - ui.addNotification(null, E('p', {}, _('Firmware values recorded. The notice goes away with the next check.')), 'info'); - return true; - }).catch(failed); - }; - - o = s.taboption('fan', form.Button, '_trip_dt_adopt', ' '); - o.inputtitle = _('Use the firmware values'); - // Parse the form first, then set the values, otherwise the form - // writes the old list back over them. Apply reloads the page, so - // it comes last. - o.onclick = function() { - return m.save(function() { - uci.set('thermal-guard', 'main', 'trip_active', tripDt); - }).then(keepTrips).then(function(ok) { - if (ok) - return ui.changes.apply(true); - }); - }; - - o = s.taboption('fan', form.Button, '_trip_dt_keep', ' '); - o.inputtitle = _('Keep my values'); - o.onclick = keepTrips; - } - - if (st.fan_levels) { - o = s.taboption('fan', form.DummyValue, '_fan_levels', _('Fan steps of the firmware'), - _('Raw value per fan step from the device tree, with the duty cycle of the signal. They belong to the fan that is fitted and change only with a new firmware image.')); - o.cfgvalue = function() { - return st.fan_levels.split(' ').map(function(v, i) { - return _('step %d: %d (%d %%)').format(i, parseInt(v, 10), Math.round(parseInt(v, 10) * 100 / 255)); - }).join(', '); - }; - } - - // ---- modem ---- - o = s.taboption('modem', form.ListValue, 'modem_source', _('Modem temperature source'), - _('Automatic tries the known modem commands in turn. Select your manufacturer if detection fails.')); - o.value('auto', _('Automatic')); - o.value('quectel', _('Quectel (AT+QTEMP)')); - o.value('fibocom', _('Fibocom (AT+GTSENRDTEMP)')); - o.value('file', _('File')); - o.value('command', _('Command')); - o.value('none', _('No modem')); - o.default = 'auto'; - - o = s.taboption('modem', form.Value, 'modem_at_port', _('Modem control port'), - _('Serial port the commands are sent to. Automatic checks the usual ports.')); - o.placeholder = 'auto'; - o.depends('modem_source', 'auto'); - o.depends('modem_source', 'quectel'); - o.depends('modem_source', 'fibocom'); - o.validate = function(id, v) { - if (v === 'auto') - return true; - return checkPathPrefix(v, ['/dev/']); - }; - - o = s.taboption('modem', form.Value, 'at_lock', _('Lock file for modem access'), - _('Prevents this service and other programs from sending commands to the modem at the same time. Point those programs at the same file.')); - o.placeholder = '/var/lock/modem-at.lock'; - o.depends('modem_source', 'auto'); - o.depends('modem_source', 'quectel'); - o.depends('modem_source', 'fibocom'); - o.validate = function(id, v) { return checkPathPrefix(v, ['/var/lock/', '/var/run/', '/tmp/']); }; - - o = s.taboption('modem', form.Value, 'modem_temp_file', _('Temperature file')); - o.depends('modem_source', 'file'); - o.validate = function(id, v) { return checkPathPrefix(v, ['/var/run/', '/tmp/', '/sys/']); }; - - o = s.taboption('modem', form.DummyValue, '_modem_temp_hook', _('Own temperature source'), - _('Reads /etc/thermal-guard/hooks/modem-temp, which has to print the temperature in degrees as a whole number. Place the file over SSH; it runs as root and is deliberately not editable from here.')); - o.depends('modem_source', 'command'); - - // ---- actions ---- - o = s.taboption('actions', form.ListValue, 'stage1_action', _('Action at stage 2 of 3')); - o.value('modem_radio_off', _('Switch the modem radio off')); - o.value('wifi_off', _('Switch Wi-Fi off')); - o.value('interface_down', _('Take a network interface down')); - o.value('command', _('Run own script')); - o.value('none', _('Nothing')); - o.default = 'modem_radio_off'; - - o = s.taboption('actions', form.DummyValue, '_stage1_hook', _('Script at stage 2 of 3'), - _('Runs /etc/thermal-guard/hooks/stage1. Place the file over SSH and make it executable.')); - o.depends('stage1_action', 'command'); - - o = s.taboption('actions', form.ListValue, 'stage2_action', _('Action at stage 3 of 3')); - o.value('wifi_off', _('Switch Wi-Fi off')); - o.value('modem_radio_off', _('Switch the modem radio off')); - o.value('interface_down', _('Take a network interface down')); - o.value('command', _('Run own script')); - o.value('none', _('Nothing')); - o.default = 'wifi_off'; - - o = s.taboption('actions', form.DummyValue, '_stage2_hook', _('Script at stage 3 of 3'), - _('Runs /etc/thermal-guard/hooks/stage2. Place the file over SSH and make it executable.')); - o.depends('stage2_action', 'command'); - - o = s.taboption('actions', form.ListValue, 'modem_radio_off_at', _('How the modem is switched off'), - _('Flight mode suits most modems. Use minimum function for a modem that does not accept flight mode. Either way the modem is asked afterwards (AT+CFUN?), and a modem that does not confirm counts as a failed stage.')); - o.value('AT+CFUN=4', _('Flight mode (AT+CFUN=4)')); - o.value('AT+CFUN=0', _('Minimum function (AT+CFUN=0)')); - o.default = 'AT+CFUN=4'; - o.depends('stage1_action', 'modem_radio_off'); - o.depends('stage2_action', 'modem_radio_off'); - - o = s.taboption('actions', form.Value, 'action_interface', _('Interface to take down'), - _('Network interface taken down by the interface action and brought back up when the stages are cleared. Empty uses the modem network interface.')); - o.datatype = 'uciname'; - o.depends('stage1_action', 'interface_down'); - o.depends('stage2_action', 'interface_down'); - - o = s.taboption('actions', form.Value, 'notify_url', _('Notification address'), - _('The warning text is posted here. Works with ntfy, Gotify, Matrix, Slack and anything else that accepts a plain POST body.')); - o.placeholder = 'https://ntfy.sh/my-topic'; - // Anchored at both ends, and quotes, backslashes and control characters - // are refused: the service writes this into a curl config file, where - // any of them would turn the rest of the value into directives. - o.validate = function(id, v) { - if (!v) - return true; - if (!/^https?:\/\/[\x21-\x7e]+$/.test(v) || /["\\]/.test(v)) - return _('Must be an http:// or https:// address without quotes or spaces'); - return true; - }; - - o = s.taboption('actions', form.Value, 'notify_header', _('Additional header'), - _('Sent with the notification, for example an authorisation token. Needs curl installed; the smaller uclient-fetch cannot send headers.')); - o.placeholder = 'Authorization: Bearer ...'; - o.password = true; - o.validate = function(id, v) { - if (!v) - return true; - if (!/^[\x20-\x7e]+$/.test(v) || /["\\]/.test(v)) - return _('Must be printable text without quotes or backslashes'); - return true; - }; - - o = s.taboption('actions', form.Value, 'uplink_interface', _('Modem network interface'), - _('Lets the service tell whether another internet connection exists, so a message from stage 2 of 3 on goes out or waits in the queue. Also the interface the interface action takes when none is set.')); - o.datatype = 'uciname'; - - return m.render(); - } -}); diff --git a/openwrt-thermal-guard/luci-app-thermal-guard/htdocs/luci-static/resources/view/thermal-guard/monitor.js b/openwrt-thermal-guard/luci-app-thermal-guard/htdocs/luci-static/resources/view/thermal-guard/monitor.js deleted file mode 100644 index be98eb476..000000000 --- a/openwrt-thermal-guard/luci-app-thermal-guard/htdocs/luci-static/resources/view/thermal-guard/monitor.js +++ /dev/null @@ -1,298 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -'use strict'; -'require view'; -'require poll'; -'require dom'; -'require fs'; -'require ui'; - -var STATUS_FILE = '/var/run/thermal-guard.status'; - -function parseStatus(text) { - var st = {}; - (text || '').split('\n').forEach(function(line) { - var i = line.indexOf('='); - if (i > 0) - st[line.substring(0, i)] = line.substring(i + 1).trim(); - }); - return st; -} - -function num(v) { - var n = parseInt(v, 10); - return isNaN(n) ? null : n; -} - -// Colour follows the configured thresholds, not a fixed scale, so the bar means -// the same thing on a passively cooled box and on one with a fan. -function tempClass(value, warn, crit) { - if (value === null) - return ''; - if (crit !== null && value >= crit) - return 'tg-crit'; - if (warn !== null && value >= warn) - return 'tg-warn'; - return 'tg-ok'; -} - -function gauge(value, warn, crit, max) { - var pct = (value === null) ? 0 : Math.max(0, Math.min(100, Math.round(value * 100 / max))); - return E('div', { 'class': 'tg-gauge' }, [ - E('div', { - 'class': 'tg-fill ' + tempClass(value, warn, crit), - 'style': 'width:%d%%'.format(pct) - }) - ]); -} - -// Sensor names come straight from the kernel. Translate the ones people actually -// recognise, leave the rest as reported rather than guessing. -function sensorLabel(name) { - var m = name.match(/^(?:mt79\d\d|mtwifi)_phy(\d)/); - if (m) - return _('Wi-Fi %d').format(parseInt(m[1], 10) + 1); - if (name.indexOf('mdio_bus') === 0 || name.indexOf('phy') === 0) - return _('Ethernet'); - if (name === 'nvme') - return _('SSD'); - return name; -} - -function extraSensors(st) { - var out = []; - for (var k in st) { - if (k.indexOf('sensor.') === 0 && st[k] !== '') - out.push([ sensorLabel(k.substring(7)), parseInt(st[k], 10) ]); - } - return out.sort(function(a, b) { return a[0] > b[0] ? 1 : -1; }); -} - -function row(label, valueNode, gaugeNode) { - return E('div', { 'class': 'tg-row' }, [ - E('div', { 'class': 'tg-label' }, label), - gaugeNode || E('div', { 'class': 'tg-gauge tg-empty' }, []), - E('div', { 'class': 'tg-value' }, valueNode) - ]); -} - -// Stage 2 and 3 do whatever the Actions tab says, so the banner has to read the -// configured action instead of naming the default. -function actionLabel(action) { - switch (action) { - case 'modem_radio_off': return _('modem radio off'); - case 'wifi_off': return _('Wi-Fi off'); - case 'interface_down': return _('network interface down'); - case 'command': return _('custom command'); - case 'none': return _('no action'); - default: return action || _('unknown action'); - } -} - -function stageText(stage, st) { - switch (stage) { - case 0: return st.fan_mode === 'none' - ? _('Stage 1 of 3 active: warning, fan left to its own controller') - : _('Stage 1 of 3 active: fan at full speed'); - case 1: return _('Stage 2 of 3 active: %s').format(actionLabel(st.stage1_action)); - case 2: return _('Stage 3 of 3 active: %s').format(actionLabel(st.stage2_action)); - default: return _('No intervention, temperatures within range'); - } -} - -// Why the page might be showing something other than a live reading. The daemon -// stamps every write with "now": a file that stopped being written means the -// service is gone, while a fresh file holding an old reading means it is -// running but not measuring. -function serviceState(st) { - var written = parseInt(st.now, 10), - interval = parseInt(st.interval, 10) || 30, - age = written ? Math.floor(Date.now() / 1000) - written : null; - - // Three missed writes, with a floor so a short interval is not jumpy. - if (age !== null && age > Math.max(90, interval * 3)) - return 'stopped'; - if (st.enabled === '0') - return 'paused'; - return 'running'; -} - -function fmtAge(ts) { - var n = parseInt(ts, 10); - if (!n) - return '-'; - var age = Math.max(0, Math.floor(Date.now() / 1000) - n); - if (age < 90) - return _('%d s ago').format(age); - return _('%d min ago').format(Math.round(age / 60)); -} - -return view.extend({ - load: function() { - return fs.read(STATUS_FILE).catch(function() { return null; }); - }, - - render: function(text) { - var style = E('style', {}, [ - '.tg-row{display:flex;align-items:center;gap:.6em;margin:.35em 0}', - '.tg-label{flex:0 0 12em}', - '.tg-value{flex:0 0 7em;text-align:right;font-weight:bold}', - '.tg-gauge{flex:1 1 auto;height:1.1em;background:rgba(128,128,128,.25);border-radius:.55em;overflow:hidden;min-width:6em}', - '.tg-gauge.tg-empty{background:transparent}', - '.tg-fill{height:100%;border-radius:.55em;transition:width .4s}', - '.tg-ok{background:#4caf50}.tg-warn{background:#ff9800}.tg-crit{background:#f44336}', - '.tg-banner{padding:.6em .8em;border-radius:.3em;margin-bottom:1em}', - '.tg-banner.tg-active{background:rgba(244,67,54,.15);border-left:4px solid #f44336}', - '.tg-banner.tg-idle{background:rgba(76,175,80,.12);border-left:4px solid #4caf50}', - '.tg-banner.tg-warn-banner{background:rgba(255,152,0,.15);border-left:4px solid #ff9800}', - '.tg-meta{opacity:.7;font-size:90%;margin-top:.8em}' - ].join('\n')); - - var body = E('div', { 'id': 'tg-body' }, []); - var container = E('div', { 'class': 'cbi-map' }, [ - style, - E('h2', {}, _('Thermal Guard')), - E('div', { 'class': 'cbi-map-descr' }, - _('Current temperatures and protection state. Thresholds and actions are configured under System, Thermal Guard.')), - body - ]); - - this.update(body, text); - - poll.add(L.bind(function() { - return fs.read(STATUS_FILE) - .catch(function() { return null; }) - .then(L.bind(function(t) { this.update(body, t); }, this)); - }, this), 5); - - return container; - }, - - update: function(body, text) { - if (text === null) { - dom.content(body, E('div', { 'class': 'alert-message warning' }, - _('No data yet. Check whether the Thermal Guard service is running.'))); - return; - } - - var st = parseStatus(text); - var cpu = num(st.cpu_c), modem = num(st.modem_c), stage = num(st.stage); - var cpuWarn = num(st.cpu_warn), cpuCrit = num(st.cpu_crit); - var modemWarn = num(st.modem_warn), modemCrit = num(st.modem_crit); - var fanState = num(st.fan_state), fanMax = num(st.fan_max), rpm = num(st.fan_rpm); - - var rows = [ - row(_('Processor temperature'), - cpu === null ? '-' : '%d °C'.format(cpu), - gauge(cpu, cpuWarn, cpuCrit, num(st.cpu_emergency) || 100)) - ]; - - if (st.modem_src !== '' || modem !== null) - rows.push(row(_('Modem temperature'), - modem === null ? _('no reading') : '%d °C'.format(modem), - gauge(modem, modemWarn, modemCrit, (num(st.modem_crit) || 90) + 10))); - - extraSensors(st).forEach(function(s) { - rows.push(row(s[0], '%d °C'.format(s[1]), gauge(s[1], null, null, 100))); - }); - - // fan_view is what can be read, even where fan_mode leaves the fan to - // another controller. Older status files only carry fan_mode. - var fanView = st.fan_view || st.fan_mode; - if (fanView && fanView !== 'none') { - // The daemon computes the percentage because only it knows which way - // the raw scale runs; an inverted board reports full speed as 0. - var fanPercent = num(st.fan_percent); - var fanLabel = (fanPercent !== null) - ? '%d %%'.format(fanPercent) - : (fanState === null ? '-' - : (fanMax ? _('fan step %d of %d').format(fanState, fanMax) : String(fanState))); - if (rpm !== null && rpm > 0) - fanLabel += ' (%d rpm)'.format(rpm); - rows.push(row(_('Fan'), fanLabel, - (fanPercent !== null) ? gauge(fanPercent, null, null, 100) - : ((fanState !== null && fanMax) ? gauge(fanState, null, null, fanMax) : null))); - - if (st.fan_foreign === '1') - rows.push(row(_('Fan control'), - _('Another service is controlling this fan'), null)); - } - - // Only shown where this service manages the kernel's switching - // temperatures; elsewhere they are none of its business. - if (st.trip_state === 'active') { - var tripDelta = num(st.trip_delta); - rows.push(row(_('Fan switching temperatures'), - (st.trip_now || '-').replace(/\//g, ' / ') + ' °C', null)); - if (tripDelta) - rows.push(row('', _('lowered by %d K for the modem').format(tripDelta), null)); - } - - var active = (stage !== null && stage >= 0), - state = serviceState(st), - noSensor = (st.no_sensor === '1'), - banner = [ E('strong', {}, noSensor - ? _('This device does not report any temperature') - : stageText(stage, st)) ]; - - // Not a fault to chase: some boards expose no thermal zone and no hwmon - // temperature input at all. Saying so beats a page full of dashes. - if (noSensor) - banner.push(E('div', {}, - _('No thermal sensor was found, so the processor is not watched on this hardware. The modem thresholds still apply while the modem reports a temperature.'))); - - // A stage that is still applied while nothing is watching is the one - // thing an operator has to be told about, so it comes before the rest. - if (state !== 'running' && !noSensor) - banner.push(E('div', {}, state === 'stopped' - ? (active - ? _('The service is not running. Protection stages stay applied and the fan is not handed back.') - : _('The service is not running. Temperatures are not being watched.')) - : (active - ? _('The service is paused. Protection stages stay applied and the fan is not handed back.') - : _('The service is paused. Temperatures are not being watched.')))); - - if (st.trip_dt_changed === '1') - banner.push(E('div', {}, - _('The firmware brings new fan switching temperatures (%s °C). Your own values still apply; review them under System, Thermal Guard, Fan.').format((st.trip_dt || '').replace(/\//g, ', ')))); - - if (active) - banner.push(E('div', {}, [ - E('div', {}, _('Protection stages stay active until you clear them. Clear them once the cause is fixed, for example a replaced fan.')), - E('button', { - 'class': 'btn cbi-button cbi-button-reset', - 'style': 'margin-top:.5em', - 'click': ui.createHandlerFn(this, 'handleClear') - }, _('Clear protection stages')) - ])); - - dom.content(body, [ - E('div', { - 'class': 'tg-banner ' + ((state !== 'running' || noSensor) ? 'tg-warn-banner' - : (active ? 'tg-active' : 'tg-idle')) - }, banner), - E('div', {}, rows), - E('div', { 'class': 'tg-meta' }, [ - _('Last reading: %s').format(fmtAge(st.ts)), - st.modem_src ? ' · ' + _('modem source: %s').format(st.modem_src) : '', - st.fan_mode ? ' · ' + _('fan mode: %s').format(st.fan_mode) : '', - st.version ? ' · ' + _('version %s').format(st.version) : '' - ].join('')) - ]); - }, - - // Runs "thermal-guard reset": hands the fan back, radio on, stage cleared. - handleClear: function() { - return fs.exec('/usr/sbin/thermal-guard', [ 'reset' ]).then(function(res) { - if (res.code !== 0) - ui.addNotification(null, E('p', {}, _('Clearing failed: %s').format(res.stderr || res.stdout || res.code)), 'error'); - else - ui.addNotification(null, E('p', {}, _('Protection stages cleared.')), 'info'); - }).catch(function(err) { - ui.addNotification(null, E('p', {}, _('Clearing failed: %s').format(err)), 'error'); - }); - }, - - handleSave: null, - handleSaveApply: null, - handleReset: null -}); diff --git a/openwrt-thermal-guard/luci-app-thermal-guard/po/de/thermal-guard.po b/openwrt-thermal-guard/luci-app-thermal-guard/po/de/thermal-guard.po deleted file mode 100644 index 6f7da04dc..000000000 --- a/openwrt-thermal-guard/luci-app-thermal-guard/po/de/thermal-guard.po +++ /dev/null @@ -1,509 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: luci-app-thermal-guard\n" -"PO-Revision-Date: 2026-09-22\n" -"Last-Translator: SyntaxError NullPointer <287466119+SyntaxError-NullPointer@users.noreply.github.com>\n" -"Language-Team: German\n" -"Language: de\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgid "Enter a whole number" -msgstr "Ganze Zahl eingeben" - -msgid "Must be above %s (%d)" -msgstr "Muss über %s (%d) liegen" - -msgid "Must be below %s (%d)" -msgstr "Muss unter %s (%d) liegen" - -msgid "Must be an absolute path without \"..\"" -msgstr "Muss ein vollständiger Pfad ohne „..\" sein" - -msgid "Path must start with %s" -msgstr "Pfad muss mit %s beginnen" - -msgid "Thermal Guard" -msgstr "Thermal Guard" - -msgid "Monitors the temperature of the main processor and the modem. If the device gets too hot, it intervenes in up to three protection stages. Stages stay active until you clear them on the status page." -msgstr "Überwacht die Temperatur des Hauptprozessors und des Modems. Wird das Gerät zu heiß, greift der Dienst in bis zu drei Schutzstufen ein. Die Stufen bleiben aktiv, bis Sie sie auf der Statusseite zurücksetzen." - -msgid "General" -msgstr "Allgemein" - -msgid "Thresholds" -msgstr "Schwellen" - -msgid "Temperatures at which each protection stage starts. What actually happens at stage 2 and 3 is set on the Actions tab." -msgstr "Temperaturen, ab denen die jeweilige Schutzstufe einsetzt. Was bei Stufe 2 und 3 geschieht, legen Sie im Reiter „Aktionen\" fest." - -msgid "Fan" -msgstr "Lüfter" - -msgid "Modem" -msgstr "Modem" - -msgid "Actions" -msgstr "Aktionen" - -msgid "What happens when a stage starts. Stage 1 of 3 warns and, if this service drives the fan (Fan tab), sets it to full speed. That cannot be changed." -msgstr "Was beim Einsetzen einer Stufe geschieht. Stufe 1 von 3 warnt und setzt den Lüfter auf volle Drehzahl, sofern dieser Dienst ihn ansteuert (Reiter „Lüfter\"). Das ist nicht änderbar." - -msgid "Enabled" -msgstr "Aktiv" - -msgid "Switching this off pauses the temperature checks. Protection stages that are already active stay applied and the fan is not handed back, so a broken fan cannot be hidden by a checkbox. Use Clear on the status page for that." -msgstr "Durch Deaktivieren werden die Temperaturmessungen angehalten. Bereits aktive Schutzstufen bleiben bestehen und der Lüfter wird nicht freigegeben. Ein defekter Lüfter lässt sich dadurch nicht verbergen. Nutzen Sie dafür die Funktion „Schutzstufen zurücksetzen\" auf der Statusseite." - -msgid "Interval" -msgstr "Intervall" - -msgid "Seconds between two temperature readings. Values below five seconds are treated as five." -msgstr "Sekunden zwischen zwei Temperaturmessungen. Werte unter fünf Sekunden werden als fünf behandelt." - -msgid "Processor temperature source" -msgstr "Quelle der Prozessortemperatur" - -msgid "System file the processor temperature is read from. The default fits most devices; change it only if your device reports its temperature elsewhere." -msgstr "Systemdatei, aus der die Prozessortemperatur gelesen wird. Die Vorgabe passt für die meisten Geräte und muss nur geändert werden, wenn Ihr Gerät die Temperatur an anderer Stelle meldet." - -msgid "Processor warning (stage 1 of 3)" -msgstr "Prozessor Warnung (Stufe 1 von 3)" - -msgid "Stage 1 of 3 starts when either temperature reaches its warning level: a warning, and full fan speed if this service drives the fan." -msgstr "Stufe 1 von 3 setzt ein, sobald eine der beiden Temperaturen ihre Warnschwelle erreicht: eine Warnung und, sofern dieser Dienst den Lüfter ansteuert, volle Drehzahl." - -msgid "Processor critical" -msgstr "Prozessor kritisch" - -msgid "Processor critical (stage 2 of 3)" -msgstr "Prozessor kritisch (Stufe 2 von 3)" - -msgid "Processor warning" -msgstr "Prozessor Warnung" - -msgid "Processor emergency" -msgstr "Prozessor Notfall" - -msgid "Processor emergency (stage 3 of 3)" -msgstr "Prozessor Notfall (Stufe 3 von 3)" - -msgid "Modem warning (stage 1 of 3)" -msgstr "Modem Warnung (Stufe 1 von 3)" - -msgid "Modem critical" -msgstr "Modem kritisch" - -msgid "Modem critical (stage 2 of 3)" -msgstr "Modem kritisch (Stufe 2 von 3)" - -msgid "Modem warning" -msgstr "Modem Warnung" - -msgid "Hold time" -msgstr "Haltezeit" - -msgid "Minutes a stage has to last before the next one starts on time alone: stage 2 of 3 if the temperature has not dropped, stage 3 of 3 if it is still rising." -msgstr "Minuten, die eine Stufe andauern muss, bevor allein nach Zeit die nächste einsetzt: Stufe 2 von 3, wenn die Temperatur nicht gesunken ist, Stufe 3 von 3, wenn sie weiter steigt." - -msgid "Hysteresis" -msgstr "Hysterese" - -msgid "How many degrees the temperature has to fall below the warning level before the device counts as cooled down." -msgstr "Um wie viel Grad die Temperatur unter die Warnschwelle fallen muss, damit das Gerät als abgekühlt gilt." - -msgid "Fan control method" -msgstr "Art der Lüftersteuerung" - -msgid "Leave the fan to the regulator the device already has, unless it has none. Automatic uses the kernel fan control when the device offers it and otherwise leaves the fan alone. It never picks direct control, which needs the fan's polarity." -msgstr "Überlassen Sie den Lüfter der Regelung, die das Gerät bereits hat, sofern es eine gibt. „Automatisch\" nutzt die Kernel-Lüfterregelung, wenn das Gerät sie anbietet, und lässt den Lüfter sonst unangetastet. Die direkte Steuerung wählt es nie, denn dafür muss die Polarität des Lüfters bekannt sein." - -msgid "Automatic" -msgstr "Automatisch" - -msgid "Kernel fan control" -msgstr "Kernel-Lüfterregelung" - -msgid "Direct fan control (PWM)" -msgstr "Direkte Lüftersteuerung (PWM)" - -msgid "Leave the fan alone" -msgstr "Lüfter nicht ansteuern" - -msgid "Fan identifier" -msgstr "Kennung des Lüfters" - -msgid "Name the kernel uses for the fan. Change it only if the fan is not found automatically." -msgstr "Name, unter dem der Kernel den Lüfter führt. Nur ändern, wenn der Lüfter nicht automatisch gefunden wird." - -msgid "Control mode to restore" -msgstr "Wiederherzustellendes Regelverfahren" - -msgid "Control mode handed back to the kernel when the stages are cleared and no earlier control mode was recorded. The default fits almost every device." -msgstr "Regelverfahren, das beim Zurücksetzen der Schutzstufen an den Kernel übergeben wird, wenn kein früheres festgehalten wurde. Die Vorgabe passt für nahezu alle Geräte." - -msgid "Fan speed file" -msgstr "Datei für die Lüfterdrehzahl" - -msgid "Only needed for direct fan control." -msgstr "Nur bei direkter Lüftersteuerung nötig." - -msgid "Fan control file" -msgstr "Datei zum Aktivieren der Steuerung" - -msgid "Only needed for direct fan control. Switches the fan chip to manual control." -msgstr "Nur bei direkter Lüftersteuerung nötig. Schaltet den Lüfterbaustein auf manuelle Steuerung um." - -msgid "Value for full speed" -msgstr "Wert für volle Drehzahl" - -msgid "Required for direct fan control. The raw value that makes this fan spin fastest. Some boards wire the signal inverted, where 1 is full speed and 255 stops the fan. Without this value and the slowest one, the fan is left alone." -msgstr "Erforderlich für die direkte Lüftersteuerung. Der Rohwert, bei dem der Lüfter am schnellsten dreht. Auf manchen Platinen ist das Signal invertiert: 1 bedeutet dann volle Drehzahl und 255 Stillstand. Fehlt dieser Wert oder der für die langsamste Stufe, bleibt der Lüfter unangetastet." - -msgid "Value for the slowest setting" -msgstr "Wert für die niedrigste Drehzahl" - -msgid "Required for direct fan control. The opposite end of the same scale. Together with full speed it states the polarity, and it shows the fan speed as a percentage. Never written to the hardware." -msgstr "Erforderlich für die direkte Lüftersteuerung. Der Gegenwert am anderen Ende derselben Skala. Zusammen mit dem Wert für volle Drehzahl legt er die Polarität fest und dient zur Anzeige der Drehzahl in Prozent. Wird nie an die Hardware geschrieben." - -msgid "Modem temperature source" -msgstr "Quelle der Modemtemperatur" - -msgid "Automatic tries the known modem commands in turn. Select your manufacturer if detection fails." -msgstr "„Automatisch\" probiert die bekannten Modem-Kommandos nacheinander. Wählen Sie Ihren Hersteller, falls die Erkennung fehlschlägt." - -msgid "Quectel (AT+QTEMP)" -msgstr "Quectel (AT+QTEMP)" - -msgid "Fibocom (AT+GTSENRDTEMP)" -msgstr "Fibocom (AT+GTSENRDTEMP)" - -msgid "File" -msgstr "Datei" - -msgid "Command" -msgstr "Kommando" - -msgid "No modem" -msgstr "Kein Modem" - -msgid "Modem control port" -msgstr "Steuerport des Modems" - -msgid "Serial port the commands are sent to. Automatic checks the usual ports." -msgstr "Serieller Anschluss, über den die Kommandos an das Modem gehen. „Automatisch\" sucht die üblichen Anschlüsse ab." - -msgid "Lock file for modem access" -msgstr "Sperrdatei für Modemzugriffe" - -msgid "Prevents this service and other programs from sending commands to the modem at the same time. Point those programs at the same file." -msgstr "Verhindert, dass dieser Dienst und andere Programme gleichzeitig Kommandos an das Modem senden. Richten Sie diese Programme auf dieselbe Datei aus." - -msgid "Temperature file" -msgstr "Temperaturdatei" - -msgid "Own temperature source" -msgstr "Eigene Temperaturquelle" - -msgid "Reads /etc/thermal-guard/hooks/modem-temp, which has to print the temperature in degrees as a whole number. Place the file over SSH; it runs as root and is deliberately not editable from here." -msgstr "Liest /etc/thermal-guard/hooks/modem-temp aus. Das Skript muss die Temperatur in Grad als ganze Zahl ausgeben. Legen Sie die Datei per SSH an. Sie wird als root ausgeführt und ist bewusst nicht über diese Oberfläche bearbeitbar." - -msgid "Action at stage 2 of 3" -msgstr "Aktion bei Stufe 2 von 3" - -msgid "Switch the modem radio off" -msgstr "Mobilfunk abschalten" - -msgid "Switch Wi-Fi off" -msgstr "WLAN abschalten" - -msgid "Take a network interface down" -msgstr "Netzwerkschnittstelle herunterfahren" - -msgid "Run own script" -msgstr "Eigenes Skript ausführen" - -msgid "Nothing" -msgstr "Nichts" - -msgid "Script at stage 2 of 3" -msgstr "Skript bei Stufe 2 von 3" - -msgid "Runs /etc/thermal-guard/hooks/stage1. Place the file over SSH and make it executable." -msgstr "Führt /etc/thermal-guard/hooks/stage1 aus. Legen Sie die Datei per SSH an und machen Sie sie ausführbar." - -msgid "Action at stage 3 of 3" -msgstr "Aktion bei Stufe 3 von 3" - -msgid "Script at stage 3 of 3" -msgstr "Skript bei Stufe 3 von 3" - -msgid "How the modem is switched off" -msgstr "Art der Modem-Abschaltung" - -msgid "Flight mode suits most modems. Use minimum function for a modem that does not accept flight mode. Either way the modem is asked afterwards (AT+CFUN?), and a modem that does not confirm counts as a failed stage." -msgstr "Der Flugmodus passt für die meisten Modems. Minimalbetrieb ist für Modems gedacht, die den Flugmodus nicht annehmen. In beiden Fällen wird das Modem anschließend abgefragt (AT+CFUN?); bestätigt es den Zustand nicht, gilt die Stufe als fehlgeschlagen." - -msgid "Flight mode (AT+CFUN=4)" -msgstr "Flugmodus (AT+CFUN=4)" - -msgid "Minimum function (AT+CFUN=0)" -msgstr "Minimalbetrieb (AT+CFUN=0)" - -msgid "Interface to take down" -msgstr "Herunterzufahrende Schnittstelle" - -msgid "Network interface taken down by the interface action and brought back up when the stages are cleared. Empty uses the modem network interface." -msgstr "Netzwerkschnittstelle, die die Schnittstellen-Aktion herunterfährt und beim Zurücksetzen der Stufen wieder startet. Leer bedeutet: die Netzwerkschnittstelle des Modems." - -msgid "Runs /etc/thermal-guard/hooks/stage2. Place the file over SSH and make it executable." -msgstr "Führt /etc/thermal-guard/hooks/stage2 aus. Legen Sie die Datei per SSH an und machen Sie sie ausführbar." - -msgid "Notification address" -msgstr "Benachrichtigungsadresse" - -msgid "The warning text is posted here. Works with ntfy, Gotify, Matrix, Slack and anything else that accepts a plain POST body." -msgstr "An diese Adresse wird der Warntext gesendet. Funktioniert mit ntfy, Gotify, Matrix, Slack und jedem anderen Dienst, der einen einfachen POST-Inhalt entgegennimmt." - -msgid "Must be an http:// or https:// address without quotes or spaces" -msgstr "Muss eine http:// oder https://-Adresse ohne Anführungszeichen und Leerzeichen sein" - -msgid "Additional header" -msgstr "Zusätzlicher Header" - -msgid "Sent with the notification, for example an authorisation token. Needs curl installed; the smaller uclient-fetch cannot send headers." -msgstr "Wird mit der Benachrichtigung mitgesendet, zum Beispiel ein Autorisierungstoken. Erfordert ein installiertes curl; das kleinere uclient-fetch kann keine Header senden." - -msgid "Must be printable text without quotes or backslashes" -msgstr "Muss druckbarer Text ohne Anführungszeichen und Backslashes sein" - -msgid "Modem network interface" -msgstr "Netzwerkschnittstelle des Modems" - -msgid "Lets the service tell whether another internet connection exists, so a message from stage 2 of 3 on goes out or waits in the queue. Also the interface the interface action takes when none is set." -msgstr "Damit erkennt der Dienst, ob eine andere Internetverbindung besteht, und entscheidet ab Stufe 2 von 3, ob eine Nachricht sofort hinausgeht oder in der Warteschlange wartet. Außerdem die Netzwerkschnittstelle, die die Schnittstellen-Aktion verwendet, wenn keine eingestellt ist." - -msgid "Wi-Fi %d" -msgstr "WLAN %d" - -msgid "Ethernet" -msgstr "Ethernet" - -msgid "SSD" -msgstr "SSD" - -msgid "modem radio off" -msgstr "Mobilfunk abgeschaltet" - -msgid "Wi-Fi off" -msgstr "WLAN abgeschaltet" - -msgid "network interface down" -msgstr "Netzwerkschnittstelle heruntergefahren" - -msgid "custom command" -msgstr "eigenes Skript" - -msgid "no action" -msgstr "keine Aktion" - -msgid "unknown action" -msgstr "unbekannte Aktion" - -msgid "Stage 1 of 3 active: fan at full speed" -msgstr "Stufe 1 von 3 aktiv: Lüfter auf voller Drehzahl" - -msgid "Stage 1 of 3 active: warning, fan left to its own controller" -msgstr "Stufe 1 von 3 aktiv: Warnung, Lüfter bleibt bei seiner eigenen Regelung" - -msgid "Stage 2 of 3 active: %s" -msgstr "Stufe 2 von 3 aktiv: %s" - -msgid "Stage 3 of 3 active: %s" -msgstr "Stufe 3 von 3 aktiv: %s" - -msgid "No intervention, temperatures within range" -msgstr "Kein Eingriff, Temperaturen im normalen Bereich" - -msgid "%d s ago" -msgstr "vor %d s" - -msgid "%d min ago" -msgstr "vor %d min" - -msgid "Current temperatures and protection state. Thresholds and actions are configured under System, Thermal Guard." -msgstr "Aktuelle Temperaturen und Schutzzustand. Schwellen und Aktionen werden unter System, Thermal Guard eingestellt." - -msgid "No data yet. Check whether the Thermal Guard service is running." -msgstr "Noch keine Daten. Prüfen Sie, ob der Dienst Thermal Guard läuft." - -msgid "Processor temperature" -msgstr "Prozessortemperatur" - -msgid "Modem temperature" -msgstr "Modemtemperatur" - -msgid "no reading" -msgstr "kein Messwert" - -msgid "fan step %d of %d" -msgstr "Lüfterstufe %d von %d" - -msgid "Fan control" -msgstr "Lüftersteuerung" - -msgid "Another service is controlling this fan" -msgstr "Der Lüfter wird von einem anderen Dienst gesteuert" - -msgid "This device does not report any temperature" -msgstr "Dieses Gerät meldet keine Temperatur" - -msgid "No thermal sensor was found, so the processor is not watched on this hardware. The modem thresholds still apply while the modem reports a temperature." -msgstr "Es wurde kein Temperatursensor gefunden, der Prozessor wird auf dieser Hardware daher nicht überwacht. Die Modemschwellen gelten weiter, solange das Modem eine Temperatur meldet." - -msgid "The service is not running. Protection stages stay applied and the fan is not handed back." -msgstr "Der Dienst läuft nicht. Aktive Schutzstufen bleiben bestehen und der Lüfter wird nicht freigegeben." - -msgid "The service is not running. Temperatures are not being watched." -msgstr "Der Dienst läuft nicht. Die Temperaturen werden nicht überwacht." - -msgid "The service is paused. Protection stages stay applied and the fan is not handed back." -msgstr "Der Dienst ist angehalten. Aktive Schutzstufen bleiben bestehen und der Lüfter wird nicht freigegeben." - -msgid "The service is paused. Temperatures are not being watched." -msgstr "Der Dienst ist angehalten. Die Temperaturen werden nicht überwacht." - -msgid "Protection stages stay active until you clear them. Clear them once the cause is fixed, for example a replaced fan." -msgstr "Schutzstufen bleiben aktiv, bis Sie sie zurücksetzen. Setzen Sie sie zurück, sobald die Ursache behoben ist, etwa nach dem Austausch des Lüfters." - -msgid "Clear protection stages" -msgstr "Schutzstufen zurücksetzen" - -msgid "Last reading: %s" -msgstr "Letzte Messung: %s" - -msgid "modem source: %s" -msgstr "Modemquelle: %s" - -msgid "fan mode: %s" -msgstr "Lüftersteuerung: %s" - -msgid "version %s" -msgstr "Version %s" - -msgid "Clearing failed: %s" -msgstr "Zurücksetzen fehlgeschlagen: %s" - -msgid "Protection stages cleared." -msgstr "Schutzstufen zurückgesetzt." - -msgid "Temperatures" -msgstr "Temperaturen" - -msgid "Processor" -msgstr "Prozessor" - -msgid "protection stage %d active" -msgstr "Schutzstufe %d aktiv" - -msgid "Fan switching temperatures" -msgstr "Schalttemperaturen des Lüfters" - -msgid "lowered by %d K for the modem" -msgstr "für das Modem um %d K abgesenkt" - -msgid "The firmware brings new fan switching temperatures (%s °C). Your own values still apply; review them under System, Thermal Guard, Fan." -msgstr "Die Firmware bringt neue Schalttemperaturen für den Lüfter mit (%s °C). Ihre eigenen Werte gelten weiter; prüfen Sie sie unter System, Thermal Guard, Lüfter." - -msgid "the kernel knows no fan on this device" -msgstr "der Kernel kennt auf diesem Gerät keinen Lüfter" - -msgid "no thermal zone drives the fan" -msgstr "keine Thermalzone steuert den Lüfter" - -msgid "the fan's thermal zone is not under /sys/class/thermal" -msgstr "die Thermalzone des Lüfters liegt nicht unter /sys/class/thermal" - -msgid "the fan is not bound to any switching temperature" -msgstr "der Lüfter ist an keine Schalttemperatur gebunden" - -msgid "the switching temperatures cannot be changed" -msgstr "die Schalttemperaturen lassen sich nicht ändern" - -msgid "unknown reason" -msgstr "unbekannter Grund" - -msgid "Managed by this service, switching temperatures now %s °C" -msgstr "Von diesem Dienst verwaltet, Schalttemperaturen derzeit %s °C" - -msgid "Not possible on this device: %s" -msgstr "Auf diesem Gerät nicht möglich: %s" - -msgid "Not managed by this service" -msgstr "Nicht von diesem Dienst verwaltet" - -msgid "Speed the fan up for the modem" -msgstr "Lüfter für das Modem hochregeln" - -msgid "Where the kernel drives the fan, its switching temperatures are lowered while the modem runs hotter than the processor, so the fan speeds up for the modem as well. They only ever go down, never above the base values. A missing modem reading keeps them where they are." -msgstr "Wo der Kernel den Lüfter steuert, werden seine Schalttemperaturen abgesenkt, solange das Modem wärmer ist als der Prozessor. So dreht der Lüfter auch für das Modem hoch. Die Werte werden nur abgesenkt, nie über die Grundwerte angehoben. Fehlt ein Messwert des Modems, bleiben sie, wo sie sind." - -msgid "Off" -msgstr "Aus" - -msgid "Follow the modem temperature" -msgstr "Der Modemtemperatur folgen" - -msgid "Modem allowance" -msgstr "Spielraum für das Modem" - -msgid "Degrees the modem may run above the processor before the switching temperatures are lowered." -msgstr "Grad, um die das Modem wärmer sein darf als der Prozessor, bevor die Schalttemperaturen abgesenkt werden." - -msgid "Base values in degrees at which the kernel moves the fan one step up, lowest first, one value per step. Empty uses the values of the installed firmware: %s °C." -msgstr "Grundwerte in Grad, bei denen der Kernel den Lüfter eine Stufe höher schaltet, der niedrigste zuerst, ein Wert je Stufe. Leer gelten die Werte der installierten Firmware: %s °C." - -msgid "Base values in degrees at which the kernel moves the fan one step up, lowest first, one value per step. Empty uses the values of the installed firmware." -msgstr "Grundwerte in Grad, bei denen der Kernel den Lüfter eine Stufe höher schaltet, der niedrigste zuerst, ein Wert je Stufe. Leer gelten die Werte der installierten Firmware." - -msgid "Must be between 20 and 95" -msgstr "Muss zwischen 20 und 95 liegen" - -msgid "Each value must be higher than the one before" -msgstr "Jeder Wert muss höher sein als der vorige" - -msgid "Must be below %d, the temperature at which the kernel itself intervenes" -msgstr "Muss unter %d liegen, der Temperatur, bei der der Kernel selbst eingreift" - -msgid "Needs exactly %d values, one per fan step" -msgstr "Genau %d Werte nötig, einer je Lüfterstufe" - -msgid "New firmware values" -msgstr "Neue Werte der Firmware" - -msgid "The installed firmware now uses %s °C. Your own base values were set for an earlier firmware and still apply." -msgstr "Die installierte Firmware verwendet jetzt %s °C. Ihre eigenen Grundwerte wurden für eine frühere Firmware eingestellt und gelten weiter." - -msgid "Use the firmware values" -msgstr "Werte der Firmware übernehmen" - -msgid "Keep my values" -msgstr "Eigene Werte behalten" - -msgid "Recording the firmware values failed: %s" -msgstr "Die Firmware-Werte konnten nicht festgehalten werden: %s" - -msgid "Firmware values recorded. The notice goes away with the next check." -msgstr "Firmware-Werte festgehalten. Der Hinweis verschwindet mit der nächsten Prüfung." - -msgid "Fan steps of the firmware" -msgstr "Lüfterstufen der Firmware" - -msgid "Raw value per fan step from the device tree, with the duty cycle of the signal. They belong to the fan that is fitted and change only with a new firmware image." -msgstr "Rohwert je Lüfterstufe aus dem Device Tree, mit dem Tastverhältnis des Signals. Sie gehören zum eingebauten Lüfter und ändern sich nur mit einer neuen Firmware." - -msgid "step %d: %d (%d %%)" -msgstr "Stufe %d: %d (%d %%)" diff --git a/openwrt-thermal-guard/luci-app-thermal-guard/po/templates/thermal-guard.pot b/openwrt-thermal-guard/luci-app-thermal-guard/po/templates/thermal-guard.pot deleted file mode 100644 index 3edd45ede..000000000 --- a/openwrt-thermal-guard/luci-app-thermal-guard/po/templates/thermal-guard.pot +++ /dev/null @@ -1,504 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: luci-app-thermal-guard\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -msgid "Enter a whole number" -msgstr "" - -msgid "Must be above %s (%d)" -msgstr "" - -msgid "Must be below %s (%d)" -msgstr "" - -msgid "Must be an absolute path without \"..\"" -msgstr "" - -msgid "Path must start with %s" -msgstr "" - -msgid "Thermal Guard" -msgstr "" - -msgid "Monitors the temperature of the main processor and the modem. If the device gets too hot, it intervenes in up to three protection stages. Stages stay active until you clear them on the status page." -msgstr "" - -msgid "General" -msgstr "" - -msgid "Thresholds" -msgstr "" - -msgid "Temperatures at which each protection stage starts. What actually happens at stage 2 and 3 is set on the Actions tab." -msgstr "" - -msgid "Fan" -msgstr "" - -msgid "Modem" -msgstr "" - -msgid "Actions" -msgstr "" - -msgid "What happens when a stage starts. Stage 1 of 3 warns and, if this service drives the fan (Fan tab), sets it to full speed. That cannot be changed." -msgstr "" - -msgid "Enabled" -msgstr "" - -msgid "Switching this off pauses the temperature checks. Protection stages that are already active stay applied and the fan is not handed back, so a broken fan cannot be hidden by a checkbox. Use Clear on the status page for that." -msgstr "" - -msgid "Interval" -msgstr "" - -msgid "Seconds between two temperature readings. Values below five seconds are treated as five." -msgstr "" - -msgid "Processor temperature source" -msgstr "" - -msgid "System file the processor temperature is read from. The default fits most devices; change it only if your device reports its temperature elsewhere." -msgstr "" - -msgid "Processor warning (stage 1 of 3)" -msgstr "" - -msgid "Stage 1 of 3 starts when either temperature reaches its warning level: a warning, and full fan speed if this service drives the fan." -msgstr "" - -msgid "Processor critical" -msgstr "" - -msgid "Processor critical (stage 2 of 3)" -msgstr "" - -msgid "Processor warning" -msgstr "" - -msgid "Processor emergency" -msgstr "" - -msgid "Processor emergency (stage 3 of 3)" -msgstr "" - -msgid "Modem warning (stage 1 of 3)" -msgstr "" - -msgid "Modem critical" -msgstr "" - -msgid "Modem critical (stage 2 of 3)" -msgstr "" - -msgid "Modem warning" -msgstr "" - -msgid "Hold time" -msgstr "" - -msgid "Minutes a stage has to last before the next one starts on time alone: stage 2 of 3 if the temperature has not dropped, stage 3 of 3 if it is still rising." -msgstr "" - -msgid "Hysteresis" -msgstr "" - -msgid "How many degrees the temperature has to fall below the warning level before the device counts as cooled down." -msgstr "" - -msgid "Fan control method" -msgstr "" - -msgid "Leave the fan to the regulator the device already has, unless it has none. Automatic uses the kernel fan control when the device offers it and otherwise leaves the fan alone. It never picks direct control, which needs the fan's polarity." -msgstr "" - -msgid "Automatic" -msgstr "" - -msgid "Kernel fan control" -msgstr "" - -msgid "Direct fan control (PWM)" -msgstr "" - -msgid "Leave the fan alone" -msgstr "" - -msgid "Fan identifier" -msgstr "" - -msgid "Name the kernel uses for the fan. Change it only if the fan is not found automatically." -msgstr "" - -msgid "Control mode to restore" -msgstr "" - -msgid "Control mode handed back to the kernel when the stages are cleared and no earlier control mode was recorded. The default fits almost every device." -msgstr "" - -msgid "Fan speed file" -msgstr "" - -msgid "Only needed for direct fan control." -msgstr "" - -msgid "Fan control file" -msgstr "" - -msgid "Only needed for direct fan control. Switches the fan chip to manual control." -msgstr "" - -msgid "Value for full speed" -msgstr "" - -msgid "Required for direct fan control. The raw value that makes this fan spin fastest. Some boards wire the signal inverted, where 1 is full speed and 255 stops the fan. Without this value and the slowest one, the fan is left alone." -msgstr "" - -msgid "Value for the slowest setting" -msgstr "" - -msgid "Required for direct fan control. The opposite end of the same scale. Together with full speed it states the polarity, and it shows the fan speed as a percentage. Never written to the hardware." -msgstr "" - -msgid "Modem temperature source" -msgstr "" - -msgid "Automatic tries the known modem commands in turn. Select your manufacturer if detection fails." -msgstr "" - -msgid "Quectel (AT+QTEMP)" -msgstr "" - -msgid "Fibocom (AT+GTSENRDTEMP)" -msgstr "" - -msgid "File" -msgstr "" - -msgid "Command" -msgstr "" - -msgid "No modem" -msgstr "" - -msgid "Modem control port" -msgstr "" - -msgid "Serial port the commands are sent to. Automatic checks the usual ports." -msgstr "" - -msgid "Lock file for modem access" -msgstr "" - -msgid "Prevents this service and other programs from sending commands to the modem at the same time. Point those programs at the same file." -msgstr "" - -msgid "Temperature file" -msgstr "" - -msgid "Own temperature source" -msgstr "" - -msgid "Reads /etc/thermal-guard/hooks/modem-temp, which has to print the temperature in degrees as a whole number. Place the file over SSH; it runs as root and is deliberately not editable from here." -msgstr "" - -msgid "Action at stage 2 of 3" -msgstr "" - -msgid "Switch the modem radio off" -msgstr "" - -msgid "Switch Wi-Fi off" -msgstr "" - -msgid "Take a network interface down" -msgstr "" - -msgid "Run own script" -msgstr "" - -msgid "Nothing" -msgstr "" - -msgid "Script at stage 2 of 3" -msgstr "" - -msgid "Runs /etc/thermal-guard/hooks/stage1. Place the file over SSH and make it executable." -msgstr "" - -msgid "Action at stage 3 of 3" -msgstr "" - -msgid "Script at stage 3 of 3" -msgstr "" - -msgid "How the modem is switched off" -msgstr "" - -msgid "Flight mode suits most modems. Use minimum function for a modem that does not accept flight mode. Either way the modem is asked afterwards (AT+CFUN?), and a modem that does not confirm counts as a failed stage." -msgstr "" - -msgid "Flight mode (AT+CFUN=4)" -msgstr "" - -msgid "Minimum function (AT+CFUN=0)" -msgstr "" - -msgid "Interface to take down" -msgstr "" - -msgid "Network interface taken down by the interface action and brought back up when the stages are cleared. Empty uses the modem network interface." -msgstr "" - -msgid "Runs /etc/thermal-guard/hooks/stage2. Place the file over SSH and make it executable." -msgstr "" - -msgid "Notification address" -msgstr "" - -msgid "The warning text is posted here. Works with ntfy, Gotify, Matrix, Slack and anything else that accepts a plain POST body." -msgstr "" - -msgid "Must be an http:// or https:// address without quotes or spaces" -msgstr "" - -msgid "Additional header" -msgstr "" - -msgid "Sent with the notification, for example an authorisation token. Needs curl installed; the smaller uclient-fetch cannot send headers." -msgstr "" - -msgid "Must be printable text without quotes or backslashes" -msgstr "" - -msgid "Modem network interface" -msgstr "" - -msgid "Lets the service tell whether another internet connection exists, so a message from stage 2 of 3 on goes out or waits in the queue. Also the interface the interface action takes when none is set." -msgstr "" - -msgid "Wi-Fi %d" -msgstr "" - -msgid "Ethernet" -msgstr "" - -msgid "SSD" -msgstr "" - -msgid "modem radio off" -msgstr "" - -msgid "Wi-Fi off" -msgstr "" - -msgid "network interface down" -msgstr "" - -msgid "custom command" -msgstr "" - -msgid "no action" -msgstr "" - -msgid "unknown action" -msgstr "" - -msgid "Stage 1 of 3 active: fan at full speed" -msgstr "" - -msgid "Stage 1 of 3 active: warning, fan left to its own controller" -msgstr "" - -msgid "Stage 2 of 3 active: %s" -msgstr "" - -msgid "Stage 3 of 3 active: %s" -msgstr "" - -msgid "No intervention, temperatures within range" -msgstr "" - -msgid "%d s ago" -msgstr "" - -msgid "%d min ago" -msgstr "" - -msgid "Current temperatures and protection state. Thresholds and actions are configured under System, Thermal Guard." -msgstr "" - -msgid "No data yet. Check whether the Thermal Guard service is running." -msgstr "" - -msgid "Processor temperature" -msgstr "" - -msgid "Modem temperature" -msgstr "" - -msgid "no reading" -msgstr "" - -msgid "fan step %d of %d" -msgstr "" - -msgid "Fan control" -msgstr "" - -msgid "Another service is controlling this fan" -msgstr "" - -msgid "This device does not report any temperature" -msgstr "" - -msgid "No thermal sensor was found, so the processor is not watched on this hardware. The modem thresholds still apply while the modem reports a temperature." -msgstr "" - -msgid "The service is not running. Protection stages stay applied and the fan is not handed back." -msgstr "" - -msgid "The service is not running. Temperatures are not being watched." -msgstr "" - -msgid "The service is paused. Protection stages stay applied and the fan is not handed back." -msgstr "" - -msgid "The service is paused. Temperatures are not being watched." -msgstr "" - -msgid "Protection stages stay active until you clear them. Clear them once the cause is fixed, for example a replaced fan." -msgstr "" - -msgid "Clear protection stages" -msgstr "" - -msgid "Last reading: %s" -msgstr "" - -msgid "modem source: %s" -msgstr "" - -msgid "fan mode: %s" -msgstr "" - -msgid "version %s" -msgstr "" - -msgid "Clearing failed: %s" -msgstr "" - -msgid "Protection stages cleared." -msgstr "" - -msgid "Temperatures" -msgstr "" - -msgid "Processor" -msgstr "" - -msgid "protection stage %d active" -msgstr "" - -msgid "Fan switching temperatures" -msgstr "" - -msgid "lowered by %d K for the modem" -msgstr "" - -msgid "The firmware brings new fan switching temperatures (%s °C). Your own values still apply; review them under System, Thermal Guard, Fan." -msgstr "" - -msgid "the kernel knows no fan on this device" -msgstr "" - -msgid "no thermal zone drives the fan" -msgstr "" - -msgid "the fan's thermal zone is not under /sys/class/thermal" -msgstr "" - -msgid "the fan is not bound to any switching temperature" -msgstr "" - -msgid "the switching temperatures cannot be changed" -msgstr "" - -msgid "unknown reason" -msgstr "" - -msgid "Managed by this service, switching temperatures now %s °C" -msgstr "" - -msgid "Not possible on this device: %s" -msgstr "" - -msgid "Not managed by this service" -msgstr "" - -msgid "Speed the fan up for the modem" -msgstr "" - -msgid "Where the kernel drives the fan, its switching temperatures are lowered while the modem runs hotter than the processor, so the fan speeds up for the modem as well. They only ever go down, never above the base values. A missing modem reading keeps them where they are." -msgstr "" - -msgid "Off" -msgstr "" - -msgid "Follow the modem temperature" -msgstr "" - -msgid "Modem allowance" -msgstr "" - -msgid "Degrees the modem may run above the processor before the switching temperatures are lowered." -msgstr "" - -msgid "Base values in degrees at which the kernel moves the fan one step up, lowest first, one value per step. Empty uses the values of the installed firmware: %s °C." -msgstr "" - -msgid "Base values in degrees at which the kernel moves the fan one step up, lowest first, one value per step. Empty uses the values of the installed firmware." -msgstr "" - -msgid "Must be between 20 and 95" -msgstr "" - -msgid "Each value must be higher than the one before" -msgstr "" - -msgid "Must be below %d, the temperature at which the kernel itself intervenes" -msgstr "" - -msgid "Needs exactly %d values, one per fan step" -msgstr "" - -msgid "New firmware values" -msgstr "" - -msgid "The installed firmware now uses %s °C. Your own base values were set for an earlier firmware and still apply." -msgstr "" - -msgid "Use the firmware values" -msgstr "" - -msgid "Keep my values" -msgstr "" - -msgid "Recording the firmware values failed: %s" -msgstr "" - -msgid "Firmware values recorded. The notice goes away with the next check." -msgstr "" - -msgid "Fan steps of the firmware" -msgstr "" - -msgid "Raw value per fan step from the device tree, with the duty cycle of the signal. They belong to the fan that is fitted and change only with a new firmware image." -msgstr "" - -msgid "step %d: %d (%d %%)" -msgstr "" diff --git a/openwrt-thermal-guard/luci-app-thermal-guard/root/usr/share/luci/menu.d/luci-app-thermal-guard.json b/openwrt-thermal-guard/luci-app-thermal-guard/root/usr/share/luci/menu.d/luci-app-thermal-guard.json deleted file mode 100644 index bf331aa56..000000000 --- a/openwrt-thermal-guard/luci-app-thermal-guard/root/usr/share/luci/menu.d/luci-app-thermal-guard.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "admin/status/thermal-guard": { - "title": "Thermal Guard", - "order": 60, - "action": { - "type": "view", - "path": "thermal-guard/monitor" - }, - "depends": { - "acl": [ "luci-app-thermal-guard" ] - } - }, - - "admin/system/thermal-guard": { - "title": "Thermal Guard", - "order": 62, - "action": { - "type": "view", - "path": "thermal-guard/config" - }, - "depends": { - "acl": [ "luci-app-thermal-guard" ], - "uci": { "thermal-guard": true } - } - } -} diff --git a/openwrt-thermal-guard/luci-app-thermal-guard/root/usr/share/rpcd/acl.d/luci-app-thermal-guard.json b/openwrt-thermal-guard/luci-app-thermal-guard/root/usr/share/rpcd/acl.d/luci-app-thermal-guard.json deleted file mode 100644 index 6e3af4b7e..000000000 --- a/openwrt-thermal-guard/luci-app-thermal-guard/root/usr/share/rpcd/acl.d/luci-app-thermal-guard.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "luci-app-thermal-guard": { - "description": "Read the thermal-guard status, edit its configuration, clear stages and record firmware trips", - "read": { - "file": { - "/var/run/thermal-guard.status": [ "read" ], - "/tmp/run/thermal-guard.status": [ "read" ] - }, - "ubus": { - "file": [ "read" ] - }, - "uci": [ "thermal-guard" ] - }, - "write": { - "file": { - "/usr/sbin/thermal-guard reset": [ "exec" ], - "/usr/sbin/thermal-guard trips-keep": [ "exec" ] - }, - "ubus": { - "file": [ "exec" ] - }, - "uci": [ "thermal-guard" ] - } - } -} diff --git a/openwrt-thermal-guard/screenshots/overview-bpi-r3-mini.png b/openwrt-thermal-guard/screenshots/overview-bpi-r3-mini.png deleted file mode 100644 index 5fe3e3969..000000000 Binary files a/openwrt-thermal-guard/screenshots/overview-bpi-r3-mini.png and /dev/null differ diff --git a/openwrt-thermal-guard/screenshots/overview-gl-x3000.png b/openwrt-thermal-guard/screenshots/overview-gl-x3000.png deleted file mode 100644 index 7f6607101..000000000 Binary files a/openwrt-thermal-guard/screenshots/overview-gl-x3000.png and /dev/null differ diff --git a/openwrt-thermal-guard/screenshots/settings-actions.png b/openwrt-thermal-guard/screenshots/settings-actions.png deleted file mode 100644 index 9e28da4b3..000000000 Binary files a/openwrt-thermal-guard/screenshots/settings-actions.png and /dev/null differ diff --git a/openwrt-thermal-guard/screenshots/settings-fan.png b/openwrt-thermal-guard/screenshots/settings-fan.png deleted file mode 100644 index bc0976c46..000000000 Binary files a/openwrt-thermal-guard/screenshots/settings-fan.png and /dev/null differ diff --git a/openwrt-thermal-guard/screenshots/settings-general.png b/openwrt-thermal-guard/screenshots/settings-general.png deleted file mode 100644 index 33bccebe1..000000000 Binary files a/openwrt-thermal-guard/screenshots/settings-general.png and /dev/null differ diff --git a/openwrt-thermal-guard/screenshots/settings-modem-fibocom.png b/openwrt-thermal-guard/screenshots/settings-modem-fibocom.png deleted file mode 100644 index b63b88379..000000000 Binary files a/openwrt-thermal-guard/screenshots/settings-modem-fibocom.png and /dev/null differ diff --git a/openwrt-thermal-guard/screenshots/settings-modem-quectel.png b/openwrt-thermal-guard/screenshots/settings-modem-quectel.png deleted file mode 100644 index 80f5f2eee..000000000 Binary files a/openwrt-thermal-guard/screenshots/settings-modem-quectel.png and /dev/null differ diff --git a/openwrt-thermal-guard/screenshots/settings-thresholds.png b/openwrt-thermal-guard/screenshots/settings-thresholds.png deleted file mode 100644 index 5d790ae72..000000000 Binary files a/openwrt-thermal-guard/screenshots/settings-thresholds.png and /dev/null differ diff --git a/openwrt-thermal-guard/screenshots/status-bpi-r3-mini.png b/openwrt-thermal-guard/screenshots/status-bpi-r3-mini.png deleted file mode 100644 index 2f5d7f212..000000000 Binary files a/openwrt-thermal-guard/screenshots/status-bpi-r3-mini.png and /dev/null differ diff --git a/openwrt-thermal-guard/screenshots/status-gl-x3000.png b/openwrt-thermal-guard/screenshots/status-gl-x3000.png deleted file mode 100644 index 71fb1a02c..000000000 Binary files a/openwrt-thermal-guard/screenshots/status-gl-x3000.png and /dev/null differ diff --git a/openwrt-thermal-guard/tests/build-jsonfilter.sh b/openwrt-thermal-guard/tests/build-jsonfilter.sh deleted file mode 100755 index a666a73d3..000000000 --- a/openwrt-thermal-guard/tests/build-jsonfilter.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/sh -# shellcheck shell=busybox -# SPDX-License-Identifier: GPL-2.0-only -# Builds jsonfilter for the test suites on a development host or CI runner. -# It comes from jsonpath, linked against a static libubox, both at the -# commits the ImmortalWrt image uses. Needs git, cmake, a C compiler and the -# json-c headers (Ubuntu: cmake libjson-c-dev). The binary lands in -# /bin/jsonfilter. - -set -eu - -LIBUBOX_REF=7dd127841e82eb1cfb61185da37dde7b9bd9ba6d -JSONPATH_REF=b9034210bd331749673416c6bf389cccd4e23610 - -[ $# -eq 1 ] || { echo "usage: build-jsonfilter.sh " >&2; exit 2; } -mkdir -p "$1" -out=$(cd "$1" && pwd) -src="$out/src" - -fetch() { # $1 project, $2 commit - [ -d "$src/$1" ] || git clone -q "https://git.openwrt.org/project/$1.git" "$src/$1" - git -C "$src/$1" checkout -q "$2" -} -fetch libubox "$LIBUBOX_REF" -fetch jsonpath "$JSONPATH_REF" - -cmake -S "$src/libubox" -B "$src/libubox/build" -DBUILD_LUA=OFF -DBUILD_EXAMPLES=OFF \ - > /dev/null -cmake --build "$src/libubox/build" --target ubox-static > /dev/null -mkdir -p "$out/include/libubox" "$out/lib" "$out/bin" -cp "$src"/libubox/*.h "$out/include/libubox/" -cp "$src/libubox/build/libubox.a" "$out/lib/" - -# jsonpath builds in its source tree, its parser generator uses relative paths. -cmake -S "$src/jsonpath" -B "$src/jsonpath" -DCMAKE_INCLUDE_PATH="$out/include" \ - -DCMAKE_EXE_LINKER_FLAGS="-L$out/lib" > /dev/null -cmake --build "$src/jsonpath" > /dev/null -cp "$src/jsonpath/jsonpath" "$out/bin/jsonfilter" -echo "$out/bin/jsonfilter" diff --git a/openwrt-thermal-guard/tests/check-quoted-newlines.py b/openwrt-thermal-guard/tests/check-quoted-newlines.py deleted file mode 100755 index 7292d0f2f..000000000 --- a/openwrt-thermal-guard/tests/check-quoted-newlines.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0-only -"""Report quoted strings in shell files that contain a real line break. - -A line end has to come from an escape (printf '%s\n'), not from the source -text: an editor or a CRLF checkout changes the latter without a trace, and -shellcheck does not look. Heredocs are skipped, and so are awk and sed -programs, whose text spans lines on purpose. In double quotes a break inside -$( ) does not count, that is a command spanning lines. - -Usage: check-quoted-newlines.py FILE... exit 1 when anything is found - check-quoted-newlines.py --selftest -""" - -import os -import re -import sys -import tempfile - -HEREDOC = re.compile(r"<<-?\s*['\"]?([A-Za-z_][A-Za-z0-9_]*)['\"]?") -PROGRAM = re.compile(r"\b(awk|sed)\b") -SUBST = re.compile(r"\$\((?:[^()]|\([^()]*\))*\)") - - -def closing_double(text, start): - """Index of the double quote that ends the string opened before start.""" - i, depth = start, 0 - while i < len(text): - c = text[i] - if c == "\\": - i += 2 - continue - if text.startswith("$(", i): - depth += 1 - i += 2 - continue - if c == ")" and depth: - depth -= 1 - elif c == '"' and not depth: - return i - i += 1 - return len(text) - - -def scan(path): - """Yields (line, first characters) for every string with a line break.""" - text = open(path, encoding="utf-8").read() - i, line, heredoc = 0, 1, None - while i < len(text): - c = text[i] - if heredoc: - end = text.find("\n", i) - if end < 0: - return - if text[i:end].strip() == heredoc: - heredoc = None - i, line = end + 1, line + 1 - continue - if c == "\n": - i, line = i + 1, line + 1 - continue - if c == "#" and (i == 0 or text[i - 1] in " \t\n;"): - end = text.find("\n", i) - i = len(text) if end < 0 else end - continue - match = HEREDOC.match(text, i) - if match: - heredoc = match.group(1) - end = text.find("\n", i) - i = len(text) if end < 0 else end - continue - if c == "\\": - i += 2 - continue - if c in "'\"": - end = text.find("'", i + 1) if c == "'" else closing_double(text, i + 1) - if end < 0: - end = len(text) - body = text[i + 1:end] - before = text[text.rfind("\n", 0, i) + 1:i] - flat = body if c == "'" else SUBST.sub("", body) - if "\n" in flat and not (c == "'" and PROGRAM.search(before)): - yield line, body[:40] - i, line = end + 1, line + body.count("\n") - continue - i += 1 - - -def check(paths): - found = 0 - for path in paths: - for line, head in scan(path): - print(f"{path}:{line}: line break inside a quoted string: {head!r}") - found += 1 - return found - - -def selftest(): - cases = { - # These are Python strings: \\n puts the shell escape \n into the - # planted file, a bare \n a real line break. Mixing them up tests the - # opposite of what the name says. - "printf with an escape": ("printf '%s=%s\\n' a b\n", 0), - "printf with a real break": ("printf '%s=%s\n' a b\n", 1), - "assignment": ('list="$list$t\n"\n', 1), - "awk program": ("awk '\n{ print }\n' f\n", 0), - "command spanning lines": ('x="$(grep -c \\\n\tfoo f)"\n', 0), - "heredoc": ("cat <<'END'\nit's\nEND\n", 0), - "comment": ("# don't\necho ok\n", 0), - } - fail = 0 - with tempfile.TemporaryDirectory() as tmp: - for name, (text, want) in cases.items(): - path = os.path.join(tmp, "case.sh") - with open(path, "w", encoding="utf-8") as f: - f.write(text) - got = len(list(scan(path))) - ok = got == want - fail |= not ok - print(f"{'ok ' if ok else 'FAIL'} {name}: {got} found, {want} expected") - return fail - - -if __name__ == "__main__": - if sys.argv[1:] == ["--selftest"]: - sys.exit(1 if selftest() else 0) - if not sys.argv[1:]: - print(__doc__.strip().splitlines()[-2], file=sys.stderr) - sys.exit(2) - sys.exit(1 if check(sys.argv[1:]) else 0) diff --git a/openwrt-thermal-guard/tests/fixtures/wireless-status-bpi-r3-mini.json b/openwrt-thermal-guard/tests/fixtures/wireless-status-bpi-r3-mini.json deleted file mode 100644 index 8f14c0513..000000000 --- a/openwrt-thermal-guard/tests/fixtures/wireless-status-bpi-r3-mini.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "MT7986_1_1": { - "up": true, - "pending": false, - "autostart": true, - "disabled": false, - "retry_setup_failed": false, - "config": { - "type": "mtwifi", - "band": "2g", - "channel": "auto", - "htmode": "HE20", - "noscan": true, - "txpower": 100, - "mu_beamformer": true, - "dbdc_main": true, - "country": "DE" - }, - "interfaces": [ - { - "section": "mgmt_2g", - "config": { - "network": [ - "mgmt" - ], - "isolate": true, - "device": "MT7986_1_1", - "mode": "ap", - "encryption": "sae-mixed", - "key": "REDACTED", - "ssid": "example-ssid", - "hidden": false, - "ieee80211k": false, - "radios": [ - - ] - }, - "ifname": "ra0", - "vlans": [ - - ], - "stations": [ - - ] - }, - { - "section": "gast_2g", - "config": { - "network": [ - "gast" - ], - "isolate": true, - "device": "MT7986_1_1", - "mode": "ap", - "encryption": "sae-mixed", - "key": "REDACTED", - "ssid": "example-ssid", - "hidden": false, - "ieee80211k": true, - "radios": [ - - ] - }, - "ifname": "ra1", - "vlans": [ - - ], - "stations": [ - - ] - }, - { - "section": "iot_2g", - "config": { - "network": [ - "iot" - ], - "isolate": true, - "device": "MT7986_1_1", - "mode": "ap", - "encryption": "psk2", - "key": "REDACTED", - "ssid": "example-ssid", - "hidden": false, - "ieee80211k": false, - "radios": [ - - ] - }, - "ifname": "ra2", - "vlans": [ - - ], - "stations": [ - - ] - }, - { - "section": "yoda_2g", - "config": { - "network": [ - "lan" - ], - "device": "MT7986_1_1", - "mode": "ap", - "encryption": "sae-mixed", - "key": "REDACTED", - "ssid": "example-ssid", - "radios": [ - - ] - }, - "ifname": "ra3", - "vlans": [ - - ], - "stations": [ - - ] - } - ] - }, - "MT7986_1_2": { - "up": true, - "pending": false, - "autostart": true, - "disabled": false, - "retry_setup_failed": false, - "config": { - "type": "mtwifi", - "band": "5g", - "channel": "auto", - "htmode": "HE80", - "noscan": false, - "txpower": 100, - "mu_beamformer": true, - "dbdc_main": false, - "country": "DE" - }, - "interfaces": [ - { - "section": "yoda_5g", - "config": { - "network": [ - "lan" - ], - "device": "MT7986_1_2", - "mode": "ap", - "encryption": "sae-mixed", - "key": "REDACTED", - "ssid": "example-ssid", - "radios": [ - - ] - }, - "ifname": "rax0", - "vlans": [ - - ], - "stations": [ - - ] - }, - { - "section": "gast_5g", - "config": { - "network": [ - "gast" - ], - "isolate": true, - "device": "MT7986_1_2", - "mode": "ap", - "encryption": "sae-mixed", - "key": "REDACTED", - "ssid": "example-ssid", - "hidden": false, - "ieee80211k": true, - "radios": [ - - ] - }, - "ifname": "rax1", - "vlans": [ - - ], - "stations": [ - - ] - } - ] - } -} diff --git a/openwrt-thermal-guard/tests/fixtures/wireless-status-gl-x3000.json b/openwrt-thermal-guard/tests/fixtures/wireless-status-gl-x3000.json deleted file mode 100644 index c9a5cf414..000000000 --- a/openwrt-thermal-guard/tests/fixtures/wireless-status-gl-x3000.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "radio0": { - "up": true, - "pending": false, - "autostart": true, - "disabled": false, - "retry_setup_failed": false, - "config": { - "type": "mac80211", - "band": "2g", - "cell_density": 0, - "channel": "1", - "htmode": "HE20", - "path": "platform/soc/18000000.wifi" - }, - "interfaces": [ - { - "section": "default_radio0", - "config": { - "network": [ - "lan" - ], - "device": [ - "radio0" - ], - "mode": "ap", - "encryption": "sae-mixed", - "key": "REDACTED", - "ocv": 0, - "ssid": "example-ssid", - "radios": [ - - ] - }, - "ifname": "phy0-ap0", - "vlans": [ - - ], - "stations": [ - - ] - } - ] - }, - "radio1": { - "up": true, - "pending": false, - "autostart": true, - "disabled": false, - "retry_setup_failed": false, - "config": { - "type": "mac80211", - "band": "5g", - "cell_density": 0, - "channel": "36", - "htmode": "HE80", - "path": "platform/soc/18000000.wifi+1" - }, - "interfaces": [ - { - "section": "default_radio1", - "config": { - "network": [ - "lan" - ], - "device": [ - "radio1" - ], - "mode": "ap", - "encryption": "sae-mixed", - "key": "REDACTED", - "ocv": 0, - "ssid": "example-ssid", - "radios": [ - - ] - }, - "ifname": "phy1-ap0", - "vlans": [ - - ], - "stations": [ - - ] - } - ] - } -} diff --git a/openwrt-thermal-guard/tests/functions.sh b/openwrt-thermal-guard/tests/functions.sh deleted file mode 100644 index 0f983e4ff..000000000 --- a/openwrt-thermal-guard/tests/functions.sh +++ /dev/null @@ -1,48 +0,0 @@ -# shellcheck shell=busybox -# SPDX-License-Identifier: GPL-2.0-only -# Stand-in for /lib/functions.sh with the two calls thermal-guard makes. Reads -# the UCI file "$UCI_CONFIG_DIR/" and fills CONFIG_
_