mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 18:44:03 +08:00
🤞 Sync 2026-09-27 22:31:51
This commit is contained in:
1 parent
a9e8d948d5
commit
eba40cf537
18 files changed
+5738
No files matched your search
@@ -0,0 +1,68 @@
|
||||
#
|
||||
# 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_NAME:=thermal-guard
|
||||
PKG_VERSION:=2.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=SyntaxError NullPointer <287466119+SyntaxError-NullPointer@users.noreply.github.com>
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/thermal-guard
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
TITLE:=Overheating protection with staged mitigation
|
||||
DEPENDS:=+libubox +ubus +coreutils-timeout +jsonfilter
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/thermal-guard/description
|
||||
Watches SoC and modem temperature and escalates in stages when a router runs
|
||||
hot, typically after a fan failure: fan to full speed, modem radio off, Wi-Fi
|
||||
off. Stages do not roll back on their own, clearing them is an operator
|
||||
decision, and they survive a reboot. Fan control works either through the
|
||||
kernel thermal zone or through sysfs PWM, modem temperature through Quectel or
|
||||
Fibocom AT commands, a file or a hook script. Warnings are posted to a URL.
|
||||
Where the kernel drives the fan, it can lower the fan's trip points while the
|
||||
modem runs hotter than the CPU.
|
||||
endef
|
||||
|
||||
define Package/thermal-guard/conffiles
|
||||
/etc/config/thermal-guard
|
||||
endef
|
||||
|
||||
# procd keeps an instance whose command line did not change, so without this
|
||||
# an upgrade leaves the old daemon running. A service that was stopped stays so.
|
||||
define Package/thermal-guard/postinst
|
||||
#!/bin/sh
|
||||
[ "$${PKG_UPGRADE}" = 1 ] && [ -z "$${IPKG_INSTROOT}" ] &&
|
||||
/etc/init.d/thermal-guard running && /etc/init.d/thermal-guard restart
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/thermal-guard/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/usr/libexec $(1)/usr/share/thermal-guard $(1)/etc/config $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/etc/thermal-guard/hooks $(1)/lib/upgrade/keep.d
|
||||
$(INSTALL_DATA) ./files/lib/upgrade/keep.d/thermal-guard $(1)/lib/upgrade/keep.d/thermal-guard
|
||||
$(INSTALL_DATA) ./files/etc/thermal-guard/hooks/README $(1)/etc/thermal-guard/hooks/README
|
||||
$(INSTALL_BIN) ./files/usr/sbin/thermal-guard $(1)/usr/sbin/thermal-guard
|
||||
$(INSTALL_DATA) ./files/usr/share/thermal-guard/backends.sh $(1)/usr/share/thermal-guard/backends.sh
|
||||
$(INSTALL_DATA) ./files/usr/share/thermal-guard/trips.sh $(1)/usr/share/thermal-guard/trips.sh
|
||||
$(INSTALL_CONF) ./files/etc/config/thermal-guard $(1)/etc/config/thermal-guard
|
||||
$(INSTALL_BIN) ./files/etc/init.d/thermal-guard $(1)/etc/init.d/thermal-guard
|
||||
$(INSTALL_BIN) ./files/etc/uci-defaults/90-thermal-guard-trip-base $(1)/etc/uci-defaults/
|
||||
$(INSTALL_BIN) ./files/usr/libexec/thermal-guard-collectd $(1)/usr/libexec/thermal-guard-collectd
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,thermal-guard))
|
||||
@@ -0,0 +1,80 @@
|
||||
|
||||
config thermal-guard 'main'
|
||||
option enabled '1'
|
||||
# seconds between readings, minimum 5
|
||||
option interval '20'
|
||||
|
||||
# stage thresholds in degrees celsius, must be ordered warn < crit < emergency
|
||||
option cpu_warn '70'
|
||||
option cpu_crit '80'
|
||||
option cpu_emergency '90'
|
||||
option modem_warn '80'
|
||||
option modem_crit '88'
|
||||
|
||||
# minutes a stage has to persist before the next one can trigger on time alone
|
||||
option hold_minutes '3'
|
||||
# degrees below the warn thresholds that count as cooled down
|
||||
option hysteresis '10'
|
||||
|
||||
option cpu_temp_path '/sys/class/thermal/thermal_zone0/temp'
|
||||
# also report the other temperature sensors of the board, Wi-Fi chips and so on
|
||||
option extra_sensors '1'
|
||||
|
||||
# none, cooling_device (kernel thermal zone), pwm (sysfs) or auto.
|
||||
# none leaves the fan to whatever regulates it already. auto takes the cooling
|
||||
# device if there is one and otherwise stays off the fan; it never picks pwm.
|
||||
option fan_mode 'none'
|
||||
# type of the fan's cooling device, for cooling_device, auto and trip
|
||||
# management; empty means pwm-fan
|
||||
option fan_cooling_device_type 'pwm-fan'
|
||||
# governor the zone is handed back to on reset when none was recorded
|
||||
# before: step_wise, fair_share, bang_bang or power_allocator. Not
|
||||
# user_space, nothing would regulate then.
|
||||
option fan_governor 'step_wise'
|
||||
# pwm mode. Leave the paths empty to look the fan chip up by hwmon name.
|
||||
#option pwm_path '/sys/class/hwmon/hwmon2/pwm1'
|
||||
#option pwm_enable_path '/sys/class/hwmon/hwmon2/pwm1_enable'
|
||||
# raw value for fastest and for slowest, both required for pwm mode. Some
|
||||
# boards run these the other way round, 1 full speed and 255 stopped
|
||||
#option pwm_full '255'
|
||||
#option pwm_idle '0'
|
||||
|
||||
# auto, quectel (AT+QTEMP), fibocom (AT+GTSENRDTEMP), file, command or none
|
||||
option modem_source 'auto'
|
||||
option modem_at_port 'auto'
|
||||
#option modem_temp_file '/var/run/modem_temp'
|
||||
# "command" reads /etc/thermal-guard/hooks/modem-temp
|
||||
# shared lock so concurrent AT users do not talk over each other
|
||||
option at_lock '/var/lock/modem-at.lock'
|
||||
|
||||
# Lower the fan's active trips while the modem runs hotter than the CPU, so
|
||||
# the kernel spins the fan up for it. off or modem. Needs a thermal zone that
|
||||
# drives the fan; nothing is written while this is off and trip_active is unset.
|
||||
# Unset means off, so an image can switch it on for its board at first boot
|
||||
#option trip_boost 'modem'
|
||||
# how much warmer than the CPU the modem may run before trips are lowered, 0-40
|
||||
#option trip_boost_offset '15'
|
||||
# baseline for the active trips in degrees, ascending, one value per trip.
|
||||
# Unset means the device tree values of the running image
|
||||
#list trip_active '50'
|
||||
#list trip_active '60'
|
||||
#list trip_active '70'
|
||||
|
||||
# modem_radio_off, wifi_off, interface_down, command or none. "command" runs
|
||||
# /etc/thermal-guard/hooks/stage1 and .../stage2
|
||||
option stage1_action 'modem_radio_off'
|
||||
option stage2_action 'wifi_off'
|
||||
# AT+CFUN=4 (flight mode) or AT+CFUN=0 (minimum function), nothing else.
|
||||
# Read back with AT+CFUN? either way
|
||||
option modem_radio_off_at 'AT+CFUN=4'
|
||||
# interface taken down by interface_down, the uplink interface when unset
|
||||
#option action_interface 'wwan'
|
||||
|
||||
# the message is posted here. Works with ntfy, Gotify, Matrix, Slack and
|
||||
# anything else that takes a plain POST body. For a delivery HTTP cannot
|
||||
# express, put an executable at /etc/thermal-guard/hooks/notify instead
|
||||
#option notify_url 'https://ntfy.sh/my-topic'
|
||||
# only sent when curl is installed, uclient-fetch cannot set headers
|
||||
#option notify_header 'Authorization: Bearer secret'
|
||||
# network interface of the modem, lets the daemon tell whether another uplink exists
|
||||
#option uplink_interface 'wwan'
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# shellcheck shell=busybox
|
||||
# shellcheck disable=SC2034 # rc.common reads START, STOP and USE_PROCD
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Stopping or crashing leaves a forced fan forced, on purpose. Only
|
||||
# "thermal-guard reset" hands it back. Do not add a stop_service that releases
|
||||
# it: dying mid incident with the fan running is the safe direction.
|
||||
|
||||
START=95
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/usr/sbin/thermal-guard
|
||||
|
||||
# The instance runs whatever "enabled" says. The daemon reads the option every
|
||||
# cycle and pauses on its own, which keeps it writing the status file so the
|
||||
# web interface can say "paused, stage still active" instead of showing a
|
||||
# snapshot that quietly stopped ageing.
|
||||
start_service() {
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG" daemon
|
||||
# Respawn on crash. A forced fan stays forced meanwhile, which is the safe direction.
|
||||
procd_set_param respawn 3600 10 0
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger thermal-guard
|
||||
}
|
||||
|
||||
# Nothing to do. tg_cfg runs at the top of every cycle, so a changed option is
|
||||
# live within one interval. Restarting here would drop a stage action that is
|
||||
# in flight, which is a real risk for no gain. Use restart if you want that.
|
||||
reload_service() {
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
Hooks for thermal-guard
|
||||
=======================
|
||||
|
||||
Executables placed here run as root when the matching action fires. They are
|
||||
files rather than configuration options on purpose: the LuCI page may edit
|
||||
every option in /etc/config/thermal-guard, but it cannot write into this
|
||||
directory, so nothing changed through the web interface can introduce code
|
||||
that runs as root.
|
||||
|
||||
Put a hook here over SSH and make it executable (chmod 0755). A hook that is
|
||||
missing or not executable is skipped: for stage1 and stage2 the daemon logs
|
||||
that the action could not be carried out, a missing notify hook means no
|
||||
delivery that way, and a missing modem-temp hook means no modem reading.
|
||||
|
||||
notify Receives the message text on stdin. Only needed for a delivery
|
||||
that notify_url cannot express, a local mailer for example.
|
||||
When notify_url is set as well, it is tried first and this hook
|
||||
is the fallback.
|
||||
|
||||
stage1 Runs when stage 2 of 3 starts and stage1_action is 'command'.
|
||||
stage2 Runs when stage 3 of 3 starts and stage2_action is 'command'.
|
||||
|
||||
modem-temp Runs when modem_source is 'command'. Has to print the modem
|
||||
temperature in whole degrees Celsius on stdout, nothing else.
|
||||
|
||||
Every hook runs under a time limit: 15 seconds for notify, stage1 and stage2,
|
||||
5 seconds for modem-temp, which runs every cycle. A hook still running then
|
||||
gets TERM, and KILL 5 seconds later. It counts as failed: a stage action is
|
||||
reported as not carried out, a modem-temp hook as no reading. Keep hooks short;
|
||||
a mail or HTTP call inside one needs its own, shorter time limit.
|
||||
|
||||
Example, a notify hook for a local mailer:
|
||||
|
||||
#!/bin/sh
|
||||
exec mail -s "thermal-guard" root
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=busybox
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
# Moves trip_dt out of /etc/config/thermal-guard. The daemon keeps the device
|
||||
# tree trips that trip_active was made for in /etc/thermal-guard/trip_base now.
|
||||
# Safe to run again; always exits 0, so it runs once and is removed.
|
||||
|
||||
dir="${PERSIST_DIR:-/etc/thermal-guard}"
|
||||
f="$dir/trip_base"
|
||||
v=$(uci -q get thermal-guard.main.trip_dt)
|
||||
[ -n "$v" ] || exit 0
|
||||
|
||||
# Whole degrees only. Anything else is nothing worth moving, only removing.
|
||||
case "$v" in
|
||||
*[!0-9\ ]*) v="" ;;
|
||||
esac
|
||||
if [ -n "$v" ] && [ ! -e "$f" ]; then
|
||||
# A failed write keeps the option, so the value is not lost with it.
|
||||
mkdir -p "$dir" && echo "$v" > "$f.tmp" && chmod 644 "$f.tmp" &&
|
||||
mv "$f.tmp" "$f" || exit 0
|
||||
sync
|
||||
fi
|
||||
uci -q delete thermal-guard.main.trip_dt
|
||||
uci -q commit thermal-guard
|
||||
exit 0
|
||||
@@ -0,0 +1 @@
|
||||
/etc/thermal-guard/
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=busybox
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# collectd exec plugin feed. Reads the status file the daemon writes and prints
|
||||
# PUTVAL lines, so temperatures end up in RRD without anyone else touching the
|
||||
# modem. Add to /etc/collectd.conf:
|
||||
#
|
||||
# LoadPlugin exec
|
||||
# <Plugin exec>
|
||||
# Exec "nobody:nogroup" "/usr/libexec/thermal-guard-collectd"
|
||||
# </Plugin>
|
||||
#
|
||||
# The status file is world readable, so an unprivileged user is enough.
|
||||
|
||||
set -u
|
||||
|
||||
STATUS_FILE="${STATUS_FILE:-/var/run/thermal-guard.status}"
|
||||
INTERVAL="${COLLECTD_INTERVAL:-30}"
|
||||
HOSTNAME="${COLLECTD_HOSTNAME:-$(cat /proc/sys/kernel/hostname 2>/dev/null || echo localhost)}"
|
||||
|
||||
# collectd wants whole seconds and refuses an interval of zero
|
||||
INTERVAL=${INTERVAL%%.*}
|
||||
[ "$INTERVAL" -ge 1 ] 2>/dev/null || INTERVAL=30
|
||||
|
||||
# The status file is written by the daemon, but a truncated write or a stale
|
||||
# file from an older version can still hand us something that is not a number.
|
||||
# Anything that is not a plain integer becomes empty, and empty is not emitted.
|
||||
tg_num() {
|
||||
case "$1" in
|
||||
''|*[!0-9-]*|-|?*-*) echo "" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
putval() { # $1 type-instance, $2 value
|
||||
[ -n "$2" ] || return 0
|
||||
printf 'PUTVAL "%s/thermal_guard/temperature-%s" interval=%s N:%s\n' \
|
||||
"$HOSTNAME" "$1" "$INTERVAL" "$2"
|
||||
}
|
||||
|
||||
putgauge() { # $1 type-instance, $2 value
|
||||
[ -n "$2" ] || return 0
|
||||
printf 'PUTVAL "%s/thermal_guard/gauge-%s" interval=%s N:%s\n' \
|
||||
"$HOSTNAME" "$1" "$INTERVAL" "$2"
|
||||
}
|
||||
|
||||
while :; do
|
||||
if [ -r "$STATUS_FILE" ]; then
|
||||
cpu=""; modem=""; stage=""; fan=""
|
||||
while IFS='=' read -r k v; do
|
||||
case "$k" in
|
||||
cpu_c) cpu=$(tg_num "$v") ;;
|
||||
modem_c) modem=$(tg_num "$v") ;;
|
||||
stage) stage=$(tg_num "$v") ;;
|
||||
fan_state) fan=$(tg_num "$v") ;;
|
||||
esac
|
||||
done < "$STATUS_FILE"
|
||||
|
||||
putval soc "$cpu"
|
||||
putval modem "$modem"
|
||||
putgauge fan_step "$fan"
|
||||
# -1 means idle, the graph then shows anything above zero as an incident
|
||||
putgauge stage "$stage"
|
||||
fi
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
Executable
+2179
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,969 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
# Hardware backends for thermal-guard: fan control, modem temperature, stage actions.
|
||||
# Sourced by /usr/sbin/thermal-guard, never run on its own.
|
||||
# shellcheck shell=busybox
|
||||
# shellcheck disable=SC2034 # MODEM_VAL and MODEM_SRC are read by the caller
|
||||
|
||||
# ---- helpers ---------------------------------------------------------------
|
||||
|
||||
# Keep integers only so a garbled sysfs read or AT reply cannot reach the logic.
|
||||
tg_int() {
|
||||
case "$1" in
|
||||
''|*[!0-9-]*|-|?*-*) echo "" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Whether a program or function is there. The one place that asks, so the
|
||||
# selftest can take a tool away without touching the host.
|
||||
tg_have() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Absolute path, no .. and no wildcards. Stops UCI from pointing us at random files.
|
||||
tg_path_ok() {
|
||||
case "$1" in
|
||||
''|*..*|*\**|*\?*|*\\*|*[!\ -~]*) return 1 ;;
|
||||
/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# $1 path, $2 prefix. The prefix ends in a slash, so a sibling like /tmpfoo
|
||||
# cannot pass as /tmp/.
|
||||
tg_path_under() {
|
||||
tg_path_ok "$1" || return 1
|
||||
case "$1" in
|
||||
"$2"*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
tg_path_thermal() { tg_path_under "$1" "$THERMAL_ROOT/"; }
|
||||
tg_path_hwmon() {
|
||||
tg_path_under "$1" "$HWMON_ROOT/" || tg_path_under "$1" /sys/devices/
|
||||
}
|
||||
# Boards differ on where the SoC sensor lives: a thermal zone on most targets,
|
||||
# a hwmon node on x86 with coretemp. Both have to stay reachable.
|
||||
tg_path_sensor() {
|
||||
tg_path_thermal "$1" || tg_path_hwmon "$1"
|
||||
}
|
||||
tg_path_modem_file() {
|
||||
tg_path_under "$1" /var/run/ || tg_path_under "$1" /tmp/ || tg_path_under "$1" /sys/
|
||||
}
|
||||
tg_path_lock() {
|
||||
tg_path_under "$1" /var/lock/ || tg_path_under "$1" /var/run/ ||
|
||||
tg_path_under "$1" /tmp/
|
||||
}
|
||||
tg_path_dev() { tg_path_under "$1" /dev/; }
|
||||
|
||||
# ---- notification delivery -------------------------------------------------
|
||||
#
|
||||
# A message goes out over HTTP, which covers ntfy, Gotify, Matrix, Slack and the
|
||||
# rest, and needs no shell command from the configuration to do it. curl is
|
||||
# preferred where present because it can send a header; uclient-fetch ships in
|
||||
# the OpenWrt base but has no way to set one.
|
||||
tg_fetcher() {
|
||||
tg_have curl && { echo curl; return; }
|
||||
tg_have uclient-fetch && { echo uclient-fetch; return; }
|
||||
tg_have wget && { echo wget; return; }
|
||||
}
|
||||
|
||||
# Curl reads its config file as directives, and one of them writes a file. A
|
||||
# quote would end the value and turn the rest into more of them, a newline
|
||||
# would start one outright. Neither belongs in a url or a header anyway, so
|
||||
# reject both along with every other non-printable character rather than
|
||||
# escaping and hoping the escaping is right.
|
||||
tg_http_safe() {
|
||||
# The '\' alternative is a literal backslash and is meant to be one.
|
||||
# shellcheck disable=SC1003
|
||||
case "$1" in
|
||||
''|*'"'*|*'\'*|*[!\ -~]*) return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# $1 message. The url and any header go through a config file rather than the
|
||||
# command line: they routinely carry an access token, and /proc/*/cmdline is
|
||||
# readable by every local process.
|
||||
tg_notify_http() {
|
||||
local tool cfg rc
|
||||
[ -n "$NOTIFY_URL" ] || return 1
|
||||
|
||||
case "$NOTIFY_URL" in
|
||||
http://?*|https://?*) ;;
|
||||
*) tg_log "notify_url is not an http or https address, not sending" warning; return 1 ;;
|
||||
esac
|
||||
if ! tg_http_safe "$NOTIFY_URL"; then
|
||||
tg_log "notify_url contains characters that are not allowed in one, not sending" warning
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$NOTIFY_HEADER" ] && ! tg_http_safe "$NOTIFY_HEADER"; then
|
||||
tg_log "notify_header contains characters that are not allowed in one, not sending" warning
|
||||
return 1
|
||||
fi
|
||||
|
||||
tool=$(tg_fetcher)
|
||||
[ -n "$tool" ] || {
|
||||
tg_log "no curl or wget available, cannot send notifications" warning
|
||||
return 1
|
||||
}
|
||||
|
||||
if [ "$tool" = curl ]; then
|
||||
NOTIFY_HEADER_TOLD=""
|
||||
mkdir -p "$STATE_DIR"
|
||||
cfg="$STATE_DIR/notify.conf"
|
||||
: > "$cfg"
|
||||
chmod 600 "$cfg" 2>/dev/null
|
||||
printf 'url = "%s"\n' "$NOTIFY_URL" >> "$cfg"
|
||||
[ -n "$NOTIFY_HEADER" ] && printf 'header = "%s"\n' "$NOTIFY_HEADER" >> "$cfg"
|
||||
printf '%s\n' "$1" | curl -fsS -m 15 -K "$cfg" --data-binary @- >/dev/null 2>&1
|
||||
rc=$?
|
||||
rm -f "$cfg"
|
||||
return $rc
|
||||
fi
|
||||
|
||||
# Said once per fetcher, not per message and not per retry of the outbox.
|
||||
if [ -z "$NOTIFY_HEADER" ]; then
|
||||
NOTIFY_HEADER_TOLD=""
|
||||
elif [ "$NOTIFY_HEADER_TOLD" != "$tool" ]; then
|
||||
tg_log "$tool cannot send a header, install curl for authenticated webhooks" warning
|
||||
NOTIFY_HEADER_TOLD=$tool
|
||||
fi
|
||||
# -T bounds the network wait only, the outer limit also covers a hung DNS lookup.
|
||||
timeout "$HTTP_TIMEOUT" "$tool" -q -T 15 -O /dev/null --post-data="$1" "$NOTIFY_URL" \
|
||||
>/dev/null 2>&1
|
||||
}
|
||||
|
||||
# The escape hatch for anything HTTP cannot express, a local mailer for example.
|
||||
# It is a file an administrator puts there over ssh, not a string from the
|
||||
# configuration, so editing the config cannot introduce code that runs as root.
|
||||
tg_notify_hook() {
|
||||
local hook="$HOOK_DIR/notify"
|
||||
[ -x "$hook" ] || return 1
|
||||
printf '%s\n' "$1" | timeout -k 5 "$HOOK_TIMEOUT" "$hook" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Run an AT command through a lock. Several daemons and LuCI apps share one port
|
||||
# and the modem answers a collision with nothing at all. Both the wait for the
|
||||
# lock and the query are bounded: a modem that hangs, or a program that sits on
|
||||
# the lock, must not stop the whole loop. BusyBox flock has no -w, so the outer
|
||||
# timeout covers the wait and the inner one the query.
|
||||
# Without flock the query still runs, the modem reading matters more than the
|
||||
# lock. But another program on the port can garble it now, so say so once.
|
||||
tg_at_unlocked() {
|
||||
[ "$FLOCK_TOLD" = 1 ] && return 0
|
||||
tg_cfg_log "no flock command, AT queries run without the shared lock"
|
||||
[ "$DAEMON" = 1 ] && FLOCK_TOLD=1
|
||||
return 0
|
||||
}
|
||||
|
||||
tg_at() {
|
||||
local port="$1" cmd="$2" lock=$AT_LOCK
|
||||
# Checked here as well as at load: the value goes to flock, which creates
|
||||
# the file it names.
|
||||
tg_path_lock "$lock" || lock=/var/lock/modem-at.lock
|
||||
[ -c "$port" ] || return 1
|
||||
tg_have sms_tool || return 1
|
||||
if ! tg_have timeout; then
|
||||
if [ "$AT_NO_TIMEOUT_TOLD" = 0 ]; then
|
||||
tg_cfg_log "no timeout command, AT queries run without a time limit"
|
||||
[ "$DAEMON" = 1 ] && AT_NO_TIMEOUT_TOLD=1
|
||||
fi
|
||||
if tg_have flock; then
|
||||
flock "$lock" sms_tool -d "$port" at "$cmd" 2>/dev/null
|
||||
else
|
||||
tg_at_unlocked
|
||||
sms_tool -d "$port" at "$cmd" 2>/dev/null
|
||||
fi
|
||||
return
|
||||
fi
|
||||
if tg_have flock; then
|
||||
timeout "$AT_WAIT" flock "$lock" timeout "$AT_TIMEOUT" sms_tool -d "$port" at "$cmd" 2>/dev/null
|
||||
else
|
||||
tg_at_unlocked
|
||||
timeout "$AT_TIMEOUT" sms_tool -d "$port" at "$cmd" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Other programs read the modem temperature here instead of asking the modem
|
||||
# themselves. Written after a good reading only, so its age shows an outage.
|
||||
tg_modem_publish() {
|
||||
local f="$STATE_DIR/modem-temp"
|
||||
[ -n "$MODEM_VAL" ] || return 0
|
||||
mkdir -p "$STATE_DIR"
|
||||
printf '%s %s %s\n' "$MODEM_VAL" "$(date +%s)" "$MODEM_SRC" > "$f.tmp" || return 0
|
||||
chmod 644 "$f.tmp" 2>/dev/null
|
||||
mv "$f.tmp" "$f"
|
||||
}
|
||||
|
||||
# ---- fan -------------------------------------------------------------------
|
||||
|
||||
tg_fan_detect() {
|
||||
tg_fan_pick
|
||||
tg_fan_pick_view
|
||||
}
|
||||
|
||||
# Pick a backend. "auto" only ever means the cooling device, whose polarity the
|
||||
# device tree carries; a raw pwm output needs pwm_full and pwm_idle from the
|
||||
# operator, and without both this stays off the fan.
|
||||
tg_fan_pick() {
|
||||
case "$FAN_MODE" in
|
||||
cooling_device|pwm|none) ;;
|
||||
*)
|
||||
if [ -n "$(tg_fan_find_cdev)" ]; then
|
||||
FAN_MODE=cooling_device
|
||||
else
|
||||
FAN_MODE=none
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$FAN_MODE" = pwm ]; then
|
||||
if ! tg_fan_polarity_known; then
|
||||
FAN_MODE=none
|
||||
[ "$FAN_POLARITY_TOLD" = 1 ] && return 0
|
||||
tg_cfg_log "fan_mode pwm needs both pwm_full and pwm_idle, leaving the fan alone"
|
||||
[ "$DAEMON" = 1 ] && FAN_POLARITY_TOLD=1
|
||||
return 0
|
||||
fi
|
||||
tg_fan_resolve_pwm
|
||||
fi
|
||||
|
||||
if [ "$FAN_MODE" = cooling_device ]; then
|
||||
[ -n "$FAN_CDEV" ] || FAN_CDEV=$(tg_fan_find_cdev)
|
||||
[ -n "$FAN_CDEV" ] || { FAN_MODE=none; return; }
|
||||
if ! tg_path_thermal "$FAN_CDEV"; then
|
||||
tg_cfg_log "rejecting fan cooling device path: $FAN_CDEV"
|
||||
FAN_CDEV=""; FAN_MODE=none; return
|
||||
fi
|
||||
# A zone is optional. It is only needed to take the cooling device away
|
||||
# from a kernel governor, and plenty of boards ship a pwm-fan cooling
|
||||
# device that no zone binds. There the state can be written straight
|
||||
# out, and there is nothing to hand back either.
|
||||
[ -n "$FAN_ZONE" ] || FAN_ZONE=$(tg_fan_find_zone "$FAN_CDEV")
|
||||
if [ -n "$FAN_ZONE" ] && ! tg_path_thermal "$FAN_ZONE"; then
|
||||
tg_cfg_log "rejecting fan thermal zone path: $FAN_ZONE"
|
||||
FAN_ZONE=""
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# What the fan is read from, which FAN_MODE does not decide when it is none: the
|
||||
# fan then belongs to another program, and what it is doing is still the most
|
||||
# useful thing on the page. The raw pwm output comes first where the polarity is
|
||||
# known, because a cooling device's cur_state is derived from cooling-levels
|
||||
# and reads full speed as stopped on a board whose levels descend.
|
||||
tg_fan_pick_view() {
|
||||
local cdev
|
||||
if [ "$FAN_MODE" != none ]; then
|
||||
FAN_VIEW=$FAN_MODE
|
||||
return 0
|
||||
fi
|
||||
FAN_VIEW=none
|
||||
tg_fan_resolve_pwm
|
||||
if [ -n "$PWM_PATH" ] && [ -r "$PWM_PATH" ] && tg_fan_polarity_known; then
|
||||
FAN_VIEW=pwm
|
||||
return 0
|
||||
fi
|
||||
cdev=$(tg_fan_find_cdev)
|
||||
if [ -n "$cdev" ] && tg_path_thermal "$cdev"; then
|
||||
FAN_CDEV=$cdev; FAN_VIEW=cooling_device
|
||||
return 0
|
||||
fi
|
||||
[ -n "$PWM_PATH" ] && [ -r "$PWM_PATH" ] && FAN_VIEW=pwm
|
||||
return 0
|
||||
}
|
||||
|
||||
tg_fan_polarity_known() {
|
||||
[ -n "$(tg_int "$PWM_FULL")" ] && [ -n "$(tg_int "$PWM_IDLE")" ]
|
||||
}
|
||||
|
||||
# A cooling device whose max_state is 0 can cool by exactly nothing. Selecting
|
||||
# it would have tg_fan_full write that 0 and report success, so stage 1 would
|
||||
# announce a fan at full speed and change nothing at all.
|
||||
tg_fan_find_cdev() {
|
||||
local d max
|
||||
for d in "$THERMAL_ROOT"/cooling_device*; do
|
||||
[ -r "$d/type" ] || continue
|
||||
case "$(cat "$d/type" 2>/dev/null)" in
|
||||
"$FAN_CDEV_TYPE") ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
max=$(tg_int "$(cat "$d/max_state" 2>/dev/null)")
|
||||
[ -n "$max" ] && [ "$max" -gt 0 ] || continue
|
||||
echo "$d"
|
||||
return
|
||||
done
|
||||
}
|
||||
|
||||
# Fill in the pwm paths from the detected chip. A configured path that works is
|
||||
# left alone; one that stopped working is replaced, because a renumbered hwmon
|
||||
# is the likely reason and running without a fan is not an option here.
|
||||
tg_fan_resolve_pwm() {
|
||||
local h
|
||||
[ -n "$PWM_PATH" ] && [ -w "$PWM_PATH" ] && [ -n "$PWM_ENABLE_PATH" ] && return 0
|
||||
|
||||
h=$(tg_fan_find_hwmon)
|
||||
[ -n "$h" ] || return 0
|
||||
tg_path_hwmon "$h" || return 0
|
||||
|
||||
if [ -z "$PWM_PATH" ] || [ ! -w "$PWM_PATH" ]; then
|
||||
[ -w "$h/pwm1" ] && tg_fan_set_pwm_path "$h/pwm1"
|
||||
fi
|
||||
[ -n "$PWM_ENABLE_PATH" ] || [ ! -e "$h/pwm1_enable" ] || PWM_ENABLE_PATH="$h/pwm1_enable"
|
||||
return 0
|
||||
}
|
||||
|
||||
tg_fan_set_pwm_path() {
|
||||
local old=$PWM_PATH
|
||||
[ "$1" = "$old" ] && return 0
|
||||
PWM_PATH="$1"
|
||||
[ "$PWM_PATH_TOLD" = "$old>$1" ] && return 0
|
||||
if [ -z "$old" ]; then
|
||||
tg_cfg_log "fan pwm output detected at '$1'" info
|
||||
else
|
||||
tg_cfg_log "fan pwm path '$old' unusable, using detected '$1'"
|
||||
fi
|
||||
[ "$DAEMON" = 1 ] && PWM_PATH_TOLD="$old>$1"
|
||||
return 0
|
||||
}
|
||||
|
||||
# hwmon numbers are assigned in driver probe order, so hwmon2 today can be
|
||||
# hwmon1 after adding an NVMe or a kernel update. Find the fan chip by its name
|
||||
# instead: writing a duty cycle to whatever landed on that number is exactly the
|
||||
# silent failure this daemon exists to prevent.
|
||||
tg_fan_find_hwmon() {
|
||||
local h
|
||||
for h in "$HWMON_ROOT"/hwmon*; do
|
||||
[ -r "$h/name" ] || continue
|
||||
case "$(cat "$h/name" 2>/dev/null)" in
|
||||
pwmfan|pwm-fan|pwm_fan) echo "$h"; return ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# The zone that drives this cooling device, so we know whose governor to switch.
|
||||
tg_fan_find_zone() {
|
||||
local cdev="$1" z c
|
||||
for z in "$THERMAL_ROOT"/thermal_zone*; do
|
||||
for c in "$z"/cdev*; do
|
||||
[ -e "$c" ] || continue
|
||||
[ "$(readlink -f "$c")" = "$(readlink -f "$cdev")" ] && { echo "$z"; return; }
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
tg_fan_state() {
|
||||
case "$FAN_VIEW" in
|
||||
cooling_device)
|
||||
tg_path_thermal "$FAN_CDEV" || return
|
||||
cat "$FAN_CDEV/cur_state" 2>/dev/null
|
||||
;;
|
||||
pwm)
|
||||
tg_path_hwmon "$PWM_PATH" || return
|
||||
cat "$PWM_PATH" 2>/dev/null
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
tg_fan_max() {
|
||||
case "$FAN_VIEW" in
|
||||
cooling_device)
|
||||
tg_path_thermal "$FAN_CDEV" || return
|
||||
cat "$FAN_CDEV/max_state" 2>/dev/null
|
||||
;;
|
||||
pwm) tg_fan_polarity_known && echo "$PWM_FULL" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Fan speed as 0..100 regardless of how the raw scale runs. For pwm that is the
|
||||
# span between PWM_IDLE and PWM_FULL, which may descend on an inverted board;
|
||||
# for a cooling device it is cur_state against max_state, which always ascends.
|
||||
# Without this the status page reports a fan at full speed as nearly stopped.
|
||||
tg_fan_percent() { # $1 optional raw value, otherwise the live reading
|
||||
local cur max span pct
|
||||
cur=${1:-$(tg_fan_state)}
|
||||
cur=$(tg_int "$cur")
|
||||
[ -n "$cur" ] || return 0
|
||||
|
||||
case "$FAN_VIEW" in
|
||||
pwm)
|
||||
tg_fan_polarity_known || return 0
|
||||
span=$((PWM_FULL - PWM_IDLE))
|
||||
[ "$span" -ne 0 ] || return 0
|
||||
pct=$(( (cur - PWM_IDLE) * 100 / span ))
|
||||
;;
|
||||
cooling_device)
|
||||
max=$(tg_int "$(tg_fan_max)")
|
||||
[ -n "$max" ] && [ "$max" -gt 0 ] || return 0
|
||||
pct=$((cur * 100 / max))
|
||||
;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
|
||||
[ "$pct" -lt 0 ] && pct=0
|
||||
[ "$pct" -gt 100 ] && pct=100
|
||||
echo "$pct"
|
||||
}
|
||||
|
||||
# We forced the fan when the stage was entered. If the value is gone the chip
|
||||
# belongs to someone else, a second fan daemon or a governor we did not take
|
||||
# over. Re-assert, because too much cooling is the safe direction, but say once
|
||||
# what is happening instead of quietly fighting every cycle.
|
||||
FAN_FOREIGN=0
|
||||
tg_fan_check_owner() {
|
||||
local want have
|
||||
if [ "$STAGE" -lt 0 ]; then
|
||||
# Idle, so nothing of ours is in there to compare against. For a
|
||||
# cooling device the zone still tells: user_space means somebody took
|
||||
# it off its governor, and it was not this daemon. Worth knowing
|
||||
# before an incident rather than during one.
|
||||
FAN_FOREIGN=0
|
||||
[ "$FAN_MODE" = cooling_device ] && [ -n "$FAN_ZONE" ] || return 0
|
||||
tg_path_thermal "$FAN_ZONE" || return 0
|
||||
[ "$(cat "$FAN_ZONE/policy" 2>/dev/null)" = user_space ] || return 0
|
||||
FAN_FOREIGN=1
|
||||
tg_cfg_log "thermal zone is on user_space while no stage is active, another program drives this fan"
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$FAN_MODE" in
|
||||
pwm) want=$PWM_FULL ;;
|
||||
cooling_device) want=$(tg_int "$(tg_fan_max)") ;;
|
||||
*) FAN_FOREIGN=0; return 0 ;;
|
||||
esac
|
||||
have=$(tg_int "$(tg_fan_state)")
|
||||
[ -n "$want" ] && [ -n "$have" ] || return 0
|
||||
|
||||
if [ "$have" = "$want" ]; then
|
||||
FAN_FOREIGN=0
|
||||
return 0
|
||||
fi
|
||||
[ "$FAN_FOREIGN" = 1 ] || tg_log "fan was set to $want but reads $have, another controller owns it. Set fan_mode=none to leave it alone." warning
|
||||
FAN_FOREIGN=1
|
||||
tg_fan_full || tg_log "could not reassert the fan (mode $FAN_MODE)" crit
|
||||
}
|
||||
|
||||
tg_fan_rpm() {
|
||||
local h
|
||||
for h in "$HWMON_ROOT"/hwmon*/fan1_input; do
|
||||
[ -r "$h" ] && { cat "$h"; return; }
|
||||
done
|
||||
}
|
||||
|
||||
# Force full speed. cooling_device takes the zone away from the kernel governor,
|
||||
# pwm writes the duty cycle after taking manual control of the chip.
|
||||
tg_fan_full() {
|
||||
case "$FAN_MODE" in
|
||||
cooling_device)
|
||||
tg_path_thermal "$FAN_CDEV" || return 1
|
||||
# Take the zone off its governor first, where there is one, and
|
||||
# remember what it was on. Another program may already have taken
|
||||
# it, and handing it back to the configured default rather than to
|
||||
# what was actually there would cut that program's cooling short.
|
||||
if [ -n "$FAN_ZONE" ]; then
|
||||
tg_path_thermal "$FAN_ZONE" || return 1
|
||||
[ -n "$FAN_SAVED_POLICY" ] ||
|
||||
FAN_SAVED_POLICY=$(cat "$FAN_ZONE/policy" 2>/dev/null)
|
||||
echo user_space > "$FAN_ZONE/policy" 2>/dev/null || return 1
|
||||
fi
|
||||
# max_state means maximum cooling whichever way the cooling-levels
|
||||
# in the device tree run, so this mode needs no idea of polarity.
|
||||
tg_fan_max > "$FAN_CDEV/cur_state" 2>/dev/null
|
||||
;;
|
||||
pwm)
|
||||
# Remember what the chip was doing before we take it over. The
|
||||
# release writes this back, which beats asking the admin for a
|
||||
# shell command and survives a daemon restart via the state file.
|
||||
if [ -n "$PWM_ENABLE_PATH" ] && [ -z "$FAN_SAVED_ENABLE" ]; then
|
||||
FAN_SAVED_ENABLE=$(tg_int "$(cat "$PWM_ENABLE_PATH" 2>/dev/null)")
|
||||
fi
|
||||
if [ -n "$PWM_PATH" ] && [ -z "$FAN_SAVED_PWM" ]; then
|
||||
FAN_SAVED_PWM=$(tg_int "$(cat "$PWM_PATH" 2>/dev/null)")
|
||||
fi
|
||||
|
||||
if [ -n "$PWM_ENABLE_PATH" ]; then
|
||||
tg_path_hwmon "$PWM_ENABLE_PATH" || return 1
|
||||
echo 1 > "$PWM_ENABLE_PATH" 2>/dev/null
|
||||
fi
|
||||
if [ -n "$PWM_PATH" ]; then
|
||||
tg_path_hwmon "$PWM_PATH" || return 1
|
||||
echo "$PWM_FULL" > "$PWM_PATH" 2>/dev/null
|
||||
fi
|
||||
;;
|
||||
none) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Hand the fan back. Only called on an explicit reset or a clean stop, never on
|
||||
# the escalation path, so a crash leaves the fan running.
|
||||
# Hands the zone to governor $1 and reads the policy back. The kernel refuses
|
||||
# a governor it was built without, which would leave the zone on user_space
|
||||
# and the fan at full speed with nobody told. step_wise is in every kernel
|
||||
# with thermal support. Without available_policies the name is written as is.
|
||||
tg_fan_zone_policy() {
|
||||
local want="$1" avail got
|
||||
avail=$(cat "$FAN_ZONE/available_policies" 2>/dev/null)
|
||||
if [ -n "$avail" ]; then
|
||||
case " $avail " in
|
||||
*" $want "*) ;;
|
||||
*)
|
||||
tg_log "governor '$want' is not available on this zone, handing it to step_wise" warning
|
||||
want=step_wise ;;
|
||||
esac
|
||||
fi
|
||||
# Braces, so a failed open is as quiet as a failed write.
|
||||
{ echo "$want" > "$FAN_ZONE/policy"; } 2>/dev/null
|
||||
got=$(cat "$FAN_ZONE/policy" 2>/dev/null)
|
||||
[ "$got" = "$want" ] && return 0
|
||||
tg_log "zone still reports policy '${got:-nothing}' after handing it back" crit
|
||||
return 1
|
||||
}
|
||||
|
||||
# Writes $2 to $1 and reads it back; $3 names the value in the log.
|
||||
tg_fan_write_back() {
|
||||
local got
|
||||
{ echo "$2" > "$1"; } 2>/dev/null
|
||||
got=$(tg_int "$(cat "$1" 2>/dev/null)")
|
||||
[ "$got" = "$2" ] && return 0
|
||||
tg_log "$3 reads '${got:-nothing}' after handing it back, wrote '$2'" crit
|
||||
return 1
|
||||
}
|
||||
|
||||
tg_fan_release() {
|
||||
local mode=""
|
||||
case "$FAN_MODE" in
|
||||
cooling_device)
|
||||
# Nothing to hand back where no zone drives the cooling device.
|
||||
[ -n "$FAN_ZONE" ] || return 0
|
||||
tg_path_thermal "$FAN_ZONE" || return 0
|
||||
# Back to whatever held the zone before, which may be another
|
||||
# program's user_space rather than a kernel governor. Only where
|
||||
# nothing was recorded, because the state was lost, does the
|
||||
# configured default get used as the safety net.
|
||||
if [ -n "$FAN_SAVED_POLICY" ]; then
|
||||
tg_fan_zone_policy "$FAN_SAVED_POLICY"
|
||||
else
|
||||
tg_log "no saved fan policy, handing the zone to $FAN_GOVERNOR"
|
||||
tg_fan_zone_policy "$FAN_GOVERNOR"
|
||||
fi
|
||||
FAN_SAVED_POLICY=""
|
||||
;;
|
||||
pwm)
|
||||
# Put back what we found. The fallback 2 means "automatic" on the
|
||||
# usual pwm chips; 0 is never written, because on some of them that
|
||||
# stops the fan, which is the opposite of what a release should do.
|
||||
if [ -n "$PWM_ENABLE_PATH" ] && tg_path_hwmon "$PWM_ENABLE_PATH"; then
|
||||
mode=$FAN_SAVED_ENABLE
|
||||
if [ -z "$mode" ] || [ "$mode" = 0 ]; then
|
||||
tg_log "no saved fan control mode, handing the fan to automatic"
|
||||
mode=2
|
||||
fi
|
||||
tg_fan_write_back "$PWM_ENABLE_PATH" "$mode" "fan control mode"
|
||||
fi
|
||||
# The duty cycle only in manual mode, or where there is no mode to
|
||||
# set. In any other mode the chip drives it: the value saved before
|
||||
# the takeover means nothing, some drivers refuse it with EINVAL,
|
||||
# and reading it back would report the chip's own value as failure.
|
||||
case "$mode" in
|
||||
''|1)
|
||||
if [ -n "$FAN_SAVED_PWM" ] && [ -n "$PWM_PATH" ] && tg_path_hwmon "$PWM_PATH"; then
|
||||
tg_fan_write_back "$PWM_PATH" "$FAN_SAVED_PWM" "fan pwm"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
FAN_SAVED_ENABLE=""; FAN_SAVED_PWM=""
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- temperatures ----------------------------------------------------------
|
||||
|
||||
# Where the SoC sensor lives differs per target: a thermal zone on mediatek and
|
||||
# most ARM boards, a hwmon node on ramips (MT7628) and on x86 with coretemp.
|
||||
# Called when the configured path does not read, so a board nobody configured
|
||||
# still gets watched instead of running blind.
|
||||
#
|
||||
# Prefers a zone or chip whose name looks like the SoC, then falls back to the
|
||||
# first thermal zone, which is the SoC on the boards that have only one.
|
||||
tg_find_cpu_sensor() {
|
||||
local f name
|
||||
|
||||
for f in "$THERMAL_ROOT"/thermal_zone*/temp; do
|
||||
[ -r "$f" ] || continue
|
||||
name=$(cat "${f%/*}/type" 2>/dev/null)
|
||||
case "$name" in
|
||||
*cpu*|*soc*|*tsens*|*pkg*) echo "$f"; return ;;
|
||||
esac
|
||||
done
|
||||
|
||||
for f in "$HWMON_ROOT"/hwmon*/temp1_input; do
|
||||
[ -r "$f" ] || continue
|
||||
name=$(cat "${f%/*}/name" 2>/dev/null)
|
||||
case "$name" in
|
||||
*cpu*|*soc*|*thermal*|coretemp|*7620*|*7628*) echo "$f"; return ;;
|
||||
esac
|
||||
done
|
||||
|
||||
for f in "$THERMAL_ROOT"/thermal_zone*/temp; do
|
||||
[ -r "$f" ] && { echo "$f"; return; }
|
||||
done
|
||||
}
|
||||
|
||||
# True when the board reports no temperature at all. Distinguishes "this path is
|
||||
# wrong" from "there is nothing to read here", which are different problems: the
|
||||
# first is a typo, the second is a fact about the hardware. Several ramips
|
||||
# boards, the GL-MT300N-V2 among them, expose neither a thermal zone nor a
|
||||
# hwmon temperature input.
|
||||
tg_has_any_sensor() {
|
||||
local f
|
||||
for f in "$THERMAL_ROOT"/thermal_zone*/temp "$HWMON_ROOT"/hwmon*/temp*_input; do
|
||||
[ -r "$f" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Sysfs reports millidegrees, some drivers degrees. Anything above 1000 is mC.
|
||||
tg_read_cpu() {
|
||||
local raw
|
||||
tg_path_sensor "$CPU_TEMP_PATH" || { echo ""; return; }
|
||||
raw=$(tg_int "$(cat "$CPU_TEMP_PATH" 2>/dev/null)")
|
||||
[ -n "$raw" ] || { echo ""; return; }
|
||||
[ "$raw" -gt 1000 ] && raw=$((raw / 1000))
|
||||
echo "$raw"
|
||||
}
|
||||
|
||||
tg_modem_port() {
|
||||
local p
|
||||
if [ "$MODEM_AT_PORT" != auto ]; then
|
||||
tg_path_dev "$MODEM_AT_PORT" || return
|
||||
[ -c "$MODEM_AT_PORT" ] && echo "$MODEM_AT_PORT"
|
||||
return
|
||||
fi
|
||||
for p in /dev/ttyUSB2 /dev/ttyUSB1 /dev/ttyUSB3 /dev/mhi_DUN /dev/cdc-wdm0; do
|
||||
[ -c "$p" ] && { echo "$p"; return; }
|
||||
done
|
||||
}
|
||||
|
||||
# Quectel: AT+QTEMP lists named sensors in degrees. Take the hottest one and drop
|
||||
# the -273 placeholders unpopulated sensors report.
|
||||
tg_modem_quectel() {
|
||||
tg_at "$1" 'AT+QTEMP' \
|
||||
| sed -n 's/^+QTEMP:.*,"\(-\{0,1\}[0-9]\{1,3\}\)".*/\1/p' \
|
||||
| awk '$1 > -100 && $1 < 150 && $1 > m { m = $1 } END { if (m != "") print m }'
|
||||
}
|
||||
|
||||
# Fibocom: AT+GTSENRDTEMP=0 answers "<id>,<millidegrees>" per sensor. Only the RF
|
||||
# and modem sensors matter, the rest read 0 while idle.
|
||||
tg_modem_fibocom() {
|
||||
tg_at "$1" 'AT+GTSENRDTEMP=0' | tr -d '\r' \
|
||||
| sed -n 's/^+GTSENRDTEMP: *\(1[01457]\),\([0-9]*\)$/\1 \2/p' \
|
||||
| awk '$2 > 0 && $2 > m { m = $2 } END { if (m > 0) printf "%d", m / 1000 }'
|
||||
}
|
||||
|
||||
# Sets MODEM_VAL and MODEM_SRC. Empty value means "no reading", which the logic
|
||||
# treats as "condition met" so a dead modem never blocks an escalation.
|
||||
tg_read_modem() {
|
||||
local port v rc
|
||||
MODEM_VAL=""; MODEM_SRC=""
|
||||
|
||||
case "$MODEM_SOURCE" in
|
||||
none) return 0 ;;
|
||||
file)
|
||||
tg_path_modem_file "$MODEM_TEMP_FILE" || return 0
|
||||
v=$(tg_int "$(cat "$MODEM_TEMP_FILE" 2>/dev/null)")
|
||||
[ -n "$v" ] && [ "$v" -gt 1000 ] && v=$((v / 1000))
|
||||
[ -n "$v" ] && { MODEM_VAL=$v; MODEM_SRC="file"; }
|
||||
return 0
|
||||
;;
|
||||
command)
|
||||
# An executable from the hook directory, not a string from UCI.
|
||||
[ -x "$HOOK_DIR/modem-temp" ] || return 0
|
||||
v=$({ timeout -k 5 "$AT_TIMEOUT" "$HOOK_DIR/modem-temp"; } 2>/dev/null)
|
||||
rc=$?
|
||||
# Stopped at its time limit: coreutils timeout says 124, or 137 for
|
||||
# the KILL a hook that ignored TERM gets; a BusyBox timeout dies of
|
||||
# the signal itself (143, 137). What such a hook printed before is
|
||||
# no reading.
|
||||
[ "$rc" -eq 124 ] || [ "$rc" -gt 128 ] && return 0
|
||||
v=$(tg_int "$v")
|
||||
[ -n "$v" ] && { MODEM_VAL=$v; MODEM_SRC="command"; }
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
port=$(tg_modem_port)
|
||||
[ -n "$port" ] || return 0
|
||||
|
||||
case "$MODEM_SOURCE" in
|
||||
quectel) v=$(tg_modem_quectel "$port") ;;
|
||||
fibocom) v=$(tg_modem_fibocom "$port") ;;
|
||||
auto)
|
||||
v=$(tg_modem_quectel "$port")
|
||||
[ -n "$v" ] || v=$(tg_modem_fibocom "$port")
|
||||
;;
|
||||
esac
|
||||
|
||||
v=$(tg_int "$v")
|
||||
[ -n "$v" ] && [ "$v" -ge 1 ] && [ "$v" -le 150 ] && { MODEM_VAL=$v; MODEM_SRC="at"; }
|
||||
return 0
|
||||
}
|
||||
|
||||
# Everything else the board reports: Wi-Fi chips, ethernet phys, disks. Printed as
|
||||
# "name=degrees" per line. The source of the processor reading is skipped so it does
|
||||
# not show up twice, and hwmon entries win over thermal zones with the same name.
|
||||
tg_read_extra_sensors() {
|
||||
local f name label raw seen=" " cpu_zone="" cpu_real="" cpu
|
||||
[ "$EXTRA_SENSORS" = 1 ] || return 0
|
||||
|
||||
# The processor is shown on its own, so its sensor is skipped by path. A
|
||||
# thermal zone is also mirrored by the kernel as a hwmon node named after
|
||||
# the zone's type, which the path does not catch: that mirror is skipped
|
||||
# by name. Neither check makes the other redundant.
|
||||
# The loops walk class paths. A cpu_temp_path set by hand in its
|
||||
# /sys/devices form matches none of them, so only then is it resolved and
|
||||
# compared resolved: that costs a readlink per sensor, which the default
|
||||
# and every detected path, all in class form, do not need.
|
||||
case "$CPU_TEMP_PATH" in
|
||||
"$THERMAL_ROOT"/*|"$HWMON_ROOT"/*) ;;
|
||||
*) cpu_real=$(readlink -f "$CPU_TEMP_PATH" 2>/dev/null) ;;
|
||||
esac
|
||||
cpu=${cpu_real:-$CPU_TEMP_PATH}
|
||||
# The kernel names a zone's hwmon mirror after the zone type with "-" turned
|
||||
# into "_", hwmon names carry no hyphen: zone cpu-thermal, mirror
|
||||
# cpu_thermal. So the zone side is normalised, once, and compared as is.
|
||||
case "$cpu" in
|
||||
*/thermal_zone*/temp)
|
||||
cpu_zone=$(cat "${cpu%/temp}/type" 2>/dev/null | tr '-' '_') ;;
|
||||
esac
|
||||
|
||||
for f in "$HWMON_ROOT"/hwmon*/temp*_input; do
|
||||
[ -r "$f" ] || continue
|
||||
[ "$f" = "$CPU_TEMP_PATH" ] && continue
|
||||
[ -n "$cpu_real" ] && [ "$(readlink -f "$f")" = "$cpu_real" ] && continue
|
||||
name=$(cat "${f%/*}/name" 2>/dev/null)
|
||||
[ -n "$name" ] || continue
|
||||
[ -n "$cpu_zone" ] && [ "$name" = "$cpu_zone" ] && continue
|
||||
# a chip with several sensors labels them, use that to tell them apart
|
||||
label=$(cat "${f%_input}_label" 2>/dev/null)
|
||||
[ -n "$label" ] && name="$name $label"
|
||||
case "$seen" in *" $name "*) continue ;; esac
|
||||
raw=$(tg_int "$(cat "$f" 2>/dev/null)")
|
||||
[ -n "$raw" ] || continue
|
||||
[ "$raw" -gt 1000 ] && raw=$((raw / 1000))
|
||||
[ "$raw" -gt -100 ] && [ "$raw" -lt 200 ] || continue
|
||||
seen="$seen$name "
|
||||
printf '%s=%s\n' "$name" "$raw"
|
||||
done
|
||||
|
||||
for f in "$THERMAL_ROOT"/thermal_zone*/temp; do
|
||||
[ -r "$f" ] || continue
|
||||
[ "$f" = "$CPU_TEMP_PATH" ] && continue
|
||||
[ -n "$cpu_real" ] && [ "$(readlink -f "$f")" = "$cpu_real" ] && continue
|
||||
name=$(cat "${f%/*}/type" 2>/dev/null)
|
||||
[ -n "$name" ] || continue
|
||||
case "$seen" in *" $name "*) continue ;; esac
|
||||
raw=$(tg_int "$(cat "$f" 2>/dev/null)")
|
||||
[ -n "$raw" ] || continue
|
||||
[ "$raw" -gt 1000 ] && raw=$((raw / 1000))
|
||||
[ "$raw" -gt -100 ] && [ "$raw" -lt 200 ] || continue
|
||||
seen="$seen$name "
|
||||
printf '%s=%s\n' "$name" "$raw"
|
||||
done
|
||||
|
||||
tg_read_mtwifi
|
||||
}
|
||||
|
||||
# The proprietary MediaTek driver (mt_wifi) registers no hwmon sensor, its
|
||||
# temperature is only reachable through a private ioctl. Same query as
|
||||
# ImmortalWrt's autocore, and like it only for interfaces that are up.
|
||||
# Numbered by position in the list, so a radio that is down leaves a gap
|
||||
# instead of renaming the other one.
|
||||
tg_read_mtwifi() {
|
||||
local ifname flags raw n=-1
|
||||
tg_have iwpriv || return 0
|
||||
for ifname in ra0 rax0 rai0 ray0 rae0 raz0; do
|
||||
n=$((n + 1))
|
||||
[ -r "$NET_ROOT/$ifname/flags" ] || continue
|
||||
flags=$(cat "$NET_ROOT/$ifname/flags" 2>/dev/null)
|
||||
case "$flags" in 0x[0-9a-fA-F]*) ;; *) continue ;; esac
|
||||
[ $((flags & 1)) -eq 1 ] || continue
|
||||
raw=$(tg_mtwifi_temp "$ifname")
|
||||
[ -n "$raw" ] || continue
|
||||
printf 'mtwifi_phy%s=%s\n' "$n" "$raw"
|
||||
done
|
||||
}
|
||||
|
||||
tg_mtwifi_temp() { # $1 interface
|
||||
local out v
|
||||
if tg_have timeout; then
|
||||
out=$(timeout 3 iwpriv "$1" stat 2>/dev/null)
|
||||
else
|
||||
out=$(iwpriv "$1" stat 2>/dev/null)
|
||||
fi
|
||||
v=$(printf '%s\n' "$out" | sed -n 's/^CurrentTemperature *= *\(-\{0,1\}[0-9]\{1,3\}\) *$/\1/p' | head -n 1)
|
||||
v=$(tg_int "$v")
|
||||
[ -n "$v" ] && [ "$v" -gt -100 ] && [ "$v" -lt 200 ] && echo "$v"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- stage actions ---------------------------------------------------------
|
||||
|
||||
# CFUN=4 is flight mode and CFUN=0 minimum function, neither a reset: a reset
|
||||
# while the PCIe link is up can wedge the host port on some boards.
|
||||
tg_action_modem_radio_off() { tg_modem_cfun_set "${MODEM_RADIO_OFF_AT#AT+CFUN=}"; }
|
||||
tg_action_modem_radio_on() { tg_modem_cfun_set 1; }
|
||||
|
||||
# Success is what the modem reports afterwards, not what the command returned:
|
||||
# a collision on the port swallows the command and answers nothing. So the
|
||||
# command is sent again on every attempt.
|
||||
tg_modem_cfun_set() { # $1 level
|
||||
local port got="" i=0
|
||||
port=$(tg_modem_port)
|
||||
[ -n "$port" ] || return 1
|
||||
while [ "$i" -lt 3 ]; do
|
||||
[ "$i" -gt 0 ] && sleep "$CFUN_SETTLE"
|
||||
tg_at "$port" "AT+CFUN=$1" >/dev/null 2>&1
|
||||
got=$(tg_at "$port" 'AT+CFUN?' | tr -d '\r' | sed -n 's/^+CFUN: *\([0-9]\).*/\1/p' | head -n1)
|
||||
[ "$got" = "$1" ] && return 0
|
||||
i=$((i + 1))
|
||||
done
|
||||
tg_log "modem reports CFUN ${got:-nothing} after AT+CFUN=$1" crit
|
||||
return 1
|
||||
}
|
||||
|
||||
# UCI section names only: the name ends up as an ifdown argument and in a ubus
|
||||
# object path.
|
||||
tg_iface_name_ok() {
|
||||
case "$1" in
|
||||
''|-*|*[!A-Za-z0-9_]*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# "true" or "false" as netifd reports it, empty for an interface it does not know.
|
||||
tg_iface_up_state() {
|
||||
timeout "$UBUS_TIMEOUT" ubus call "network.interface.$1" status 2>/dev/null \
|
||||
| jsonfilter -e '@.up' 2>/dev/null
|
||||
}
|
||||
|
||||
tg_action_interface_down() { tg_iface_set "${ACTION_INTERFACE:-$UPLINK_INTERFACE}" false; }
|
||||
tg_action_interface_up() { tg_iface_set "${ACTION_INTERFACE:-$UPLINK_INTERFACE}" true; }
|
||||
|
||||
# ifdown and ifup return before netifd has acted, so wait for its report.
|
||||
tg_iface_set() { # $1 interface, $2 true or false
|
||||
local i=0 got=""
|
||||
if ! tg_iface_name_ok "$1"; then
|
||||
tg_log "interface action has no usable interface, set action_interface or uplink_interface" warning
|
||||
return 1
|
||||
fi
|
||||
tg_have ifdown || return 1
|
||||
if [ "$2" = false ]; then
|
||||
timeout "$CMD_TIMEOUT" ifdown "$1" >/dev/null 2>&1
|
||||
else
|
||||
timeout "$CMD_TIMEOUT" ifup "$1" >/dev/null 2>&1
|
||||
fi
|
||||
while [ "$i" -lt 3 ]; do
|
||||
[ "$i" -gt 0 ] && sleep "$CFUN_SETTLE"
|
||||
got=$(tg_iface_up_state "$1")
|
||||
[ "$got" = "$2" ] && return 0
|
||||
i=$((i + 1))
|
||||
done
|
||||
tg_log "interface $1 reports up=${got:-unknown} after if$([ "$2" = false ] && echo down || echo up)" crit
|
||||
return 1
|
||||
}
|
||||
|
||||
tg_action_wifi_off() { tg_wifi_set down; }
|
||||
tg_action_wifi_on() { tg_wifi_set up; }
|
||||
|
||||
# netifd's view of the radios that are not disabled, as "<radios> <up>",
|
||||
# nothing when it does not answer. One filter for both counts, so they come
|
||||
# from the same radios. Written as !(=true), because != does not match a radio
|
||||
# without a "disabled" key at all.
|
||||
tg_wifi_radios() {
|
||||
local json v radios=0 up=0
|
||||
json=$(timeout "$UBUS_TIMEOUT" ubus call network.wireless status 2>/dev/null) || return 0
|
||||
[ -n "$json" ] || return 0
|
||||
for v in $(printf '%s\n' "$json" | jsonfilter -e '@[!(@.disabled=true)].up' 2>/dev/null); do
|
||||
radios=$((radios + 1))
|
||||
[ "$v" = true ] && up=$((up + 1))
|
||||
done
|
||||
echo "$radios $up"
|
||||
}
|
||||
|
||||
# Like the modem, the command goes out again on every attempt, and success is
|
||||
# what netifd reports afterwards. Starting hostapd per radio takes longer than
|
||||
# a modem answers, hence its own settle time and more attempts.
|
||||
tg_wifi_set() { # $1 down or up
|
||||
local i=0 state radios="" up=""
|
||||
if ! tg_have wifi; then
|
||||
tg_log "no wifi command, cannot switch Wi-Fi $1" warning
|
||||
return 1
|
||||
fi
|
||||
while [ "$i" -lt "$WIFI_TRIES" ]; do
|
||||
timeout "$CMD_TIMEOUT" wifi "$1" >/dev/null 2>&1
|
||||
[ "$WIFI_SETTLE" -gt 0 ] && sleep "$WIFI_SETTLE"
|
||||
state=$(tg_wifi_radios)
|
||||
if [ -n "$state" ]; then
|
||||
radios=${state%% *}; up=${state#* }
|
||||
# Nothing to switch off is not a success: the stage would claim an
|
||||
# effect it never had. Nothing to bring back is fine, a reset must
|
||||
# not fail on it.
|
||||
if [ "$radios" -eq 0 ]; then
|
||||
[ "$1" = up ] && return 0
|
||||
tg_log "netifd manages no Wi-Fi radio, wifi_off switched nothing off" crit
|
||||
return 1
|
||||
fi
|
||||
[ "$1" = down ] && [ "$up" -eq 0 ] && return 0
|
||||
[ "$1" = up ] && [ "$up" -ge "$radios" ] && return 0
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
if [ -z "$state" ]; then
|
||||
tg_log "netifd reports no Wi-Fi status after wifi $1" crit
|
||||
else
|
||||
tg_log "Wi-Fi reports $up of $radios radio(s) up after wifi $1" crit
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---- uplink ----------------------------------------------------------------
|
||||
|
||||
# l3 device of the modem interface, so notifications can tell "there is another
|
||||
# way out" from "the only way out is the thing we are about to switch off".
|
||||
tg_modem_l3dev() {
|
||||
[ -n "$UPLINK_INTERFACE" ] || return 0
|
||||
timeout "$UBUS_TIMEOUT" ubus call "network.interface.$UPLINK_INTERFACE" status 2>/dev/null \
|
||||
| jsonfilter -e '@.l3_device' 2>/dev/null
|
||||
}
|
||||
|
||||
tg_default_devs() {
|
||||
tg_have ip || return 0
|
||||
{ ip -4 route show default; ip -6 route show default; } 2>/dev/null \
|
||||
| sed -n 's/.* dev \([^ ]*\).*/\1/p'
|
||||
}
|
||||
|
||||
# 0 = there is a default route that does not run through the modem.
|
||||
# Without ip nothing can tell whether a route exists, and a message that waits
|
||||
# for one nobody can see is never sent. Unknown therefore means send now.
|
||||
tg_uplink_unknown() {
|
||||
tg_have ip && return 1
|
||||
[ "$IP_TOLD" = 1 ] ||
|
||||
tg_cfg_log "no ip command, cannot tell whether another uplink exists, sending at once"
|
||||
[ "$DAEMON" = 1 ] && IP_TOLD=1
|
||||
return 0
|
||||
}
|
||||
|
||||
tg_alt_uplink() {
|
||||
local md dev
|
||||
tg_uplink_unknown && return 0
|
||||
md=$(tg_modem_l3dev)
|
||||
for dev in $(tg_default_devs); do
|
||||
[ -n "$md" ] && [ "$dev" = "$md" ] && continue
|
||||
return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
tg_any_uplink() { tg_uplink_unknown || [ -n "$(tg_default_devs)" ]; }
|
||||
@@ -0,0 +1,344 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
# Trip management for thermal-guard: lowers the active trips of the fan's thermal
|
||||
# zone while the modem runs hotter than the CPU, so the kernel spins the fan up
|
||||
# for a heat source it cannot see. Sourced by /usr/sbin/thermal-guard.
|
||||
# shellcheck shell=busybox
|
||||
# shellcheck disable=SC2034 # TRIP_* are read by the caller and the status file
|
||||
#
|
||||
# Trips only ever go below the baseline, never above it. A wrong modem reading
|
||||
# can at worst make the box loud, and a dead daemon leaves the trips where they
|
||||
# were, which is the same safe direction. hot and critical are never written.
|
||||
#
|
||||
# All temperatures in here are millidegrees, the unit sysfs uses. Only UCI and
|
||||
# the log speak whole degrees.
|
||||
|
||||
# Readings the effective delta is the maximum of. Lowering acts on one reading,
|
||||
# raising the trips again needs this many in a row.
|
||||
TRIP_WINDOW_N=3
|
||||
TRIP_DELTA_STEP=5000
|
||||
TRIP_DELTA_MAX=40000
|
||||
|
||||
# Runtime state, rebuilt from sysfs by every daemon start.
|
||||
TRIP_STATE=off; TRIP_REASON=""; TRIP_ZONE=""; TRIP_IDX=""; TRIP_LIMIT=""
|
||||
TRIP_BASE=""; TRIP_DT=""; TRIP_NOW=""; TRIP_WIN=""; TRIP_DELTA=0
|
||||
TRIP_WAS_ACTIVE=0; TRIP_DT_CHANGED=0
|
||||
# What was last said per message, each cleared when its cause is gone.
|
||||
TRIP_TOLD_FIND=""; TRIP_TOLD_VALID=""; TRIP_TOLD_DT=""; TRIP_TOLD_WRITE=""
|
||||
# Set once a failed write of trip_base has been reported, until one succeeds.
|
||||
TRIP_BASE_TOLD=0
|
||||
|
||||
# $1 cpu, $2 modem, $3 offset, all whole degrees. Prints the delta in
|
||||
# millidegrees, or nothing when a reading is missing: no reading is not the same
|
||||
# as a cool modem, and must not pull the trips back up.
|
||||
tg_trip_delta() {
|
||||
local cpu modem off raw
|
||||
cpu=$(tg_int "$1"); modem=$(tg_int "$2"); off=$(tg_int "$3")
|
||||
[ -n "$cpu" ] && [ -n "$modem" ] && [ -n "$off" ] || return 0
|
||||
raw=$(( (modem - off - cpu) * 1000 ))
|
||||
if [ "$raw" -le 0 ]; then
|
||||
echo 0
|
||||
return 0
|
||||
fi
|
||||
raw=$(( (raw + TRIP_DELTA_STEP - 1) / TRIP_DELTA_STEP * TRIP_DELTA_STEP ))
|
||||
[ "$raw" -gt "$TRIP_DELTA_MAX" ] && raw=$TRIP_DELTA_MAX
|
||||
echo "$raw"
|
||||
}
|
||||
|
||||
# Keep the last TRIP_WINDOW_N deltas in TRIP_WIN.
|
||||
tg_trip_push() {
|
||||
local d out=""
|
||||
# shellcheck disable=SC2086 # word splitting of the window is intended
|
||||
set -- $TRIP_WIN "$1"
|
||||
while [ "$#" -gt "$TRIP_WINDOW_N" ]; do shift; done
|
||||
for d in "$@"; do
|
||||
out="$out${out:+ }$d"
|
||||
done
|
||||
TRIP_WIN=$out
|
||||
}
|
||||
|
||||
tg_trip_window_max() {
|
||||
local d m=0
|
||||
for d in $TRIP_WIN; do [ "$d" -gt "$m" ] && m=$d; done
|
||||
echo "$m"
|
||||
}
|
||||
|
||||
# $1 space separated list in whole degrees, $2 expected count, $3 limit in
|
||||
# millidegrees (lowest hot or critical trip, empty if the zone has none).
|
||||
tg_trip_valid() {
|
||||
local v prev="" n=0
|
||||
for v in $1; do
|
||||
[ -n "$(tg_int "$v")" ] || return 1
|
||||
[ "$v" -ge 20 ] && [ "$v" -le 95 ] || return 1
|
||||
[ -z "$prev" ] || [ "$v" -gt "$prev" ] || return 1
|
||||
[ -z "$3" ] || [ $((v * 1000)) -lt "$3" ] || return 1
|
||||
prev=$v; n=$((n + 1))
|
||||
done
|
||||
[ "$n" -gt 0 ] && [ "$n" -eq "$2" ]
|
||||
}
|
||||
|
||||
# The fan's zone and the active trips its cooling device is bound to, ordered
|
||||
# by temperature. Sets TRIP_ZONE, TRIP_IDX, TRIP_LIMIT or TRIP_REASON, a fixed
|
||||
# code the web interface translates.
|
||||
tg_trip_find() {
|
||||
local cdev z c idx t lim=""
|
||||
TRIP_ZONE=""; TRIP_IDX=""; TRIP_LIMIT=""
|
||||
cdev=$(tg_fan_find_cdev)
|
||||
[ -n "$cdev" ] || { TRIP_REASON=no_cdev; return 1; }
|
||||
|
||||
z=$FAN_ZONE
|
||||
[ -n "$z" ] || z=$(tg_fan_find_zone "$cdev")
|
||||
[ -n "$z" ] || { TRIP_REASON=no_zone; return 1; }
|
||||
tg_path_thermal "$z" || { TRIP_REASON=zone_path; return 1; }
|
||||
|
||||
# One "temp index" line per active trip bound to the fan, ordered by
|
||||
# temperature. This runs in a subshell, so nothing in it sets a global.
|
||||
TRIP_IDX=$(
|
||||
for c in "$z"/cdev*; do
|
||||
case "${c##*/}" in cdev*[!0-9]*) continue ;; esac
|
||||
[ "$(readlink -f "$c")" = "$(readlink -f "$cdev")" ] || continue
|
||||
idx=$(tg_int "$(cat "${c}_trip_point" 2>/dev/null)")
|
||||
[ -n "$idx" ] || continue
|
||||
[ "$(cat "$z/trip_point_${idx}_type" 2>/dev/null)" = active ] || continue
|
||||
t=$(tg_int "$(cat "$z/trip_point_${idx}_temp" 2>/dev/null)")
|
||||
[ -n "$t" ] || continue
|
||||
printf '%s %s\n' "$t" "$idx"
|
||||
done | sort -n | uniq | while read -r t idx; do printf '%s ' "$idx"; done
|
||||
)
|
||||
TRIP_IDX=${TRIP_IDX% }
|
||||
[ -n "$TRIP_IDX" ] || { TRIP_REASON=no_active_trip; return 1; }
|
||||
|
||||
for idx in $TRIP_IDX; do
|
||||
[ -w "$z/trip_point_${idx}_temp" ] || { TRIP_REASON=not_writable; return 1; }
|
||||
done
|
||||
# hot and critical bound the baseline from above
|
||||
for c in "$z"/trip_point_*_type; do
|
||||
case "$(cat "$c" 2>/dev/null)" in
|
||||
hot|critical)
|
||||
t=$(tg_int "$(cat "${c%_type}_temp" 2>/dev/null)")
|
||||
[ -n "$t" ] && { [ -z "$lim" ] || [ "$t" -lt "$lim" ]; } && lim=$t
|
||||
;;
|
||||
esac
|
||||
done
|
||||
TRIP_ZONE=$z; TRIP_LIMIT=$lim
|
||||
return 0
|
||||
}
|
||||
|
||||
tg_trip_read() { # prints the managed trips in millidegrees
|
||||
local idx out=""
|
||||
for idx in $TRIP_IDX; do
|
||||
out="$out${out:+ }$(tg_int "$(cat "$TRIP_ZONE/trip_point_${idx}_temp" 2>/dev/null)")"
|
||||
done
|
||||
echo "$out"
|
||||
}
|
||||
|
||||
# $1 target list in millidegrees, same order as TRIP_IDX. Lowering writes the
|
||||
# lowest trip first and raising the highest first, so the trips stay ascending
|
||||
# at every point in between. Every write re-checks the type: a trip that is not
|
||||
# active is never touched, whatever the index says.
|
||||
tg_trip_write() {
|
||||
local want="$1" now p idx w got fail=0
|
||||
now=$(tg_trip_read)
|
||||
[ "$now" = "$want" ] && return 0
|
||||
for p in $(tg_trip_pairs "$want" "$now"); do
|
||||
idx=${p%%:*}; w=${p#*:}
|
||||
[ "$(cat "$TRIP_ZONE/trip_point_${idx}_type" 2>/dev/null)" = active ] || {
|
||||
tg_log "trip $idx is no longer active, not writing it" warning; fail=1; continue; }
|
||||
echo "$w" > "$TRIP_ZONE/trip_point_${idx}_temp" 2>/dev/null
|
||||
got=$(tg_int "$(cat "$TRIP_ZONE/trip_point_${idx}_temp" 2>/dev/null)")
|
||||
[ "$got" = "$w" ] || fail=1
|
||||
done
|
||||
TRIP_NOW=$(tg_trip_read)
|
||||
if [ "$fail" = 1 ]; then
|
||||
tg_trip_tell "write" "could not set trips to $(tg_trip_deg "$want") C, they read $(tg_trip_deg "$TRIP_NOW") C" warning
|
||||
return 1
|
||||
fi
|
||||
TRIP_TOLD_WRITE=""
|
||||
tg_log "trips set to $(tg_trip_deg "$want") C (delta $((TRIP_DELTA / 1000)) K)"
|
||||
}
|
||||
|
||||
# $1 target, $2 current, both in TRIP_IDX order. Prints idx:value in the order
|
||||
# they have to be written.
|
||||
tg_trip_pairs() {
|
||||
local i=0 idx p pairs=""
|
||||
for idx in $TRIP_IDX; do
|
||||
i=$((i + 1))
|
||||
p="$idx:$(tg_trip_nth "$i" "$1")"
|
||||
if [ -n "$2" ] && [ "${1%% *}" -lt "${2%% *}" ]; then
|
||||
pairs="$pairs${pairs:+ }$p"
|
||||
else
|
||||
pairs="$p${pairs:+ }$pairs"
|
||||
fi
|
||||
done
|
||||
echo "$pairs"
|
||||
}
|
||||
|
||||
tg_trip_nth() { # $1 position, $2 space separated list
|
||||
local n=0 v
|
||||
for v in $2; do
|
||||
n=$((n + 1))
|
||||
[ "$n" = "$1" ] && { echo "$v"; return 0; }
|
||||
done
|
||||
}
|
||||
|
||||
tg_trip_count() { # number of words in $1
|
||||
local n=0 v
|
||||
for v in $1; do n=$((n + 1)); done
|
||||
echo "$n"
|
||||
}
|
||||
|
||||
tg_trip_deg() { # millidegree list to whole degrees, for the log
|
||||
local v out=""
|
||||
for v in $1; do out="$out${out:+/}$((v / 1000))"; done
|
||||
echo "$out"
|
||||
}
|
||||
|
||||
# Say a thing once until it changes, the daemon runs every interval.
|
||||
tg_trip_tell() { # $1 find, valid, dt or write, $2 text, $3 priority as for tg_cfg_log
|
||||
local told
|
||||
case "$1" in
|
||||
find) told=$TRIP_TOLD_FIND ;;
|
||||
valid) told=$TRIP_TOLD_VALID ;;
|
||||
dt) told=$TRIP_TOLD_DT ;;
|
||||
write) told=$TRIP_TOLD_WRITE ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
[ "$told" = "$2" ] && return 0
|
||||
case "$1" in
|
||||
find) TRIP_TOLD_FIND=$2 ;;
|
||||
valid) TRIP_TOLD_VALID=$2 ;;
|
||||
dt) TRIP_TOLD_DT=$2 ;;
|
||||
write) TRIP_TOLD_WRITE=$2 ;;
|
||||
esac
|
||||
tg_cfg_log "$2" "${3:-warning}"
|
||||
}
|
||||
|
||||
# Device tree values of this boot: read once per boot, before anything wrote.
|
||||
tg_trip_dt_load() {
|
||||
local f="$STATE_DIR/trip_dt"
|
||||
if [ -r "$f" ]; then
|
||||
TRIP_DT=$(cat "$f")
|
||||
else
|
||||
TRIP_DT=$(tg_trip_read)
|
||||
mkdir -p "$STATE_DIR"
|
||||
echo "$TRIP_DT" > "$f"
|
||||
fi
|
||||
}
|
||||
|
||||
# trip_base holds the device tree set, in whole degrees, that the operator's
|
||||
# trip_active was made for. It lives on the overlay because a sysupgrade is
|
||||
# exactly when the device tree changes. Without trip_active it means nothing,
|
||||
# so it is neither read nor written then.
|
||||
tg_trip_base_save() { # $1 whole degrees, space separated
|
||||
local f="$PERSIST_DIR/trip_base"
|
||||
mkdir -p "$PERSIST_DIR" 2>/dev/null &&
|
||||
echo "$1" > "$f.tmp" 2>/dev/null && chmod 644 "$f.tmp" &&
|
||||
mv "$f.tmp" "$f" 2>/dev/null || return 1
|
||||
sync
|
||||
}
|
||||
|
||||
tg_trip_base_read() {
|
||||
local v
|
||||
v=$(cat "$PERSIST_DIR/trip_base" 2>/dev/null)
|
||||
case "$v" in
|
||||
*[!0-9\ ]*) echo "" ;;
|
||||
*) echo "$v" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Sets TRIP_DT_CHANGED when the device tree set of this boot is not the one
|
||||
# trip_base names. The first cycle with trip_active records it.
|
||||
tg_trip_base_check() {
|
||||
local now base msg
|
||||
now=$(tg_trip_deg "$TRIP_DT" | tr '/' ' ')
|
||||
if [ ! -e "$PERSIST_DIR/trip_base" ]; then
|
||||
[ "$DRY" = 1 ] && return 0
|
||||
if tg_trip_base_save "$now"; then
|
||||
TRIP_BASE_TOLD=0
|
||||
return 0
|
||||
fi
|
||||
[ "$TRIP_BASE_TOLD" = 1 ] && return 0
|
||||
tg_cfg_log "could not write $PERSIST_DIR/trip_base, a new firmware will not be noticed"
|
||||
[ "$DAEMON" = 1 ] && TRIP_BASE_TOLD=1
|
||||
return 0
|
||||
fi
|
||||
base=$(tg_trip_base_read)
|
||||
[ -n "$base" ] && [ "$base" != "$now" ] || return 0
|
||||
TRIP_DT_CHANGED=1
|
||||
msg="device tree trips are now $(tg_trip_deg "$TRIP_DT") C,"
|
||||
tg_trip_tell dt "$msg trip_active was set for $(echo "$base" | tr ' ' '/') C" notice
|
||||
}
|
||||
|
||||
# One cycle. $1 cpu, $2 modem in whole degrees.
|
||||
tg_trip_cycle() {
|
||||
local cpu="$1" modem="$2" d want="" b
|
||||
if [ "$TRIP_BOOST" = off ] && [ -z "$TRIP_ACTIVE" ]; then
|
||||
TRIP_STATE=off; TRIP_REASON=""
|
||||
TRIP_TOLD_FIND=""; TRIP_TOLD_VALID=""; TRIP_TOLD_DT=""
|
||||
# Switched off while running: hand the zone back its device tree set
|
||||
# once, then leave it alone for good.
|
||||
if [ "$TRIP_WAS_ACTIVE" = 1 ] && [ -n "$TRIP_ZONE" ] && [ -n "$TRIP_DT" ]; then
|
||||
TRIP_DELTA=0
|
||||
tg_trip_write "$TRIP_DT"
|
||||
TRIP_WAS_ACTIVE=0; TRIP_WIN=""
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! tg_trip_find; then
|
||||
TRIP_STATE=inactive
|
||||
tg_trip_tell find "trip management inactive ($TRIP_REASON), zone ${TRIP_ZONE:-none}"
|
||||
return 0
|
||||
fi
|
||||
TRIP_TOLD_FIND=""
|
||||
[ -n "$TRIP_DT" ] || tg_trip_dt_load
|
||||
TRIP_NOW=$(tg_trip_read)
|
||||
|
||||
# Baseline: the operator's list when it holds up, the device tree otherwise
|
||||
if [ -n "$TRIP_ACTIVE" ] &&
|
||||
tg_trip_valid "$TRIP_ACTIVE" "$(tg_trip_count "$TRIP_IDX")" "$TRIP_LIMIT"; then
|
||||
TRIP_BASE=""
|
||||
for b in $TRIP_ACTIVE; do TRIP_BASE="$TRIP_BASE${TRIP_BASE:+ }$((b * 1000))"; done
|
||||
TRIP_TOLD_VALID=""
|
||||
else
|
||||
if [ -n "$TRIP_ACTIVE" ]; then
|
||||
tg_trip_tell valid "rejecting trip_active '$TRIP_ACTIVE', using the device tree values"
|
||||
else
|
||||
TRIP_TOLD_VALID=""
|
||||
fi
|
||||
TRIP_BASE=$TRIP_DT
|
||||
fi
|
||||
|
||||
TRIP_DT_CHANGED=0
|
||||
[ -n "$TRIP_ACTIVE" ] && tg_trip_base_check
|
||||
[ "$TRIP_DT_CHANGED" = 1 ] || TRIP_TOLD_DT=""
|
||||
|
||||
if [ "$TRIP_BOOST" = modem ]; then
|
||||
d=$(tg_trip_delta "$cpu" "$modem" "$TRIP_BOOST_OFFSET")
|
||||
[ -n "$d" ] && tg_trip_push "$d"
|
||||
d=$(tg_trip_window_max)
|
||||
# While a stage is in force the trips do not come back up
|
||||
[ "$STAGE" -ge 0 ] && [ "$d" -lt "$TRIP_DELTA" ] && d=$TRIP_DELTA
|
||||
else
|
||||
d=0; TRIP_WIN=""
|
||||
fi
|
||||
TRIP_DELTA=$d
|
||||
|
||||
for b in $TRIP_BASE; do want="$want${want:+ }$((b - d))"; done
|
||||
TRIP_STATE=active; TRIP_REASON=""; TRIP_WAS_ACTIVE=1
|
||||
[ "$DRY" = 1 ] && return 0
|
||||
tg_trip_write "$want"
|
||||
}
|
||||
|
||||
# cooling-levels of the pwm-fan from the device tree, for display only. The
|
||||
# driver reads them once at probe and exposes no sysfs attribute for them.
|
||||
# Big-endian 32 bit cells; BusyBox on the targets has hexdump but no od.
|
||||
tg_fan_levels() {
|
||||
local f
|
||||
for f in "$DT_ROOT"/*/cooling-levels "$DT_ROOT"/*/*/cooling-levels; do
|
||||
[ -r "$f" ] || continue
|
||||
tr '\0' '\n' < "${f%/*}/compatible" 2>/dev/null | grep -qx pwm-fan || continue
|
||||
hexdump -v -e '4/1 "%u " "\n"' "$f" 2>/dev/null |
|
||||
awk 'NF == 4 { printf "%s%d", s, $1 * 16777216 + $2 * 65536 + $3 * 256 + $4; s = " " }'
|
||||
return 0
|
||||
done
|
||||
}
|
||||
Reference in new issue
Block a user