feat: 添加 poweroff/temp-status/zram-swap 包

- luci-app-poweroffdevice: LuCI 软关机按钮(来自 sirpdboy)
- luci-app-temp-status: RK3588 实时温度监控(CPU/GPU/NPU)
- zram-swap: 压缩交换内存,Docker 多容器时缓解 OOM
This commit is contained in:
xiexiaobao 2026-07-08 11:05:06 +08:00
parent b1d30a8e62
commit 8a5a616718
15 changed files with 650 additions and 0 deletions

View File

@ -0,0 +1,19 @@
#
# provides Web UI to shut down (power off) your device.
# Copyright (C) 2022-2026 sirpdboy <herboy2008@gmail.com>
# This is free software, licensed under the GNU General Public License v3.
include $(TOPDIR)/rules.mk
NAME:=poweroffdevice
PKG_NAME:=luci-app-$(NAME)
LUCI_TITLE:=LuCI support for poweroffdevice Router
LUCI_DESCRIPTION:=provides Web UI to shut down (power off) your device.
LUCI_PKGARCH:=all
PKG_VERSION:=2
PKG_RELEASE:=7
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,86 @@
'use strict';
'require view';
'require ui';
'require fs';
return view.extend({
render: function() {
return E([
E('div', { class: 'cbi-section' }, [
E('h2', { class: 'cbi-section-title' }, _('PowerOff')),
E('div', { class: 'cbi-section-descr' }, _('Turn off the power to the device you are using')),
E('hr', { style: 'margin: 15px 0' }),
E('div', { style: ' padding: 20px 0' }, [
E('button', {
class: 'btn cbi-button cbi-button-negative important',
style: 'padding: 0.5rem 1rem;font-size: 1.2em;font-weight: bold;border-radius: 4px;box-shadow: 0 2px 5px rgba(0,0,0,0.2);',
click: ui.createHandlerFn(this, 'handlePowerOff')
}, _('⚠️ Perform Power Off'))
])
])
]);
},
handlePowerOff: function() {
return ui.showModal(_('⚠️ PowerOff Device'),
E('div', { style: 'min-width: 350px' }, [
E('div', { class: 'alert-message warning', style: 'margin: 0 0 15px 0; padding: 10px; background: #f0ad4e' }, [
E('h4', { style: 'margin: 0 0 5px 0; ' }, _('Warning!')),
E('p', { style: 'margin: 0; ' }, _('This action will immediately turn off the power to your device. Make sure to save all your work before proceeding.'))
]),
E('div', { style: ' border-radius: 4px; padding: 15px; margin-bottom: 15px' }, [
E('p', { style: 'margin: 0 0 10px 0; font-weight: bold' }, _('Before you continue:')),
E('ul', { style: 'margin: 0; padding-left: 20px' }, [
E('li', { style: 'margin-bottom: 5px' }, _('Save all unsaved settings')),
E('li', { style: 'margin-bottom: 5px' }, _('Check if other users are active')),
E('li', _('The device needs to be manually turned on'))
])
]),
E('div', { class: 'right', style: 'margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd' }, [
E('button', {
'class': 'btn cbi-button cbi-button-apply',
'style': 'margin-right: 10px; ',
'click': ui.hideModal
}, _('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-negative',
'style': ' font-weight: bold;',
'click': ui.createHandlerFn(this, function() {
ui.hideModal();
// Show powering off modal
ui.showModal(_('Powering Off'),
E('div', { style: 'text-align: center; padding: 30px; min-width: 300px' }, [
E('div', {
class: 'spinning',
style: 'font-size: 48px; margin-bottom: 20px; color: #f0ad4e;'
}, '⚡'),
E('h3', { style: 'margin: 0 0 15px 0' }, _('Powering off device...')),
E('p', { style: 'color: #666; margin: 0' },
_('The device should power off within a few seconds. If the device remains on, you may need to perform a manual shutdown.'))
])
);
// Execute poweroff command
return fs.exec('/sbin/poweroff').catch(function(e) {
ui.hideModal();
ui.addNotification(null,
E('div', { style: 'padding: 10px' }, [
E('h4', { style: 'color: #d9534f; margin: 0 0 5px 0' }, _('Error!')),
E('p', { style: 'margin: 0' }, e.message)
])
);
});
})
}, _('Confirm Power Off'))
])
])
);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,38 @@
#
# provides Web UI to shut down (power off) your device.
# routers are listed at https://github.com/sirpdboy/luci-app-poweroffdevice
# This is free software, licensed under the GNU General Public License v3.
msgid "PowerOff"
msgstr "关机"
msgid "PowerOff Device"
msgstr "设备关机"
msgid "Perform Power Off"
msgstr "执行关机"
msgid "Turn off the power to the device you are using"
msgstr "关闭您正在使用的设备的电源"
msgid "WARNING: Power off might result in a reboot on a device which not support power off."
msgstr "警告:对于不支持关机的设备,关机可能会导致重新启动."
msgid "PowerOffing..."
msgstr "正在关机..."
msgid "Device shutdown complete!"
msgstr "设备关机完毕!"
msgid "PowerOff failed"
msgstr "关机失败"
msgid "The device is shutting down..."
msgstr "设备正在关闭..."
msgid "The device may have powered off. If not, check manually."
msgstr "设备已断电。如果没有,请手动操作。"
msgid "Confirm"
msgstr "确认"

View File

@ -0,0 +1,49 @@
#
# provides Web UI to shut down (power off) your device.
# routers are listed at https://github.com/sirpdboy/luci-app-poweroffdevice
# This is free software, licensed under the GNU General Public License v3.
msgid "PowerOff"
msgstr "关机"
msgid "⚠️ PowerOff Device"
msgstr "⚠️ 设备关机"
msgid "⚠️ Perform Power Off"
msgstr "⚠️ 执行关机"
msgid "Turn off the power to the device you are using"
msgstr "关闭您正在使用的设备的电源"
msgid "This action will immediately turn off the power to your device. Make sure to save all your work before proceeding."
msgstr "此操作将立即关闭设备的电源。在继续之前,请确保保存所有工作"
msgid "Powering off device..."
msgstr "正在关机..."
msgid "Confirm Power Off"
msgstr "确认关机"
msgid "Warning!"
msgstr "警告"
msgid "Before you continue:"
msgstr "继续关机之前:"
msgid "Save all unsaved settings"
msgstr "保存所有未保存的设置"
msgid "Check if other users are active"
msgstr "检查其他用户是否处于活动状态"
msgid "The device needs to be manually turned on"
msgstr "设备需要手动开机"
msgid "Powering Off"
msgstr "关机中"
msgid "The device should power off within a few seconds. If the device remains on, you may need to perform a manual shutdown."
msgstr "设备应在几秒钟内关闭。如果设备保持开启状态,您可能需要执行手动关机"

View File

@ -0,0 +1,10 @@
{
"admin/system/poweroffdevice": {
"title": "PowerOff",
"order": 99,
"action": {
"type": "view",
"path": "system/poweroffdevice"
}
}
}

View File

@ -0,0 +1,24 @@
{
"luci-app-poweroffdevice": {
"description": "Grant UCI access for luci-app-poweroffdevice",
"read": {
"ubus": {
"system": [ "info" ],
},
"file": {
"/sbin/poweroff": [ "exec" ],
}
},
"write": {
"file": {
"/sbin/poweroff": [ "exec" ]
},
"ubus": {
"file": [ "exec" ],
"system": [ "poweroff" ]
}
}
}
}

View File

@ -0,0 +1,16 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for temperature status
LUCI_DESCRIPTION:=Show CPU/GPU/NPU temperature from thermal zones
LUCI_DEPENDS:=
LUCI_PKGARCH:=all
PKG_VERSION:=1
PKG_RELEASE:=1
define Package/luci-app-temp-status/conffiles
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,102 @@
'use strict';
'require view';
'require form';
'require poll';
'require fs';
'require ui';
function renderTempRow(zone, type, temp) {
var celsius = (temp / 1000).toFixed(1);
var color = '#4CAF50';
if (celsius > 75) color = '#FF9800';
if (celsius > 85) color = '#f44336';
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, [ type ]),
E('td', { 'class': 'td left' }, [ zone ]),
E('td', { 'class': 'td left', 'style': 'color:' + color + ';font-weight:bold' }, [ celsius + ' °C' ])
]);
}
function renderTempTable(zones) {
var table = E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, [ _('Type') ]),
E('th', { 'class': 'th' }, [ _('Zone') ]),
E('th', { 'class': 'th' }, [ _('Temperature') ])
])
]);
var rows = zones.sort(function(a, b) {
return a.zone.localeCompare(b.zone);
});
for (var i = 0; i < rows.length; i++) {
table.appendChild(renderTempRow(rows[i].zone, rows[i].type, rows[i].temp));
}
return table;
}
return view.extend({
load: function() {
return fs.list('/sys/class/thermal/').then(function(entries) {
var promises = [];
for (var i = 0; i < entries.length; i++) {
if (entries[i].name.indexOf('thermal_zone') === 0) {
promises.push(
fs.read('/sys/class/thermal/' + entries[i].name + '/type').then(function(zone, data) {
var type = data.trim();
return fs.read('/sys/class/thermal/' + zone + '/temp').then(function(temp) {
return { zone: zone, type: type, temp: parseInt(temp.trim()) };
});
}.bind(null, entries[i].name))
);
}
}
return Promise.all(promises);
});
},
render: function(zones) {
var viewBody = E([], [
E('div', { 'class': 'cbi-section', 'id': 'temp-status' }, [
E('h3', { 'name': 'content' }, [ _('Temperature Status') ]),
E('div', { 'class': 'cbi-section-descr' }, [ _('Real-time temperature of CPU/GPU/NPU thermal zones on Rockchip RK3588.') ]),
renderTempTable(zones),
E('div', { 'style': 'margin-top:10px;color:#666;font-size:90%' }, [
_('Note: Temperatures above 75°C are shown in orange, above 85°C in red.')
])
])
]);
poll.add(function() {
return fs.list('/sys/class/thermal/').then(function(entries) {
var promises = [];
for (var i = 0; i < entries.length; i++) {
if (entries[i].name.indexOf('thermal_zone') === 0) {
promises.push(
fs.read('/sys/class/thermal/' + entries[i].name + '/type').then(function(zone, data) {
var type = data.trim();
return fs.read('/sys/class/thermal/' + zone + '/temp').then(function(temp) {
return { zone: zone, type: type, temp: parseInt(temp.trim()) };
});
}.bind(null, entries[i].name))
);
}
}
return Promise.all(promises);
}).then(function(updatedZones) {
var tableBody = viewBody.querySelector('#temp-status table');
if (tableBody) {
var newTable = renderTempTable(updatedZones);
tableBody.parentNode.replaceChild(newTable, tableBody);
}
});
}, 5);
return viewBody;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,20 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "Temperature Status"
msgstr "温度状态"
msgid "Real-time temperature of CPU/GPU/NPU thermal zones on Rockchip RK3588."
msgstr "Rockchip RK3588 CPU/GPU/NPU 温度实时监控"
msgid "Type"
msgstr "类型"
msgid "Zone"
msgstr "区域"
msgid "Temperature"
msgstr "温度"
msgid "Note: Temperatures above 75°C are shown in orange, above 85°C in red."
msgstr "注:75°C 以上显示橙色,85°C 以上显示红色"

View File

@ -0,0 +1,20 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "Temperature Status"
msgstr "温度状态"
msgid "Real-time temperature of CPU/GPU/NPU thermal zones on Rockchip RK3588."
msgstr "Rockchip RK3588 CPU/GPU/NPU 温度实时监控"
msgid "Type"
msgstr "类型"
msgid "Zone"
msgstr "区域"
msgid "Temperature"
msgstr "温度"
msgid "Note: Temperatures above 75°C are shown in orange, above 85°C in red."
msgstr "注:75°C 以上显示橙色,85°C 以上显示红色"

View File

@ -0,0 +1,13 @@
{
"admin/status/tempstatus": {
"title": "Temperature",
"order": 90,
"action": {
"type": "view",
"path": "system/tempstatus"
},
"depends": {
"acl": [ "luci-app-temp-status" ]
}
}
}

View File

@ -0,0 +1,12 @@
{
"luci-app-temp-status": {
"description": "Grant access to temperature status",
"read": {
"file": {
"/sys/class/thermal/": [ "list" ],
"/sys/class/thermal/*/type": [ "read" ],
"/sys/class/thermal/*/temp": [ "read" ]
}
}
}
}

View File

@ -0,0 +1,48 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2013-2021 OpenWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=zram-swap
PKG_RELEASE:=32
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
include $(INCLUDE_DIR)/package.mk
define Package/zram-swap
SECTION:=utils
CATEGORY:=Base system
DEPENDS:= \
+@BUSYBOX_CONFIG_FEATURE_SWAPON_DISCARD \
+@BUSYBOX_CONFIG_FEATURE_SWAPON_PRI \
+@BUSYBOX_CONFIG_MKSWAP \
+@BUSYBOX_CONFIG_SWAPOFF \
+@BUSYBOX_CONFIG_SWAPON \
+kmod-zram
TITLE:=ZRAM swap scripts
PKGARCH:=all
endef
define Package/zram-swap/description
A script to activate swaping on a compressed zram partition. This
could be used to increase the available memory, by using compressed
memory.
endef
define Build/Prepare
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/zram-swap/install
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/zram.init $(1)/etc/init.d/zram
endef
$(eval $(call BuildPackage,zram-swap))

View File

@ -0,0 +1,190 @@
#!/bin/sh /etc/rc.common
START=15
extra_command "compact" "Trigger compaction for all zram swap devices"
extra_command "status" "Print out information & statistics about zram swap devices"
ram_getsize()
{
local line
while read line; do case "$line" in MemTotal:*) set $line; echo "$2"; break ;; esac; done </proc/meminfo
}
zram_getsize() # in megabytes
{
local zram_size="$( uci -q get system.@system[0].zram_size_mb )"
local ram_size="$( ram_getsize )"
if [ -z "$zram_size" ]; then
# e.g. 6mb for 16mb-routers or 61mb for 128mb-routers
echo $(( ram_size / 2048 ))
else
echo "$zram_size"
fi
}
zram_dev()
{
local idx="$1"
echo "/dev/zram${idx:-0}"
}
zram_reset()
{
local dev="$1"
local message="$2"
local proc_entry="/sys/block/$( basename "$dev" )/reset"
logger -s -t zram_reset -p daemon.debug "$message via $proc_entry"
echo "1" >"$proc_entry"
}
zram_getdev()
{
#get unallocated zram dev
local zdev=$( zram_dev )
if [ "$(mount | grep $zdev)" ]; then
local idx=$(cat /sys/class/zram-control/hot_add)
zdev="$( zram_dev $idx )"
fi
echo $zdev
}
zram_comp_algo()
{
local dev="$1"
local zram_comp_algo="$( uci -q get system.@system[0].zram_comp_algo )"
if [ -z "$zram_comp_algo" ]; then
# default to lzo, which is always available
zram_comp_algo="lzo"
fi
if [ $(grep -c "$zram_comp_algo" /sys/block/$( basename $dev )/comp_algorithm) -ne 0 ]; then
logger -s -t zram_comp_algo -p daemon.debug "set compression algorithm '$zram_comp_algo' for zram '$dev'"
echo $zram_comp_algo > "/sys/block/$( basename $dev )/comp_algorithm"
else
logger -s -t zram_comp_algo -p daemon.debug "compression algorithm '$zram_comp_algo' is not supported for '$dev'"
fi
}
#print various stats info about zram swap device
zram_stats()
{
local zdev="/sys/block/$( basename "$1" )"
printf "\nGathering stats info for zram device \"$( basename "$1" )\"\n\n"
printf "ZRAM\n----\n"
printf "%-25s - %s\n" "Block device" $zdev
awk '{ printf "%-25s - %d MiB\n", "Device size", $1/1024/1024 }' <$zdev/disksize
printf "%-25s - %s\n" "Compression algo" "$(cat $zdev/comp_algorithm)"
awk 'BEGIN { fmt = "%-25s - %.2f %s\n"
fmt2 = "%-25s - %d\n"
print "\nDATA\n----" }
{ printf fmt, "Original data size", $1/1024/1024, "MiB"
printf fmt, "Compressed data size", $2/1024/1024, "MiB"
printf fmt, "Compress ratio", $1/$2, ""
print "\nMEMORY\n------"
printf fmt, "Memory used, total", $3/1024/1024, "MiB"
printf fmt, "Allocator overhead", ($3-$2)/1024/1024, "MiB"
printf fmt, "Allocator efficiency", $2/$3*100, "%"
printf fmt, "Maximum memory ever used", $5/1024/1024, "MiB"
printf fmt, "Memory limit", $4/1024/1024, "MiB"
print "\nPAGES\n-----"
printf fmt2, "Same pages count", $6
printf fmt2, "Pages compacted", $7 }' <$zdev/mm_stat
awk '{ printf "%-25s - %d\n", "Free pages discarded", $4 }' <$zdev/io_stat
}
zram_compact()
{
# compact zram device (reduce memory allocation overhead)
local zdev="/sys/block/$( basename "$1" )"
local old_mem_used=$(awk '{print $3}' <$zdev/mm_stat)
local old_overhead=$(awk '{print $3-$2}' <$zdev/mm_stat)
echo 1 > $zdev/compact
# If not running interactively, than just return
[ -z "$PS1" ] && return 0
echo ""
echo "Compacting zram device $zdev"
awk -v old_mem="$old_mem_used" -v ovr="$old_overhead" 'BEGIN { fmt = "%-25s - %.1f %s\n" }
{ printf fmt, "Memory usage reduced by ", (old_mem-$3)/1024/1024, "MiB"
printf fmt, "Overhead reduced by", (ovr-($3-$2))/ovr*100, "%" }' <$zdev/mm_stat
}
start()
{
[ -e /proc/swaps ] || {
logger -s -t zram_start -p daemon.crit "kernel doesn't support swap"
return 1
}
if [ $( grep -cs zram /proc/swaps ) -ne 0 ]; then
logger -s -t zram_start -p daemon.notice "zram swap is already mounted"
return 1
fi
local zram_dev="$( zram_getdev )"
[ -e "$zram_dev" ] || {
logger -s -t zram_start -p daemon.crit "[ERROR] device '$zram_dev' not found"
return 1
}
local zram_size="$( zram_getsize )"
local zram_priority="$( uci -q get system.@system[0].zram_priority )"
if [ -z "$zram_priority" ]; then
zram_priority="100"
fi
logger -s -t zram_start -p daemon.debug "activating '$zram_dev' for swapping ($zram_size MiB)"
zram_reset "$zram_dev" "enforcing defaults"
zram_comp_algo "$zram_dev"
echo $(( $zram_size * 1024 * 1024 )) >"/sys/block/$( basename "$zram_dev" )/disksize"
busybox mkswap "$zram_dev"
busybox swapon -d -p $zram_priority "$zram_dev"
}
stop()
{
local zram_dev
for zram_dev in $( grep zram /proc/swaps |awk '{print $1}' ); do {
logger -s -t zram_stop -p daemon.debug "deactivate swap $zram_dev"
busybox swapoff "$zram_dev" && zram_reset "$zram_dev" "claiming memory back"
local dev_index="$( echo $zram_dev | grep -o "[0-9]*$" )"
if [ $dev_index -ne 0 ]; then
logger -s -t zram_stop -p daemon.debug "removing zram $zram_dev"
echo $dev_index > /sys/class/zram-control/hot_remove
fi
} done
}
# show memory stats for all zram swaps
status()
{
for zram_dev in $( grep zram /proc/swaps |awk '{print $1}' ); do {
zram_stats "$zram_dev"
} done
}
# trigger compaction for all zram swaps
compact()
{
for zram_dev in $( grep zram /proc/swaps |awk '{print $1}' ); do {
zram_compact "$zram_dev"
} done
}

View File

@ -196,6 +196,8 @@ CONFIG_PACKAGE_luci-app-nlbwmon=y
CONFIG_PACKAGE_luci-app-vnstat=y
CONFIG_PACKAGE_luci-app-diag-core=y
CONFIG_PACKAGE_luci-app-watchcat=y
CONFIG_PACKAGE_luci-app-poweroffdevice=y
CONFIG_PACKAGE_luci-app-temp-status=y
# Chinese i18n for network monitoring
CONFIG_PACKAGE_luci-i18n-statistics-zh-cn=y
@ -233,3 +235,4 @@ CONFIG_PACKAGE_iperf3=y
CONFIG_PACKAGE_jq=y
CONFIG_PACKAGE_curl=y
CONFIG_PACKAGE_bind-dig=y
CONFIG_PACKAGE_zram-swap=y