mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-08-01 15:09:33 +08:00
💋 Sync 2026-07-30 20:36:50
This commit is contained in:
parent
68d255846f
commit
48cc1b888b
@ -1,9 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=fancontrol
|
||||
PKG_VERSION:=1
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_VERSION:=2.0.0
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
@ -16,16 +15,16 @@ include $(INCLUDE_DIR)/package.mk
|
||||
define Package/fancontrol
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
TITLE:=FanControl for OpenWRT
|
||||
TITLE:=Generic hwmon PWM fan controller
|
||||
endef
|
||||
|
||||
define Package/fancontrol/description
|
||||
FanControl for OpenWRT.
|
||||
Temperature-based continuous PWM fan controller using the Linux hwmon ABI.
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
$(CP) ./src/* $(PKG_BUILD_DIR)
|
||||
$(CP) ./src/Makefile ./src/fancontrol.c $(PKG_BUILD_DIR)
|
||||
endef
|
||||
|
||||
MAKE_FLAGS += \
|
||||
@ -35,7 +34,7 @@ define Package/fancontrol/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/fancontrol $(1)/usr/bin/fancontrol
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_BIN) ./files/fancontrol.config $(1)/etc/config/fancontrol
|
||||
$(INSTALL_CONF) ./files/fancontrol.config $(1)/etc/config/fancontrol
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/fancontrol.init $(1)/etc/init.d/fancontrol
|
||||
endef
|
||||
|
||||
@ -1,8 +1,19 @@
|
||||
config fancontrol settings
|
||||
option enabled '0'
|
||||
option thermal_file '/sys/devices/virtual/thermal/thermal_zone0/temp'
|
||||
option fan_file '/sys/devices/virtual/thermal/cooling_device0/cur_state'
|
||||
option start_temp '45'
|
||||
option start_speed '35'
|
||||
option max_speed '255'
|
||||
option temp_div '1000'
|
||||
config fancontrol 'settings'
|
||||
option enabled '0'
|
||||
option thermal_file 'auto'
|
||||
option thermal_zone 'auto'
|
||||
option fan_file 'auto'
|
||||
option fan_hwmon 'auto'
|
||||
option enable_file 'auto'
|
||||
option start_temp '45'
|
||||
option full_speed_temp '85'
|
||||
option hysteresis '3'
|
||||
option start_speed '64'
|
||||
option max_speed '255'
|
||||
option kick_speed '255'
|
||||
option kick_ms '500'
|
||||
option fail_safe_speed '255'
|
||||
option exit_speed '255'
|
||||
option temp_div '1000'
|
||||
option interval '5'
|
||||
option debug '0'
|
||||
|
||||
@ -5,53 +5,51 @@ STOP=01
|
||||
USE_PROCD=1
|
||||
|
||||
NAME=fancontrol
|
||||
PROG=/usr/bin/$NAME
|
||||
PROG=/usr/bin/fancontrol
|
||||
|
||||
append_option() {
|
||||
local option="$1"
|
||||
local argument="$2"
|
||||
local value
|
||||
|
||||
config_get value settings "$option"
|
||||
[ -n "$value" ] && procd_append_param command "$argument" "$value"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "$NAME"
|
||||
local enabled
|
||||
local debug
|
||||
|
||||
local enabled
|
||||
config_get enabled settings enabled
|
||||
[ "$enabled" = "1" ] || { _info "Instance \"$NAME\" disabled."; return 1; }
|
||||
config_load "$NAME"
|
||||
config_get_bool enabled settings enabled 0
|
||||
[ "$enabled" -eq 1 ] || return 0
|
||||
|
||||
local thermal_file
|
||||
local fan_file
|
||||
local start_temp
|
||||
local start_speed
|
||||
local max_speed
|
||||
local temp_div
|
||||
|
||||
config_get thermal_file settings thermal_file
|
||||
config_get fan_file settings fan_file
|
||||
config_get start_temp settings start_temp
|
||||
config_get start_speed settings start_speed
|
||||
config_get max_speed settings max_speed
|
||||
config_get temp_div settings temp_div
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command $PROG
|
||||
[ -n "$thermal_file" ] && procd_append_param command -T "$thermal_file"
|
||||
[ -n "$fan_file" ] && procd_append_param command -F "$fan_file"
|
||||
[ -n "$start_temp" ] && procd_append_param command -t "$start_temp"
|
||||
[ -n "$start_speed" ] && procd_append_param command -s "$start_speed"
|
||||
[ -n "$max_speed" ] && procd_append_param command -m "$max_speed"
|
||||
[ -n "$temp_div" ] && procd_append_param command -d "$temp_div"
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
start() {
|
||||
start_service
|
||||
}
|
||||
|
||||
stop() {
|
||||
procd_kill_instance
|
||||
}
|
||||
|
||||
reload() {
|
||||
procd_reload_instance
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG"
|
||||
append_option thermal_file -T
|
||||
append_option thermal_zone -Z
|
||||
append_option fan_file -F
|
||||
append_option fan_hwmon -N
|
||||
append_option enable_file -E
|
||||
append_option start_temp -t
|
||||
append_option full_speed_temp -x
|
||||
append_option hysteresis -H
|
||||
append_option start_speed -s
|
||||
append_option max_speed -m
|
||||
append_option kick_speed -k
|
||||
append_option kick_ms -K
|
||||
append_option fail_safe_speed -f
|
||||
append_option exit_speed -q
|
||||
append_option temp_div -d
|
||||
append_option interval -i
|
||||
config_get_bool debug settings debug 0
|
||||
[ "$debug" -eq 1 ] && procd_append_param command -D
|
||||
procd_set_param respawn 3600 5 5
|
||||
procd_set_param term_timeout 10
|
||||
procd_set_param file /etc/config/fancontrol
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "fancontrol"
|
||||
}
|
||||
procd_add_reload_trigger fancontrol
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
CC := gcc
|
||||
CFLAGS := -O3 -Wall -fomit-frame-pointer
|
||||
CC ?= cc
|
||||
CFLAGS ?= -O2
|
||||
CFLAGS += -Wall -Wextra -Wformat=2
|
||||
|
||||
all: fancontrol
|
||||
|
||||
@ -7,7 +8,7 @@ fancontrol: %: %.o
|
||||
$(CC) $(LDFLAGS) -o $@ $^
|
||||
|
||||
%.o: %.c
|
||||
$(CC) $(CFLAGS) $(INCLUDE) -o $@ -c $<
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ -c $<
|
||||
|
||||
clean:
|
||||
rm -f fancontrol *.o *~
|
||||
rm -f fancontrol *.o *~
|
||||
|
||||
@ -1,200 +1,791 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <glob.h>
|
||||
#include <limits.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <signal.h>
|
||||
#include <syslog.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define MAX_LENGTH 200
|
||||
#define MAX_TEMP 120
|
||||
#define PROGRAM_NAME "fancontrol"
|
||||
#define PROGRAM_VERSION "2.0.0"
|
||||
#define AUTO_VALUE "auto"
|
||||
#define DEFAULT_SYSFS_ROOT "/sys"
|
||||
#define DEFAULT_STATUS_FILE "/var/run/fancontrol.status"
|
||||
#define MAX_HWMON_CHANNELS 32
|
||||
|
||||
// 定义全局变量
|
||||
char thermal_file[MAX_LENGTH] = "/sys/devices/virtual/thermal/thermal_zone0/temp"; // -T
|
||||
char fan_file[MAX_LENGTH] = "/sys/devices/virtual/thermal/cooling_device0/cur_state"; // -F
|
||||
struct fan_config {
|
||||
char thermal_file[PATH_MAX];
|
||||
char thermal_zone[128];
|
||||
char fan_file[PATH_MAX];
|
||||
char fan_hwmon[128];
|
||||
char enable_file[PATH_MAX];
|
||||
int start_temp;
|
||||
int full_speed_temp;
|
||||
int hysteresis;
|
||||
int start_pwm;
|
||||
int max_pwm;
|
||||
int kick_pwm;
|
||||
int kick_ms;
|
||||
int fail_safe_pwm;
|
||||
int exit_pwm;
|
||||
int temp_div;
|
||||
int interval;
|
||||
bool debug;
|
||||
bool once;
|
||||
};
|
||||
|
||||
int start_speed = 35; // -s
|
||||
int start_temp = 45; // -t
|
||||
int max_speed = 255; // -m
|
||||
int temp_div = 1000; // -d
|
||||
int debug_mode = 0; // -D
|
||||
struct fan_runtime {
|
||||
char thermal_file[PATH_MAX];
|
||||
char fan_file[PATH_MAX];
|
||||
char enable_file[PATH_MAX];
|
||||
char rpm_file[PATH_MAX];
|
||||
bool fan_active;
|
||||
};
|
||||
|
||||
/**
|
||||
* 底层读文件
|
||||
*/
|
||||
static int read_file(const char* path ,char* result ,size_t size) {
|
||||
FILE* fp;
|
||||
char* line = NULL;
|
||||
size_t len = 0;
|
||||
ssize_t read;
|
||||
static const char *sysfs_root = DEFAULT_SYSFS_ROOT;
|
||||
static const char *status_file = DEFAULT_STATUS_FILE;
|
||||
static volatile sig_atomic_t stop_requested;
|
||||
|
||||
fp = fopen(path ,"r");
|
||||
if (fp == NULL)
|
||||
return -1;
|
||||
|
||||
if (( read = getline(&line ,&len ,fp) ) != -1) {
|
||||
if (size != 0)
|
||||
memcpy(result ,line ,size);
|
||||
else
|
||||
memcpy(result ,line ,read - 1);
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
if (line)
|
||||
free(line);
|
||||
return 0;
|
||||
static void usage(FILE *stream, const char *name)
|
||||
{
|
||||
fprintf(stream,
|
||||
"Usage: %s [options]\n"
|
||||
" -T path temperature sysfs path or 'auto'\n"
|
||||
" -Z type preferred thermal zone type or 'auto'\n"
|
||||
" -F path PWM sysfs path or 'auto'\n"
|
||||
" -N name preferred hwmon name or 'auto'\n"
|
||||
" -E path pwm enable sysfs path or 'auto'\n"
|
||||
" -t degrees fan start temperature\n"
|
||||
" -x degrees full-speed temperature\n"
|
||||
" -H degrees stop hysteresis\n"
|
||||
" -s pwm minimum running PWM (1-255)\n"
|
||||
" -m pwm maximum PWM (1-255)\n"
|
||||
" -k pwm start kick PWM, 0 disables the kick\n"
|
||||
" -K ms start kick duration in milliseconds\n"
|
||||
" -f pwm PWM used after a sensor failure\n"
|
||||
" -q pwm PWM used when the daemon exits\n"
|
||||
" -d divisor temperature input divisor\n"
|
||||
" -i seconds polling interval\n"
|
||||
" -D enable debug logging\n"
|
||||
" -1 run one control cycle and exit\n"
|
||||
" -v print version\n"
|
||||
" -h show this help\n",
|
||||
name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 底层写文件
|
||||
*/
|
||||
static size_t write_file(const char* path ,char* buf ,size_t len) {
|
||||
FILE* fp = NULL;
|
||||
size_t size = 0;
|
||||
fp = fopen(path ,"w+");
|
||||
if (fp == NULL) {
|
||||
return 0;
|
||||
}
|
||||
size = fwrite(buf ,len ,1 ,fp);
|
||||
fclose(fp);
|
||||
return size;
|
||||
static int copy_string(char *dest, size_t size, const char *source)
|
||||
{
|
||||
int len;
|
||||
|
||||
if (!source || strpbrk(source, "\r\n"))
|
||||
return -1;
|
||||
|
||||
len = snprintf(dest, size, "%s", source);
|
||||
return len < 0 || (size_t)len >= size ? -1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取温度
|
||||
*/
|
||||
int get_temperature(char* thermal_file ,int div) {
|
||||
char buf[8] = { 0 };
|
||||
if (read_file(thermal_file ,buf ,0) == 0) {
|
||||
return atoi(buf) / div;
|
||||
}
|
||||
return -1;
|
||||
static bool is_auto(const char *value)
|
||||
{
|
||||
return !value[0] || strcmp(value, AUTO_VALUE) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取风扇速度
|
||||
*/
|
||||
int get_fanspeed(char* fan_file) {
|
||||
char buf[8] = { 0 };
|
||||
if (read_file(fan_file ,buf ,0) == 0) {
|
||||
return atoi(buf);
|
||||
}
|
||||
return -1;
|
||||
static int parse_int(const char *text, int min, int max, const char *name)
|
||||
{
|
||||
char *end = NULL;
|
||||
long value;
|
||||
|
||||
errno = 0;
|
||||
value = strtol(text, &end, 10);
|
||||
if (errno || !end || *end || value < min || value > max) {
|
||||
fprintf(stderr, "Invalid %s: %s\n", name, text);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
return (int)value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置风扇转速
|
||||
*/
|
||||
int set_fanspeed(int fan_speed ,char* fan_file) {
|
||||
char buf[8] = { 0 };
|
||||
sprintf(buf ,"%d\n" ,fan_speed);
|
||||
return write_file(fan_file ,buf ,strlen(buf));
|
||||
static int read_text_file(const char *path, char *buffer, size_t size)
|
||||
{
|
||||
ssize_t length;
|
||||
int fd;
|
||||
|
||||
if (!size)
|
||||
return -1;
|
||||
|
||||
fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
|
||||
length = read(fd, buffer, size - 1);
|
||||
close(fd);
|
||||
if (length <= 0)
|
||||
return -1;
|
||||
|
||||
buffer[length] = '\0';
|
||||
buffer[strcspn(buffer, "\r\n")] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算风扇转速
|
||||
*/
|
||||
int calculate_speed(int current_temp ,int max_temp ,int min_temp ,int max_speed ,int min_speed) {
|
||||
if (current_temp < min_temp) {
|
||||
return 0;
|
||||
}
|
||||
int fan_speed = ( current_temp - min_temp ) * ( max_speed - min_speed ) / ( max_temp - min_temp ) + min_speed;
|
||||
if (fan_speed > max_speed) {
|
||||
fan_speed = max_speed;
|
||||
}
|
||||
return fan_speed;
|
||||
static int read_long_file(const char *path, long *value)
|
||||
{
|
||||
char buffer[64];
|
||||
char *end = NULL;
|
||||
long parsed;
|
||||
|
||||
if (read_text_file(path, buffer, sizeof(buffer)))
|
||||
return -1;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtol(buffer, &end, 10);
|
||||
if (errno || end == buffer)
|
||||
return -1;
|
||||
|
||||
while (*end == ' ' || *end == '\t')
|
||||
end++;
|
||||
if (*end)
|
||||
return -1;
|
||||
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件是否存在方法
|
||||
*/
|
||||
static int file_exist(const char* name) {
|
||||
struct stat buffer;
|
||||
return stat(name ,&buffer);
|
||||
static int write_long_file(const char *path, int value)
|
||||
{
|
||||
char buffer[32];
|
||||
ssize_t written;
|
||||
int length;
|
||||
int fd;
|
||||
|
||||
length = snprintf(buffer, sizeof(buffer), "%d\n", value);
|
||||
if (length < 0 || (size_t)length >= sizeof(buffer))
|
||||
return -1;
|
||||
|
||||
fd = open(path, O_WRONLY | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
|
||||
written = write(fd, buffer, (size_t)length);
|
||||
if (close(fd) || written != length)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 信号处理函数
|
||||
*/
|
||||
void handle_termination(int signum) {
|
||||
// 设置风扇转速为 0
|
||||
set_fanspeed(0 ,fan_file);
|
||||
exit(EXIT_SUCCESS); // 优雅地退出程序
|
||||
static void sleep_ms(unsigned int milliseconds)
|
||||
{
|
||||
struct timespec remaining = {
|
||||
.tv_sec = milliseconds / 1000,
|
||||
.tv_nsec = (long)(milliseconds % 1000) * 1000000L,
|
||||
};
|
||||
|
||||
while (!stop_requested && nanosleep(&remaining, &remaining) && errno == EINTR)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册信号处理函数
|
||||
*/
|
||||
void register_signal_handlers( ) {
|
||||
struct sigaction sa;
|
||||
memset(&sa ,0 ,sizeof(sa));
|
||||
sa.sa_handler = handle_termination;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sigaction(SIGINT ,&sa ,NULL);
|
||||
sigaction(SIGTERM ,&sa ,NULL);
|
||||
static int preferred_thermal_score(const char *type)
|
||||
{
|
||||
static const char * const preferred[] = {
|
||||
"cpu-top-thermal", "cpu_top_thermal", "cpu-thermal",
|
||||
"cpu_thermal", "soc-thermal", "soc_thermal",
|
||||
};
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < sizeof(preferred) / sizeof(preferred[0]); i++)
|
||||
if (strcmp(type, preferred[i]) == 0)
|
||||
return (int)i;
|
||||
|
||||
return 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
int main(int argc ,char* argv[ ]) {
|
||||
// 解析命令行选项
|
||||
int opt;
|
||||
while (( opt = getopt(argc ,argv ,"T:F:s:t:m:d:D:v:") ) != -1) {
|
||||
switch (opt) {
|
||||
case 'T':
|
||||
snprintf(thermal_file ,sizeof(thermal_file) ,"%s" ,optarg);
|
||||
break;
|
||||
case 'F':
|
||||
snprintf(fan_file ,sizeof(fan_file) ,"%s" ,optarg);
|
||||
break;
|
||||
case 's':
|
||||
start_speed = atoi(optarg);
|
||||
break;
|
||||
case 't':
|
||||
start_temp = atoi(optarg);
|
||||
break;
|
||||
case 'm':
|
||||
max_speed = atoi(optarg);
|
||||
break;
|
||||
case 'd':
|
||||
temp_div = atoi(optarg);
|
||||
break;
|
||||
case 'D':
|
||||
debug_mode = atoi(optarg);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr ,"Usage: %s [option]\n"
|
||||
" -T sysfs # temperature sysfs file, default is '%s'\n"
|
||||
" -F sysfs # fan sysfs file, default is '%s'\n"
|
||||
" -s speed # initial speed for fan startup, default is %d\n"
|
||||
" -t temperature # fan start temperature, default is %d°C\n"
|
||||
" -m speed # fan maximum speed, default is %d\n"
|
||||
" -d div # temperature divide, default is %d\n"
|
||||
" -v # verbose\n" ,argv[0] ,thermal_file ,fan_file ,start_speed ,start_temp ,max_speed ,temp_div);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
// 检测虚拟文件是否存在
|
||||
if (file_exist(fan_file) != 0 || file_exist(thermal_file) != 0) {
|
||||
fprintf(stderr ,"File: '%s' or '%s' not exist\n" ,fan_file ,thermal_file);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
static int preferred_hwmon_temp_score(const char *name)
|
||||
{
|
||||
if (strstr(name, "cpu") || strstr(name, "CPU"))
|
||||
return 0;
|
||||
if (strstr(name, "soc") || strstr(name, "SoC"))
|
||||
return 1;
|
||||
|
||||
// 注册退出信号
|
||||
register_signal_handlers( );
|
||||
|
||||
// 监控风扇
|
||||
while (1) {
|
||||
int temperature = get_temperature(thermal_file ,temp_div);
|
||||
// 有效温度时设置风扇速度
|
||||
if (temperature > 0) {
|
||||
int fan_speed = calculate_speed(temperature ,MAX_TEMP ,start_temp ,max_speed ,start_speed);
|
||||
set_fanspeed(fan_speed ,fan_file);
|
||||
}
|
||||
if (debug_mode) {
|
||||
fprintf(stdout ,"Temperature: %d°C, Fan Speed: %d\n" ,get_temperature(thermal_file ,temp_div) ,get_fanspeed(fan_file));
|
||||
}
|
||||
sleep(5);
|
||||
}
|
||||
return 0;
|
||||
return 100;
|
||||
}
|
||||
|
||||
static int discover_hwmon_temperature(char *result, size_t result_size)
|
||||
{
|
||||
char pattern[PATH_MAX];
|
||||
char name_file[PATH_MAX];
|
||||
char hwmon_name[128];
|
||||
char candidate[PATH_MAX];
|
||||
char best[PATH_MAX] = "";
|
||||
glob_t paths = { 0 };
|
||||
int best_score = INT_MAX;
|
||||
size_t i;
|
||||
|
||||
if (snprintf(pattern, sizeof(pattern), "%s/class/hwmon/hwmon*", sysfs_root) >=
|
||||
(int)sizeof(pattern))
|
||||
return -1;
|
||||
if (glob(pattern, 0, NULL, &paths))
|
||||
return -1;
|
||||
|
||||
for (i = 0; i < paths.gl_pathc; i++) {
|
||||
int channel;
|
||||
int score;
|
||||
|
||||
if (snprintf(name_file, sizeof(name_file), "%s/name", paths.gl_pathv[i]) >=
|
||||
(int)sizeof(name_file) ||
|
||||
read_text_file(name_file, hwmon_name, sizeof(hwmon_name)))
|
||||
hwmon_name[0] = '\0';
|
||||
score = preferred_hwmon_temp_score(hwmon_name);
|
||||
|
||||
for (channel = 1; channel <= MAX_HWMON_CHANNELS; channel++) {
|
||||
if (snprintf(candidate, sizeof(candidate), "%s/temp%d_input",
|
||||
paths.gl_pathv[i], channel) >= (int)sizeof(candidate))
|
||||
continue;
|
||||
if (access(candidate, R_OK))
|
||||
continue;
|
||||
if (score < best_score) {
|
||||
copy_string(best, sizeof(best), candidate);
|
||||
best_score = score;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
globfree(&paths);
|
||||
if (!best[0])
|
||||
return -1;
|
||||
|
||||
return copy_string(result, result_size, best);
|
||||
}
|
||||
|
||||
static int discover_thermal_file(const struct fan_config *config,
|
||||
char *result, size_t result_size)
|
||||
{
|
||||
char pattern[PATH_MAX];
|
||||
char type_file[PATH_MAX];
|
||||
char type[128];
|
||||
char best[PATH_MAX] = "";
|
||||
glob_t paths = { 0 };
|
||||
int best_score = INT_MAX;
|
||||
size_t i;
|
||||
|
||||
if (snprintf(pattern, sizeof(pattern), "%s/class/thermal/thermal_zone*/temp",
|
||||
sysfs_root) >= (int)sizeof(pattern))
|
||||
return -1;
|
||||
|
||||
if (glob(pattern, 0, NULL, &paths)) {
|
||||
if (is_auto(config->thermal_zone))
|
||||
return discover_hwmon_temperature(result, result_size);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < paths.gl_pathc; i++) {
|
||||
char *slash;
|
||||
int score;
|
||||
|
||||
if (access(paths.gl_pathv[i], R_OK))
|
||||
continue;
|
||||
|
||||
if (copy_string(type_file, sizeof(type_file), paths.gl_pathv[i]))
|
||||
continue;
|
||||
slash = strrchr(type_file, '/');
|
||||
if (!slash)
|
||||
continue;
|
||||
copy_string(slash + 1, sizeof(type_file) - (size_t)(slash + 1 - type_file),
|
||||
"type");
|
||||
if (read_text_file(type_file, type, sizeof(type)))
|
||||
type[0] = '\0';
|
||||
|
||||
if (!is_auto(config->thermal_zone)) {
|
||||
if (strcmp(config->thermal_zone, type) != 0)
|
||||
continue;
|
||||
score = 0;
|
||||
} else {
|
||||
score = preferred_thermal_score(type);
|
||||
}
|
||||
|
||||
if (score < best_score) {
|
||||
copy_string(best, sizeof(best), paths.gl_pathv[i]);
|
||||
best_score = score;
|
||||
}
|
||||
}
|
||||
|
||||
globfree(&paths);
|
||||
if (!best[0]) {
|
||||
if (is_auto(config->thermal_zone))
|
||||
return discover_hwmon_temperature(result, result_size);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return copy_string(result, result_size, best);
|
||||
}
|
||||
|
||||
static bool valid_pwm_basename(const char *name, int *channel)
|
||||
{
|
||||
char trailing;
|
||||
int parsed;
|
||||
|
||||
if (sscanf(name, "pwm%d%c", &parsed, &trailing) != 1 ||
|
||||
parsed < 1 || parsed > MAX_HWMON_CHANNELS)
|
||||
return false;
|
||||
|
||||
*channel = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int derive_fan_paths(const char *pwm_file, struct fan_runtime *runtime)
|
||||
{
|
||||
char directory[PATH_MAX];
|
||||
const char *basename;
|
||||
char *slash;
|
||||
int channel;
|
||||
int length;
|
||||
|
||||
if (copy_string(directory, sizeof(directory), pwm_file))
|
||||
return -1;
|
||||
slash = strrchr(directory, '/');
|
||||
if (!slash)
|
||||
return -1;
|
||||
basename = slash + 1;
|
||||
if (!valid_pwm_basename(basename, &channel))
|
||||
return 0;
|
||||
*slash = '\0';
|
||||
|
||||
if (is_auto(runtime->enable_file)) {
|
||||
length = snprintf(runtime->enable_file, sizeof(runtime->enable_file),
|
||||
"%s/pwm%d_enable", directory, channel);
|
||||
if (length < 0 || (size_t)length >= sizeof(runtime->enable_file))
|
||||
return -1;
|
||||
if (access(runtime->enable_file, W_OK))
|
||||
runtime->enable_file[0] = '\0';
|
||||
}
|
||||
|
||||
length = snprintf(runtime->rpm_file, sizeof(runtime->rpm_file),
|
||||
"%s/fan%d_input", directory, channel);
|
||||
if (length < 0 || (size_t)length >= sizeof(runtime->rpm_file))
|
||||
return -1;
|
||||
if (access(runtime->rpm_file, R_OK))
|
||||
runtime->rpm_file[0] = '\0';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int discover_fan_file(const struct fan_config *config,
|
||||
struct fan_runtime *runtime)
|
||||
{
|
||||
char pattern[PATH_MAX];
|
||||
char name_file[PATH_MAX];
|
||||
char hwmon_name[128];
|
||||
char candidate[PATH_MAX];
|
||||
char best[PATH_MAX] = "";
|
||||
glob_t paths = { 0 };
|
||||
int best_score = INT_MAX;
|
||||
size_t i;
|
||||
|
||||
if (snprintf(pattern, sizeof(pattern), "%s/class/hwmon/hwmon*", sysfs_root) >=
|
||||
(int)sizeof(pattern))
|
||||
return -1;
|
||||
if (glob(pattern, 0, NULL, &paths))
|
||||
return -1;
|
||||
|
||||
for (i = 0; i < paths.gl_pathc; i++) {
|
||||
int channel;
|
||||
int score;
|
||||
|
||||
snprintf(name_file, sizeof(name_file), "%s/name", paths.gl_pathv[i]);
|
||||
if (read_text_file(name_file, hwmon_name, sizeof(hwmon_name)))
|
||||
continue;
|
||||
|
||||
if (!is_auto(config->fan_hwmon)) {
|
||||
if (strcmp(config->fan_hwmon, hwmon_name) != 0)
|
||||
continue;
|
||||
score = 0;
|
||||
} else {
|
||||
score = strcmp(hwmon_name, "pwmfan") == 0 ? 0 : 100;
|
||||
}
|
||||
|
||||
for (channel = 1; channel <= MAX_HWMON_CHANNELS; channel++) {
|
||||
snprintf(candidate, sizeof(candidate), "%s/pwm%d",
|
||||
paths.gl_pathv[i], channel);
|
||||
if (access(candidate, R_OK | W_OK))
|
||||
continue;
|
||||
if (score < best_score) {
|
||||
copy_string(best, sizeof(best), candidate);
|
||||
best_score = score;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
globfree(&paths);
|
||||
if (!best[0])
|
||||
return -1;
|
||||
if (copy_string(runtime->fan_file, sizeof(runtime->fan_file), best))
|
||||
return -1;
|
||||
|
||||
return derive_fan_paths(runtime->fan_file, runtime);
|
||||
}
|
||||
|
||||
static bool is_cooling_state_path(const char *path)
|
||||
{
|
||||
return strstr(path, "/cooling_device") &&
|
||||
strcmp(strrchr(path, '/') ? strrchr(path, '/') + 1 : path,
|
||||
"cur_state") == 0;
|
||||
}
|
||||
|
||||
static int resolve_paths(const struct fan_config *config,
|
||||
struct fan_runtime *runtime)
|
||||
{
|
||||
memset(runtime, 0, sizeof(*runtime));
|
||||
copy_string(runtime->enable_file, sizeof(runtime->enable_file),
|
||||
config->enable_file);
|
||||
|
||||
if (is_auto(config->thermal_file)) {
|
||||
if (discover_thermal_file(config, runtime->thermal_file,
|
||||
sizeof(runtime->thermal_file))) {
|
||||
syslog(LOG_ERR, "no readable thermal sensor was found");
|
||||
return -1;
|
||||
}
|
||||
} else if (copy_string(runtime->thermal_file,
|
||||
sizeof(runtime->thermal_file), config->thermal_file) ||
|
||||
access(runtime->thermal_file, R_OK)) {
|
||||
syslog(LOG_ERR, "temperature input is not readable: %s",
|
||||
config->thermal_file);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (is_auto(config->fan_file)) {
|
||||
if (discover_fan_file(config, runtime)) {
|
||||
syslog(LOG_ERR, "no writable hwmon PWM output was found");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
if (is_cooling_state_path(config->fan_file)) {
|
||||
syslog(LOG_ERR,
|
||||
"%s is a discrete cooling state, not a continuous PWM output",
|
||||
config->fan_file);
|
||||
return -1;
|
||||
}
|
||||
if (copy_string(runtime->fan_file, sizeof(runtime->fan_file),
|
||||
config->fan_file) || access(runtime->fan_file, R_OK | W_OK)) {
|
||||
syslog(LOG_ERR, "PWM output is not readable and writable: %s",
|
||||
config->fan_file);
|
||||
return -1;
|
||||
}
|
||||
if (derive_fan_paths(runtime->fan_file, runtime)) {
|
||||
syslog(LOG_ERR, "failed to derive controls for PWM output: %s",
|
||||
runtime->fan_file);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_auto(config->enable_file)) {
|
||||
if (copy_string(runtime->enable_file, sizeof(runtime->enable_file),
|
||||
config->enable_file) || access(runtime->enable_file, W_OK)) {
|
||||
syslog(LOG_ERR, "PWM enable control is not writable: %s",
|
||||
config->enable_file);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int calculate_pwm(const struct fan_config *config, int64_t temp_mc,
|
||||
bool fan_active)
|
||||
{
|
||||
int64_t start_mc = (int64_t)config->start_temp * 1000;
|
||||
int64_t stop_mc = (int64_t)(config->start_temp - config->hysteresis) * 1000;
|
||||
int64_t full_mc = (int64_t)config->full_speed_temp * 1000;
|
||||
int64_t pwm;
|
||||
|
||||
if ((!fan_active && temp_mc < start_mc) ||
|
||||
(fan_active && temp_mc <= stop_mc))
|
||||
return 0;
|
||||
if (temp_mc <= start_mc)
|
||||
return config->start_pwm;
|
||||
if (temp_mc >= full_mc)
|
||||
return config->max_pwm;
|
||||
|
||||
pwm = config->start_pwm +
|
||||
(temp_mc - start_mc) * (config->max_pwm - config->start_pwm) /
|
||||
(full_mc - start_mc);
|
||||
return (int)pwm;
|
||||
}
|
||||
|
||||
static int set_pwm(const struct fan_runtime *runtime, int pwm)
|
||||
{
|
||||
if (write_long_file(runtime->fan_file, pwm)) {
|
||||
syslog(LOG_ERR, "failed to write PWM %d to %s: %s", pwm,
|
||||
runtime->fan_file, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void write_status(const struct fan_runtime *runtime, int64_t temp_mc,
|
||||
int pwm, long rpm, const char *error)
|
||||
{
|
||||
char temporary[PATH_MAX];
|
||||
FILE *stream;
|
||||
|
||||
if (snprintf(temporary, sizeof(temporary), "%s.%ld", status_file,
|
||||
(long)getpid()) >= (int)sizeof(temporary))
|
||||
return;
|
||||
|
||||
stream = fopen(temporary, "w");
|
||||
if (!stream)
|
||||
return;
|
||||
|
||||
fprintf(stream,
|
||||
"running=1\n"
|
||||
"fan_active=%d\n"
|
||||
"temperature_mc=%lld\n"
|
||||
"pwm=%d\n"
|
||||
"rpm=%ld\n"
|
||||
"thermal_file=%s\n"
|
||||
"fan_file=%s\n"
|
||||
"error=%s\n",
|
||||
runtime->fan_active ? 1 : 0, (long long)temp_mc, pwm, rpm,
|
||||
runtime->thermal_file, runtime->fan_file, error ? error : "");
|
||||
if (fclose(stream) || rename(temporary, status_file))
|
||||
unlink(temporary);
|
||||
}
|
||||
|
||||
static void signal_handler(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop_requested = 1;
|
||||
}
|
||||
|
||||
static int install_signal_handlers(void)
|
||||
{
|
||||
struct sigaction action = {
|
||||
.sa_handler = signal_handler,
|
||||
};
|
||||
|
||||
sigemptyset(&action.sa_mask);
|
||||
return sigaction(SIGINT, &action, NULL) ||
|
||||
sigaction(SIGTERM, &action, NULL);
|
||||
}
|
||||
|
||||
static void set_defaults(struct fan_config *config)
|
||||
{
|
||||
memset(config, 0, sizeof(*config));
|
||||
copy_string(config->thermal_file, sizeof(config->thermal_file), AUTO_VALUE);
|
||||
copy_string(config->thermal_zone, sizeof(config->thermal_zone), AUTO_VALUE);
|
||||
copy_string(config->fan_file, sizeof(config->fan_file), AUTO_VALUE);
|
||||
copy_string(config->fan_hwmon, sizeof(config->fan_hwmon), AUTO_VALUE);
|
||||
copy_string(config->enable_file, sizeof(config->enable_file), AUTO_VALUE);
|
||||
config->start_temp = 45;
|
||||
config->full_speed_temp = 85;
|
||||
config->hysteresis = 3;
|
||||
config->start_pwm = 64;
|
||||
config->max_pwm = 255;
|
||||
config->kick_pwm = 255;
|
||||
config->kick_ms = 500;
|
||||
config->fail_safe_pwm = 255;
|
||||
config->exit_pwm = 255;
|
||||
config->temp_div = 1000;
|
||||
config->interval = 5;
|
||||
}
|
||||
|
||||
static void parse_options(int argc, char **argv, struct fan_config *config)
|
||||
{
|
||||
int option;
|
||||
|
||||
while ((option = getopt(argc, argv, "T:Z:F:N:E:t:x:H:s:m:k:K:f:q:d:i:D1vh")) != -1) {
|
||||
switch (option) {
|
||||
case 'T':
|
||||
if (copy_string(config->thermal_file,
|
||||
sizeof(config->thermal_file), optarg))
|
||||
exit(EXIT_FAILURE);
|
||||
break;
|
||||
case 'Z':
|
||||
if (copy_string(config->thermal_zone, sizeof(config->thermal_zone), optarg))
|
||||
exit(EXIT_FAILURE);
|
||||
break;
|
||||
case 'F':
|
||||
if (copy_string(config->fan_file, sizeof(config->fan_file), optarg))
|
||||
exit(EXIT_FAILURE);
|
||||
break;
|
||||
case 'N':
|
||||
if (copy_string(config->fan_hwmon, sizeof(config->fan_hwmon), optarg))
|
||||
exit(EXIT_FAILURE);
|
||||
break;
|
||||
case 'E':
|
||||
if (copy_string(config->enable_file, sizeof(config->enable_file), optarg))
|
||||
exit(EXIT_FAILURE);
|
||||
break;
|
||||
case 't':
|
||||
config->start_temp = parse_int(optarg, -100, 200, "start temperature");
|
||||
break;
|
||||
case 'x':
|
||||
config->full_speed_temp = parse_int(optarg, -99, 250,
|
||||
"full-speed temperature");
|
||||
break;
|
||||
case 'H':
|
||||
config->hysteresis = parse_int(optarg, 0, 50, "hysteresis");
|
||||
break;
|
||||
case 's':
|
||||
config->start_pwm = parse_int(optarg, 1, 255, "start PWM");
|
||||
break;
|
||||
case 'm':
|
||||
config->max_pwm = parse_int(optarg, 1, 255, "maximum PWM");
|
||||
break;
|
||||
case 'k':
|
||||
config->kick_pwm = parse_int(optarg, 0, 255, "kick PWM");
|
||||
break;
|
||||
case 'K':
|
||||
config->kick_ms = parse_int(optarg, 0, 10000, "kick duration");
|
||||
break;
|
||||
case 'f':
|
||||
config->fail_safe_pwm = parse_int(optarg, 0, 255, "fail-safe PWM");
|
||||
break;
|
||||
case 'q':
|
||||
config->exit_pwm = parse_int(optarg, 0, 255, "exit PWM");
|
||||
break;
|
||||
case 'd':
|
||||
config->temp_div = parse_int(optarg, 1, 1000000,
|
||||
"temperature divisor");
|
||||
break;
|
||||
case 'i':
|
||||
config->interval = parse_int(optarg, 1, 300, "polling interval");
|
||||
break;
|
||||
case 'D':
|
||||
config->debug = true;
|
||||
break;
|
||||
case '1':
|
||||
config->once = true;
|
||||
break;
|
||||
case 'v':
|
||||
printf("%s %s\n", PROGRAM_NAME, PROGRAM_VERSION);
|
||||
exit(EXIT_SUCCESS);
|
||||
case 'h':
|
||||
usage(stdout, argv[0]);
|
||||
exit(EXIT_SUCCESS);
|
||||
default:
|
||||
usage(stderr, argv[0]);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int validate_config(const struct fan_config *config)
|
||||
{
|
||||
if (config->full_speed_temp <= config->start_temp) {
|
||||
fprintf(stderr, "Full-speed temperature must exceed start temperature\n");
|
||||
return -1;
|
||||
}
|
||||
if (config->start_pwm > config->max_pwm) {
|
||||
fprintf(stderr, "Start PWM must not exceed maximum PWM\n");
|
||||
return -1;
|
||||
}
|
||||
if (config->kick_ms && !config->kick_pwm) {
|
||||
fprintf(stderr, "Kick PWM must be non-zero when kick duration is enabled\n");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct fan_config config;
|
||||
struct fan_runtime runtime;
|
||||
const char *environment;
|
||||
bool sensor_failed = false;
|
||||
int exit_code = EXIT_SUCCESS;
|
||||
|
||||
set_defaults(&config);
|
||||
parse_options(argc, argv, &config);
|
||||
if (validate_config(&config))
|
||||
return EXIT_FAILURE;
|
||||
|
||||
environment = getenv("FANCONTROL_SYSFS_ROOT");
|
||||
if (environment && environment[0])
|
||||
sysfs_root = environment;
|
||||
environment = getenv("FANCONTROL_STATUS_FILE");
|
||||
if (environment && environment[0])
|
||||
status_file = environment;
|
||||
|
||||
openlog(PROGRAM_NAME, LOG_PID | LOG_CONS, LOG_DAEMON);
|
||||
if (install_signal_handlers()) {
|
||||
syslog(LOG_ERR, "failed to install signal handlers: %s", strerror(errno));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (resolve_paths(&config, &runtime))
|
||||
return EXIT_FAILURE;
|
||||
|
||||
if (runtime.enable_file[0] && write_long_file(runtime.enable_file, 1))
|
||||
syslog(LOG_WARNING, "failed to select manual PWM mode through %s: %s",
|
||||
runtime.enable_file, strerror(errno));
|
||||
|
||||
syslog(LOG_NOTICE, "using temperature input %s and PWM output %s",
|
||||
runtime.thermal_file, runtime.fan_file);
|
||||
|
||||
while (!stop_requested) {
|
||||
long raw_temp;
|
||||
long actual_pwm = -1;
|
||||
long rpm = -1;
|
||||
int64_t temp_mc = -1;
|
||||
int target_pwm;
|
||||
|
||||
if (read_long_file(runtime.thermal_file, &raw_temp)) {
|
||||
target_pwm = config.fail_safe_pwm;
|
||||
if (set_pwm(&runtime, target_pwm))
|
||||
exit_code = EXIT_FAILURE;
|
||||
runtime.fan_active = target_pwm > 0;
|
||||
write_status(&runtime, temp_mc, target_pwm, rpm,
|
||||
"temperature-read-failed");
|
||||
if (!sensor_failed)
|
||||
syslog(LOG_ERR,
|
||||
"failed to read temperature from %s; using fail-safe PWM %d",
|
||||
runtime.thermal_file, target_pwm);
|
||||
sensor_failed = true;
|
||||
} else {
|
||||
if (sensor_failed)
|
||||
syslog(LOG_NOTICE, "temperature input %s recovered",
|
||||
runtime.thermal_file);
|
||||
sensor_failed = false;
|
||||
temp_mc = (int64_t)raw_temp * 1000 / config.temp_div;
|
||||
target_pwm = calculate_pwm(&config, temp_mc, runtime.fan_active);
|
||||
|
||||
if (target_pwm > 0 && !runtime.fan_active &&
|
||||
config.kick_pwm > 0 && config.kick_ms > 0) {
|
||||
if (!set_pwm(&runtime, config.kick_pwm))
|
||||
sleep_ms((unsigned int)config.kick_ms);
|
||||
}
|
||||
|
||||
if (!stop_requested &&
|
||||
(read_long_file(runtime.fan_file, &actual_pwm) ||
|
||||
actual_pwm != target_pwm) && set_pwm(&runtime, target_pwm))
|
||||
exit_code = EXIT_FAILURE;
|
||||
|
||||
runtime.fan_active = target_pwm > 0;
|
||||
if (runtime.rpm_file[0])
|
||||
read_long_file(runtime.rpm_file, &rpm);
|
||||
write_status(&runtime, temp_mc, target_pwm, rpm, NULL);
|
||||
|
||||
if (config.debug)
|
||||
syslog(LOG_DEBUG, "temperature=%lldmC pwm=%d rpm=%ld",
|
||||
(long long)temp_mc, target_pwm, rpm);
|
||||
}
|
||||
|
||||
if (config.once)
|
||||
break;
|
||||
sleep_ms((unsigned int)config.interval * 1000);
|
||||
}
|
||||
|
||||
if (!config.once && set_pwm(&runtime, config.exit_pwm))
|
||||
exit_code = EXIT_FAILURE;
|
||||
unlink(status_file);
|
||||
closelog();
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
@ -6,13 +6,16 @@
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for Fan Controller
|
||||
LUCI_DEPENDS:=+luci +fancontrol
|
||||
LUCI_TITLE:=LuCI support for the generic PWM fan controller
|
||||
LUCI_DEPENDS:=+luci-base +fancontrol
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=JiaY-shi <shi05275@163.com>
|
||||
|
||||
PKG_NAME:=luci-app-fancontrol
|
||||
PKG_VERSION:=1.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=2.0.0
|
||||
PKG_RELEASE:=2
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
|
||||
@ -0,0 +1,164 @@
|
||||
.fancontrol-runtime {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.fancontrol-status {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(127, 127, 127, .28);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.fancontrol-status-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 0;
|
||||
padding: .4rem .7rem;
|
||||
background: rgba(127, 127, 127, .08);
|
||||
border-bottom: 1px solid rgba(127, 127, 127, .22);
|
||||
}
|
||||
|
||||
.fancontrol-status-title {
|
||||
font-size: .88rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fancontrol-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.fancontrol-metric {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: .55rem .7rem;
|
||||
border-right: 1px solid rgba(127, 127, 127, .2);
|
||||
}
|
||||
|
||||
.fancontrol-metric:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.fancontrol-metric-label,
|
||||
.fancontrol-detail-label {
|
||||
display: block;
|
||||
color: var(--text-color-medium, inherit);
|
||||
font-size: .72rem;
|
||||
}
|
||||
|
||||
.fancontrol-metric-reading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: .3rem;
|
||||
min-height: 0;
|
||||
margin-top: .12rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fancontrol-metric-value {
|
||||
font-size: 1.18rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.fancontrol-metric-unit {
|
||||
overflow: hidden;
|
||||
color: var(--text-color-medium, inherit);
|
||||
font-size: .72rem;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fancontrol-level {
|
||||
height: 4px;
|
||||
margin-top: .35rem;
|
||||
overflow: hidden;
|
||||
background: rgba(127, 127, 127, .2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.fancontrol-level > div {
|
||||
height: 100%;
|
||||
background: var(--primary-color-medium, #1976d2);
|
||||
border-radius: 3px;
|
||||
transition: width .25s ease-in;
|
||||
}
|
||||
|
||||
.fancontrol-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: .3rem 1rem;
|
||||
padding: .42rem .7rem;
|
||||
background: rgba(127, 127, 127, .04);
|
||||
border-top: 1px solid rgba(127, 127, 127, .2);
|
||||
}
|
||||
|
||||
.fancontrol-detail {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(7rem, .34fr) minmax(0, 1fr);
|
||||
align-items: baseline;
|
||||
gap: .45rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fancontrol-detail:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.fancontrol-detail-value {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.fancontrol-ok {
|
||||
color: var(--success-color-medium, #00884a);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.fancontrol-metrics {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.fancontrol-metric:nth-child(2) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.fancontrol-metric:last-child {
|
||||
grid-column: 1 / -1;
|
||||
border-top: 1px solid rgba(127, 127, 127, .2);
|
||||
}
|
||||
|
||||
.fancontrol-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.fancontrol-detail:last-child {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.fancontrol-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.fancontrol-metric,
|
||||
.fancontrol-metric:nth-child(2) {
|
||||
border-right: 0;
|
||||
border-top: 1px solid rgba(127, 127, 127, .2);
|
||||
}
|
||||
|
||||
.fancontrol-metric:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.fancontrol-metric:last-child {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.fancontrol-detail {
|
||||
grid-template-columns: 1fr;
|
||||
gap: .15rem;
|
||||
}
|
||||
}
|
||||
@ -1,50 +1,270 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require dom';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require tools.widgets as widgets';
|
||||
'require view';
|
||||
|
||||
const callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: [ 'name' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function serviceRunning(result) {
|
||||
const instances = result?.fancontrol?.instances || {};
|
||||
|
||||
return Object.keys(instances).some(function(name) {
|
||||
return instances[name].running === true;
|
||||
});
|
||||
}
|
||||
|
||||
function parseStatus(text) {
|
||||
const status = {};
|
||||
|
||||
String(text || '').split(/\n/).forEach(function(line) {
|
||||
const separator = line.indexOf('=');
|
||||
|
||||
if (separator > 0)
|
||||
status[line.substring(0, separator)] = line.substring(separator + 1);
|
||||
});
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
function statusMetric(title, value, unit, level) {
|
||||
const children = [
|
||||
E('span', { 'class': 'fancontrol-metric-label' }, title),
|
||||
E('div', { 'class': 'fancontrol-metric-reading' }, [
|
||||
E('span', { 'class': 'fancontrol-metric-value' }, value),
|
||||
unit ? E('span', { 'class': 'fancontrol-metric-unit' }, unit) : ''
|
||||
])
|
||||
];
|
||||
|
||||
if (level != null)
|
||||
children.push(E('div', { 'class': 'fancontrol-level', 'aria-hidden': 'true' },
|
||||
E('div', { 'style': 'width:%d%%'.format(level) })));
|
||||
|
||||
return E('div', { 'class': 'fancontrol-metric' }, children);
|
||||
}
|
||||
|
||||
function statusDetail(title, value) {
|
||||
return E('div', { 'class': 'fancontrol-detail' }, [
|
||||
E('span', { 'class': 'fancontrol-detail-label' }, title),
|
||||
E('span', { 'class': 'fancontrol-detail-value' }, value)
|
||||
]);
|
||||
}
|
||||
|
||||
function renderStatus(node, running, status) {
|
||||
const temp = Number(status.temperature_mc);
|
||||
const pwm = Number(status.pwm);
|
||||
const rpm = Number(status.rpm);
|
||||
const tempValid = Number.isFinite(temp) && temp >= 0;
|
||||
const pwmValid = Number.isFinite(pwm) && pwm >= 0;
|
||||
const rpmValid = Number.isFinite(rpm) && rpm >= 0;
|
||||
const pwmPercent = pwmValid ? Math.round(pwm * 100 / 255) : null;
|
||||
const fault = status.error
|
||||
? E('span', { 'class': 'label warning' }, status.error)
|
||||
: E('span', { 'class': 'fancontrol-ok' }, _('None'));
|
||||
|
||||
dom.content(node, E('div', { 'class': 'fancontrol-status' }, [
|
||||
E('div', { 'class': 'fancontrol-status-head' }, [
|
||||
E('span', { 'class': 'fancontrol-status-title' }, _('Runtime Status')),
|
||||
E('span', { 'class': running ? 'label success' : 'label warning' },
|
||||
running ? _('Running') : _('Stopped'))
|
||||
]),
|
||||
E('div', { 'class': 'fancontrol-metrics' }, [
|
||||
statusMetric(_('Temperature'), tempValid ? '%.1f'.format(temp / 1000) : '-',
|
||||
tempValid ? '°C' : ''),
|
||||
statusMetric(_('PWM output'), pwmValid ? String(pwm) : '-',
|
||||
pwmValid ? '/ 255 · %d%%'.format(pwmPercent) : '', pwmPercent),
|
||||
statusMetric(_('Fan speed'), rpmValid ? String(rpm) : '-', rpmValid ? 'RPM' : '')
|
||||
]),
|
||||
E('div', { 'class': 'fancontrol-details' }, [
|
||||
statusDetail(_('Temperature input'), status.thermal_file || '-'),
|
||||
statusDetail(_('PWM device'), status.fan_file || '-'),
|
||||
statusDetail(_('Fault'), fault)
|
||||
])
|
||||
]));
|
||||
}
|
||||
|
||||
function validateAutoPath(sectionId, value) {
|
||||
if (value === 'auto' || value?.startsWith('/'))
|
||||
return true;
|
||||
|
||||
return _('Enter "auto" or an absolute sysfs path.');
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
uci.load('fancontrol')
|
||||
]);
|
||||
load() {
|
||||
return uci.load('fancontrol');
|
||||
},
|
||||
render: async function (data) {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('fancontrol', _('Fan General Control'));
|
||||
s = m.section(form.TypedSection, 'fancontrol', _('Settings'));
|
||||
render() {
|
||||
let m, s, o;
|
||||
let startTemp, fullTemp, startSpeed, maxSpeed, kickSpeed, kickMs;
|
||||
|
||||
m = new form.Map('fancontrol', _('Fan Control'),
|
||||
_('Configure a continuous PWM curve based on device temperature. Automatic hardware detection is suitable for most devices.'));
|
||||
|
||||
s = m.section(form.TypedSection, 'fancontrol', _('Runtime Status'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
s.render = function() {
|
||||
const node = E('div', { 'class': 'cbi-section fancontrol-runtime' }, [
|
||||
E('link', {
|
||||
'rel': 'stylesheet',
|
||||
'href': L.resource('view/fancontrol.css')
|
||||
}),
|
||||
E('div', { 'class': 'cbi-section-node' }, _('Collecting data...'))
|
||||
]);
|
||||
const statusNode = node.lastElementChild;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'), _('Enabled'));
|
||||
poll.add(function() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(callServiceList('fancontrol'), {}),
|
||||
L.resolveDefault(fs.read_direct('/var/run/fancontrol.status'), '')
|
||||
]).then(function(result) {
|
||||
renderStatus(statusNode, serviceRunning(result[0]), parseStatus(result[1]));
|
||||
});
|
||||
}, 5);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
s = m.section(form.NamedSection, 'settings', 'fancontrol', _('Settings'));
|
||||
s.addremove = false;
|
||||
s.tab('curve', _('Temperature Curve'),
|
||||
_('Controls when the fan starts and how its speed rises with temperature.'));
|
||||
s.tab('hardware', _('Hardware'),
|
||||
_('Automatic detection works on most devices. Select explicit sysfs entries only when necessary.'));
|
||||
s.tab('safety', _('Safety'),
|
||||
_('Controls startup assistance and fail-safe behavior. Full-speed safety defaults are recommended.'));
|
||||
|
||||
o = s.taboption('curve', form.Flag, 'enabled', _('Enable'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
o.description = _('Run the controller and apply this temperature curve.');
|
||||
|
||||
o = s.option(form.Value, 'thermal_file', _('Thermal File'), _('Thermal File'));
|
||||
o.placeholder = '/sys/devices/virtual/thermal/thermal_zone0/temp';
|
||||
var temp_div = uci.get('fancontrol', 'settings', 'temp_div');
|
||||
var temp = parseInt(await fs.read_direct(uci.get('fancontrol', 'settings', 'thermal_file')));
|
||||
if (temp_div > 0 && temp > 0) {
|
||||
o.description = _('Current temperature:') + ' <b>' + (temp / temp_div) + '°C</b>';
|
||||
} else {
|
||||
o.description = _('Thermal File');
|
||||
}
|
||||
startTemp = s.taboption('curve', form.Value, 'start_temp', _('Start temperature'));
|
||||
startTemp.default = '45';
|
||||
startTemp.datatype = 'range(-40,200)';
|
||||
startTemp.rmempty = false;
|
||||
startTemp.description = _('The fan remains off below this temperature and starts at the minimum running PWM when it is reached. Unit: °C.');
|
||||
|
||||
o = s.option(form.Value, 'fan_file', _('Fan File'), _('Fan Speed File'));
|
||||
o.placeholder = '/sys/devices/virtual/thermal/cooling_device0/cur_state';
|
||||
var speed = parseInt(await fs.read_direct(uci.get('fancontrol', 'settings', 'fan_file')));
|
||||
o.description = _('Current speed:') + ' <b>' + (speed) + '</b>';
|
||||
fullTemp = s.taboption('curve', form.Value, 'full_speed_temp', _('Full-speed temperature'));
|
||||
fullTemp.default = '85';
|
||||
fullTemp.datatype = 'range(-39,250)';
|
||||
fullTemp.rmempty = false;
|
||||
fullTemp.description = _('PWM rises linearly and reaches the configured maximum at this temperature. Unit: °C.');
|
||||
fullTemp.validate = function(sectionId, value) {
|
||||
if (Number(value) <= Number(startTemp.formvalue(sectionId)))
|
||||
return _('Full-speed temperature must be higher than start temperature.');
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'start_speed', _('Initial Speed'), _('Please enter the initial speed for fan startup.'));
|
||||
o.placeholder = '35';
|
||||
o = s.taboption('curve', form.Value, 'hysteresis', _('Stop hysteresis'));
|
||||
o.default = '3';
|
||||
o.datatype = 'range(0,50)';
|
||||
o.rmempty = false;
|
||||
o.description = _('After starting, the fan stops only after temperature falls this many degrees below the start temperature. This prevents rapid on/off cycling.');
|
||||
|
||||
o = s.option(form.Value, 'max_speed', _('Max Speed'), _('Please enter maximum fan speed.'));
|
||||
o.placeholder = '255';
|
||||
startSpeed = s.taboption('curve', form.Value, 'start_speed', _('Minimum running PWM'));
|
||||
startSpeed.default = '64';
|
||||
startSpeed.datatype = 'range(1,255)';
|
||||
startSpeed.rmempty = false;
|
||||
startSpeed.description = _('Lowest PWM used while the fan is running. Increase it if the fan stalls or cannot maintain rotation. Range: 1–255.');
|
||||
|
||||
o = s.option(form.Value, 'start_temp', _('Start Temperature'), _('Please enter the fan start temperature.'));
|
||||
o.placeholder = '45';
|
||||
maxSpeed = s.taboption('curve', form.Value, 'max_speed', _('Maximum PWM'));
|
||||
maxSpeed.default = '255';
|
||||
maxSpeed.datatype = 'range(1,255)';
|
||||
maxSpeed.rmempty = false;
|
||||
maxSpeed.description = _('Highest PWM allowed by the curve. A value of 255 means 100% duty cycle.');
|
||||
maxSpeed.validate = function(sectionId, value) {
|
||||
if (Number(value) < Number(startSpeed.formvalue(sectionId)))
|
||||
return _('Maximum PWM must not be lower than minimum running PWM.');
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.taboption('curve', form.Value, 'interval', _('Polling interval'));
|
||||
o.default = '5';
|
||||
o.datatype = 'range(1,300)';
|
||||
o.rmempty = false;
|
||||
o.description = _('How often the temperature is read and the PWM output is updated. Unit: seconds.');
|
||||
|
||||
o = s.taboption('hardware', form.Value, 'thermal_file', _('Temperature input'));
|
||||
o.default = 'auto';
|
||||
o.rmempty = false;
|
||||
o.validate = validateAutoPath;
|
||||
o.description = _('Use "auto" to select a CPU or SoC thermal sensor, or enter an absolute thermal zone or hwmon tempN_input path.');
|
||||
|
||||
o = s.taboption('hardware', form.Value, 'thermal_zone', _('Thermal zone type'));
|
||||
o.default = 'auto';
|
||||
o.rmempty = false;
|
||||
o.description = _('Preferred thermal zone type when temperature input is automatic, for example cpu_top_thermal. Leave as "auto" unless several sensors are available.');
|
||||
|
||||
o = s.taboption('hardware', form.Value, 'fan_file', _('PWM output'));
|
||||
o.default = 'auto';
|
||||
o.rmempty = false;
|
||||
o.validate = validateAutoPath;
|
||||
o.description = _('Use "auto" to select a writable hwmon pwmN output, or enter its absolute path. cooling_device cur_state is not a PWM output.');
|
||||
|
||||
o = s.taboption('hardware', form.Value, 'fan_hwmon', _('Hwmon device name'));
|
||||
o.default = 'auto';
|
||||
o.rmempty = false;
|
||||
o.description = _('Preferred hwmon name when PWM output is automatic. The pwmfan driver is preferred by default.');
|
||||
|
||||
o = s.taboption('hardware', form.Value, 'enable_file', _('PWM mode control'));
|
||||
o.default = 'auto';
|
||||
o.rmempty = false;
|
||||
o.validate = validateAutoPath;
|
||||
o.description = _('Optional pwmN_enable path used to select manual PWM mode. With "auto", the matching control is used when available.');
|
||||
|
||||
o = s.taboption('hardware', form.Value, 'temp_div', _('Temperature divisor'));
|
||||
o.default = '1000';
|
||||
o.value('1');
|
||||
o.value('1000');
|
||||
o.datatype = 'range(1,1000000)';
|
||||
o.rmempty = false;
|
||||
o.description = _('Divides the raw sensor value before control calculations. Standard Linux temperature inputs use 1000; sensors reporting whole degrees use 1.');
|
||||
|
||||
kickSpeed = s.taboption('safety', form.Value, 'kick_speed', _('Start kick PWM'));
|
||||
kickSpeed.default = '255';
|
||||
kickSpeed.datatype = 'range(0,255)';
|
||||
kickSpeed.rmempty = false;
|
||||
kickSpeed.description = _('Brief PWM applied when a stopped fan starts. It helps fans that cannot start reliably at low duty. Set to 0 together with a zero duration to disable.');
|
||||
|
||||
kickMs = s.taboption('safety', form.Value, 'kick_ms', _('Start kick duration'));
|
||||
kickMs.default = '500';
|
||||
kickMs.datatype = 'range(0,10000)';
|
||||
kickMs.rmempty = false;
|
||||
kickMs.description = _('How long the startup kick is applied. Unit: milliseconds.');
|
||||
kickMs.validate = function(sectionId, value) {
|
||||
if (Number(value) > 0 && Number(kickSpeed.formvalue(sectionId)) === 0)
|
||||
return _('Start kick PWM must be non-zero when a kick duration is configured.');
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.taboption('safety', form.Value, 'fail_safe_speed', _('Sensor failure PWM'));
|
||||
o.default = '255';
|
||||
o.datatype = 'range(0,255)';
|
||||
o.rmempty = false;
|
||||
o.description = _('PWM used when the temperature sensor cannot be read. Keeping 255 is the safest choice.');
|
||||
|
||||
o = s.taboption('safety', form.Value, 'exit_speed', _('Service exit PWM'));
|
||||
o.default = '255';
|
||||
o.datatype = 'range(0,255)';
|
||||
o.rmempty = false;
|
||||
o.description = _('PWM written when the service stops. Keeping the fan at full speed avoids losing cooling after an unexpected exit.');
|
||||
|
||||
o = s.taboption('safety', form.Flag, 'debug', _('Debug logging'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
o.description = _('Write temperature, PWM and RPM values to the system log at every polling interval.');
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,38 +1,287 @@
|
||||
msgid "Fan General Control"
|
||||
msgstr "风扇通用控制小程序"
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: luci-app-fancontrol 2.0.0\n"
|
||||
"PO-Revision-Date: 2026-07-30 00:00+0800\n"
|
||||
"Last-Translator: JiaY-shi <shi05275@163.com>\n"
|
||||
"Language-Team: Chinese (Simplified Han script)\n"
|
||||
"Language: zh_Hans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "是否开启"
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:173
|
||||
msgid ""
|
||||
"After starting, the fan stops only after temperature falls this many degrees "
|
||||
"below the start temperature. This prevents rapid on/off cycling."
|
||||
msgstr "风扇启动后,温度需要降到比启动温度低这么多度时才会停转,避免风扇频繁启停。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:143
|
||||
msgid ""
|
||||
"Automatic detection works on most devices. Select explicit sysfs entries "
|
||||
"only when necessary."
|
||||
msgstr "自动检测适用于大多数设备,仅在必要时手动指定 sysfs 项。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:238
|
||||
msgid ""
|
||||
"Brief PWM applied when a stopped fan starts. It helps fans that cannot start "
|
||||
"reliably at low duty. Set to 0 together with a zero duration to disable."
|
||||
msgstr "风扇从停止状态启动时短暂使用的 PWM,可帮助低占空比下无法可靠启动的风扇。将此项和助推时长都设为 0 可禁用。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:122
|
||||
msgid "Collecting data..."
|
||||
msgstr "正在采集数据..."
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:111
|
||||
msgid ""
|
||||
"Configure a continuous PWM curve based on device temperature. Automatic "
|
||||
"hardware detection is suitable for most devices."
|
||||
msgstr "根据设备温度配置连续 PWM 调速曲线。自动硬件检测适用于大多数设备。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:145
|
||||
msgid ""
|
||||
"Controls startup assistance and fail-safe behavior. Full-speed safety "
|
||||
"defaults are recommended."
|
||||
msgstr "控制启动助推和故障保护行为,建议保留默认的全速安全设置。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:141
|
||||
msgid "Controls when the fan starts and how its speed rises with temperature."
|
||||
msgstr "控制风扇何时启动,以及转速如何随温度升高。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:263
|
||||
msgid "Debug logging"
|
||||
msgstr "调试日志"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:232
|
||||
msgid ""
|
||||
"Divides the raw sensor value before control calculations. Standard Linux "
|
||||
"temperature inputs use 1000; sensors reporting whole degrees use 1."
|
||||
msgstr "进行控制计算前用于换算传感器原始值。标准 Linux 温度输入使用 1000,直接报告摄氏度的传感器使用 1。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:147
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:98
|
||||
msgid "Enter \"auto\" or an absolute sysfs path."
|
||||
msgstr "请输入 \"auto\" 或 sysfs 绝对路径。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:110
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/root/usr/share/luci/menu.d/luci-app-fancontrol.json:3
|
||||
msgid "Fan Control"
|
||||
msgstr "风扇控制"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:84
|
||||
msgid "Fan speed"
|
||||
msgstr "风扇转速"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:89
|
||||
msgid "Fault"
|
||||
msgstr "故障"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:158
|
||||
msgid "Full-speed temperature"
|
||||
msgstr "全速温度"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:165
|
||||
msgid "Full-speed temperature must be higher than start temperature."
|
||||
msgstr "全速温度必须高于启动温度。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/root/usr/share/rpcd/acl.d/luci-app-fancontrol.json:3
|
||||
msgid "Grant access to Fan Control"
|
||||
msgstr "授予风扇控制访问权限"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:142
|
||||
msgid "Hardware"
|
||||
msgstr "硬件"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:185
|
||||
msgid "Highest PWM allowed by the curve. A value of 255 means 100% duty cycle."
|
||||
msgstr "温控曲线允许的最高 PWM,255 表示 100% 占空比。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:244
|
||||
msgid "How long the startup kick is applied. Unit: milliseconds."
|
||||
msgstr "启动助推持续的时间,单位为毫秒。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:196
|
||||
msgid ""
|
||||
"How often the temperature is read and the PWM output is updated. Unit: "
|
||||
"seconds."
|
||||
msgstr "读取温度并更新 PWM 输出的时间间隔,单位为秒。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:215
|
||||
msgid "Hwmon device name"
|
||||
msgstr "Hwmon 设备名称"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:179
|
||||
msgid ""
|
||||
"Lowest PWM used while the fan is running. Increase it if the fan stalls or "
|
||||
"cannot maintain rotation. Range: 1–255."
|
||||
msgstr "风扇运行时使用的最低 PWM。如果风扇停转或无法持续旋转,请提高此值。范围:1–255。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:181
|
||||
msgid "Maximum PWM"
|
||||
msgstr "最大 PWM"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:188
|
||||
msgid "Maximum PWM must not be lower than minimum running PWM."
|
||||
msgstr "最大 PWM 不得低于最低运行 PWM。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:175
|
||||
msgid "Minimum running PWM"
|
||||
msgstr "最低运行 PWM"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:71
|
||||
msgid "None"
|
||||
msgstr "无"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:224
|
||||
msgid ""
|
||||
"Optional pwmN_enable path used to select manual PWM mode. With \"auto\", the "
|
||||
"matching control is used when available."
|
||||
msgstr "用于选择手动 PWM 模式的可选 pwmN_enable 路径。设为 \"auto\" 时会自动使用匹配的控制项。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:88
|
||||
msgid "PWM device"
|
||||
msgstr "PWM 设备"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:220
|
||||
msgid "PWM mode control"
|
||||
msgstr "PWM 模式控制"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:82
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:209
|
||||
msgid "PWM output"
|
||||
msgstr "PWM 输出"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:162
|
||||
msgid ""
|
||||
"PWM rises linearly and reaches the configured maximum at this temperature. "
|
||||
"Unit: °C."
|
||||
msgstr "PWM 随温度线性升高,并在此温度达到设定的最大值,单位为 °C。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:255
|
||||
msgid ""
|
||||
"PWM used when the temperature sensor cannot be read. Keeping 255 is the "
|
||||
"safest choice."
|
||||
msgstr "温度传感器读取失败时使用的 PWM。保持 255 是最安全的选择。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:261
|
||||
msgid ""
|
||||
"PWM written when the service stops. Keeping the fan at full speed avoids "
|
||||
"losing cooling after an unexpected exit."
|
||||
msgstr "服务停止时写入的 PWM。保持风扇全速可避免服务意外退出后失去散热。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:192
|
||||
msgid "Polling interval"
|
||||
msgstr "轮询间隔"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:218
|
||||
msgid ""
|
||||
"Preferred hwmon name when PWM output is automatic. The pwmfan driver is "
|
||||
"preferred by default."
|
||||
msgstr "自动选择 PWM 输出时优先使用的 hwmon 名称,默认优先选择 pwmfan 驱动。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:207
|
||||
msgid ""
|
||||
"Preferred thermal zone type when temperature input is automatic, for example "
|
||||
"cpu_top_thermal. Leave as \"auto\" unless several sensors are available."
|
||||
msgstr "自动选择温度输入时优先使用的 thermal zone 类型,例如 cpu_top_thermal。除非存在多个传感器,否则保持 \"auto\"。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:150
|
||||
msgid "Run the controller and apply this temperature curve."
|
||||
msgstr "运行风扇控制器并应用此温控曲线。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:77
|
||||
msgid "Running"
|
||||
msgstr "运行中"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:75
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:113
|
||||
msgid "Runtime Status"
|
||||
msgstr "运行状态"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:144
|
||||
msgid "Safety"
|
||||
msgstr "安全"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:251
|
||||
msgid "Sensor failure PWM"
|
||||
msgstr "传感器故障 PWM"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:257
|
||||
msgid "Service exit PWM"
|
||||
msgstr "服务退出 PWM"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:138
|
||||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
msgid "Thermal File"
|
||||
msgstr "温度虚拟文件"
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:234
|
||||
msgid "Start kick PWM"
|
||||
msgstr "启动助推 PWM"
|
||||
|
||||
msgid "Fan Speed File"
|
||||
msgstr "风扇速度虚拟文件"
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:247
|
||||
msgid "Start kick PWM must be non-zero when a kick duration is configured."
|
||||
msgstr "配置启动助推时长时,启动助推 PWM 不能为零。"
|
||||
|
||||
msgid "Initial Speed"
|
||||
msgstr "启动初始速度"
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:240
|
||||
msgid "Start kick duration"
|
||||
msgstr "启动助推时长"
|
||||
|
||||
msgid "Please enter the initial speed for fan startup."
|
||||
msgstr "请输入风扇启动的初始速度。"
|
||||
|
||||
msgid "Max Speed"
|
||||
msgstr "最大速度"
|
||||
|
||||
msgid "Please enter maximum fan speed."
|
||||
msgstr "请输入风扇最大速度"
|
||||
|
||||
msgid "Start Temperature"
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:152
|
||||
msgid "Start temperature"
|
||||
msgstr "启动温度"
|
||||
|
||||
msgid "Please enter the fan start temperature."
|
||||
msgstr "请输入风扇启动温度。"
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:169
|
||||
msgid "Stop hysteresis"
|
||||
msgstr "停转回差"
|
||||
|
||||
msgid "Current speed:"
|
||||
msgstr "当前速度:"
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:77
|
||||
msgid "Stopped"
|
||||
msgstr "已停止"
|
||||
|
||||
msgid "Current temperature:"
|
||||
msgstr "当前温度:"
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:80
|
||||
msgid "Temperature"
|
||||
msgstr "温度"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:140
|
||||
msgid "Temperature Curve"
|
||||
msgstr "温控曲线"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:226
|
||||
msgid "Temperature divisor"
|
||||
msgstr "温度除数"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:87
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:198
|
||||
msgid "Temperature input"
|
||||
msgstr "温度输入"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:156
|
||||
msgid ""
|
||||
"The fan remains off below this temperature and starts at the minimum running "
|
||||
"PWM when it is reached. Unit: °C."
|
||||
msgstr "低于此温度时风扇保持停止;达到此温度时,以最低运行 PWM 启动。单位为 °C。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:204
|
||||
msgid "Thermal zone type"
|
||||
msgstr "温度区域类型"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:202
|
||||
msgid ""
|
||||
"Use \"auto\" to select a CPU or SoC thermal sensor, or enter an absolute "
|
||||
"thermal zone or hwmon tempN_input path."
|
||||
msgstr "使用 \"auto\" 自动选择 CPU 或 SoC 温度传感器,也可输入 thermal zone 或 hwmon tempN_input 的绝对路径。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:213
|
||||
msgid ""
|
||||
"Use \"auto\" to select a writable hwmon pwmN output, or enter its absolute "
|
||||
"path. cooling_device cur_state is not a PWM output."
|
||||
msgstr "使用 \"auto\" 自动选择可写的 hwmon pwmN 输出,也可输入其绝对路径。cooling_device cur_state 不是 PWM 输出。"
|
||||
|
||||
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:266
|
||||
msgid ""
|
||||
"Write temperature, PWM and RPM values to the system log at every polling "
|
||||
"interval."
|
||||
msgstr "每次轮询时将温度、PWM 和 RPM 数值写入系统日志。"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"admin/services/fancontrol": {
|
||||
"title": "Fan General Control",
|
||||
"title": "Fan Control",
|
||||
"order": 90,
|
||||
"action": {
|
||||
"type": "view",
|
||||
@ -10,4 +10,4 @@
|
||||
"acl": [ "luci-app-fancontrol" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,17 @@
|
||||
{
|
||||
"luci-app-fancontrol": {
|
||||
"description": "Fan General Control",
|
||||
"description": "Grant access to Fan Control",
|
||||
"read": {
|
||||
"uci": [
|
||||
"fancontrol"
|
||||
],
|
||||
"uci": [ "fancontrol" ],
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
},
|
||||
"file": {
|
||||
"/sys/devices/virtual/thermal/*/*": [
|
||||
"read"
|
||||
]
|
||||
"/var/run/fancontrol.status": [ "read" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": [
|
||||
"fancontrol"
|
||||
]
|
||||
"uci": [ "fancontrol" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.7.24
|
||||
PKG_RELEASE:=196
|
||||
PKG_RELEASE:=197
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@ -1745,16 +1745,21 @@ function fetch_cert_sha256(host, port, sni, timeout, http3)
|
||||
end
|
||||
|
||||
function vps_domain_exclude(domain)
|
||||
if trim(domain) == "" then return true end
|
||||
domain = trim(domain)
|
||||
if domain == "" then return true end
|
||||
local map = {
|
||||
["engage.cloudflareclient.com"] = 1,
|
||||
["google.com"] = 1, ["www.google.com"] = 1,
|
||||
["youtube.com"] = 1, ["www.youtube.com"] = 1,
|
||||
["github.com"] = 1, ["telegram.org"] = 1,
|
||||
["cloudflare.com"] = 1, ["www.cloudflare.com"] = 1,
|
||||
["bing.com"] = 1, ["www.bing.com"] = 1, ["x.com"] = 1
|
||||
["engage.cloudflareclient.com"] = 1, ["google.com"] = 1, ["youtube.com"] = 1,
|
||||
["github.com"] = 1, ["telegram.org"] = 1, ["cloudflare.com"] = 1,
|
||||
["bing.com"] = 1, ["x.com"] = 1, ["dns.google"] = 1, ["opendns.com"] = 1,
|
||||
["cloudflare-dns.com"] = 1, ["one.one.one.one"] = 1, ["quad9.net"] = 1,
|
||||
["adguard-dns.com"] = 1, ["nextdns.io"] = 1, ["libredns.gr"] = 1
|
||||
}
|
||||
if map[domain] then return true end
|
||||
while true do
|
||||
if map[domain] then return true end
|
||||
local p = domain:find("%.")
|
||||
if not p then break end
|
||||
domain = domain:sub(p + 1)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
@ -178,6 +178,10 @@ if not is_file_nonzero(file_vpslist) then
|
||||
uci:foreach(appname, "nodes", function(t)
|
||||
process_address(t.address)
|
||||
process_address(t.download_address)
|
||||
local dns, _ = api.get_domain_port_from_url(t.domain_resolver_dns or t.domain_resolver_dns_https or "")
|
||||
if dns and dns ~= "" then
|
||||
process_address(dns)
|
||||
end
|
||||
end)
|
||||
uci:foreach(appname, "subscribe_list", function(t) --订阅链接
|
||||
local url, _ = api.get_domain_port_from_url(t.url or "")
|
||||
|
||||
@ -381,6 +381,10 @@ function add_rule(var)
|
||||
uci:foreach(appname, "nodes", function(t)
|
||||
process_address(t.address)
|
||||
process_address(t.download_address)
|
||||
local dns, _ = api.get_domain_port_from_url(t.domain_resolver_dns or t.domain_resolver_dns_https or "")
|
||||
if dns and dns ~= "" then
|
||||
process_address(dns)
|
||||
end
|
||||
end)
|
||||
uci:foreach(appname, "subscribe_list", function(t) --订阅链接
|
||||
local url, _ = api.get_domain_port_from_url(t.url or "")
|
||||
|
||||
@ -337,6 +337,10 @@ if not is_file_nonzero(file_vpslist) then
|
||||
uci:foreach(appname, "nodes", function(t)
|
||||
process_address(t.address)
|
||||
process_address(t.download_address)
|
||||
local dns, _ = api.get_domain_port_from_url(t.domain_resolver_dns or t.domain_resolver_dns_https or "")
|
||||
if dns and dns ~= "" then
|
||||
process_address(dns)
|
||||
end
|
||||
end)
|
||||
uci:foreach(appname, "subscribe_list", function(t) --订阅链接
|
||||
local url, _ = api.get_domain_port_from_url(t.url or "")
|
||||
|
||||
@ -747,13 +747,13 @@ filter_haproxy() {
|
||||
|
||||
filter_vpsip() {
|
||||
local EXCLUDE_VPSIP="^(0\.0\.0\.0|127\.0\.0\.1|1\.1\.1\.1|1\.1\.1\.2|8\.8\.8\.8|8\.8\.4\.4|9\.9\.9\.9)$"
|
||||
uci show $CONFIG | grep -E "(\.address=|\.download_address=)" | cut -d "'" -f 2 | grep -E "^([0-9]{1,3}\.){3}[0-9]{1,3}$" | grep -Ev "$EXCLUDE_VPSIP" | sed "s/^/add $IPSET_VPS /" | awk '1; END{print "COMMIT"}' | ipset -! -R
|
||||
uci show $CONFIG | grep -E "(\.address=|\.download_address=|\.domain_resolver_dns=|\.domain_resolver_dns_https=)" | cut -d "'" -f 2 | grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -Ev "$EXCLUDE_VPSIP" | sed "s/^/add $IPSET_VPS /" | awk '1; END{print "COMMIT"}' | ipset -! -R
|
||||
echolog " - [$?]加入所有IPv4节点到ipset[$IPSET_VPS]直连完成"
|
||||
uci show $CONFIG | grep -E "(\.address=|\.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed "s/^/add $IPSET_VPS6 /" | awk '1; END{print "COMMIT"}' | ipset -! -R
|
||||
uci show $CONFIG | grep -E "(\.address=|\.download_address=|\.domain_resolver_dns=|\.domain_resolver_dns_https=)" | cut -d "'" -f 2 | grep -Eo "\[?[A-Fa-f0-9:]*:[A-Fa-f0-9:]+\]?" | sed "s/^/add $IPSET_VPS6 /" | awk '1; END{print "COMMIT"}' | ipset -! -R
|
||||
echolog " - [$?]加入所有IPv6节点到ipset[$IPSET_VPS6]直连完成"
|
||||
#订阅方式为直连时
|
||||
get_subscribe_host | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -Ev "$EXCLUDE_VPSIP" | sed "s/^/add $IPSET_VPS /" | awk '{print $0} END{print "COMMIT"}' | ipset -! -R
|
||||
get_subscribe_host | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed "s/^/add $IPSET_VPS6 /" | awk '{print $0} END{print "COMMIT"}' | ipset -! -R
|
||||
get_subscribe_host | grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -Ev "$EXCLUDE_VPSIP" | sed "s/^/add $IPSET_VPS /" | awk '{print $0} END{print "COMMIT"}' | ipset -! -R
|
||||
get_subscribe_host | grep -Eo "\[?[A-Fa-f0-9:]*:[A-Fa-f0-9:]+\]?" | sed "s/^/add $IPSET_VPS6 /" | awk '{print $0} END{print "COMMIT"}' | ipset -! -R
|
||||
}
|
||||
|
||||
filter_server_port() {
|
||||
|
||||
@ -817,13 +817,13 @@ filter_vps_addr() {
|
||||
|
||||
filter_vpsip() {
|
||||
local EXCLUDE_VPSIP="^(0\.0\.0\.0|127\.0\.0\.1|1\.1\.1\.1|1\.1\.1\.2|8\.8\.8\.8|8\.8\.4\.4|9\.9\.9\.9)$"
|
||||
uci show $CONFIG | grep -E "(\.address=|\.download_address=)" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -Ev "$EXCLUDE_VPSIP" | insert_nftset $NFTSET_VPS
|
||||
uci show $CONFIG | grep -E "(\.address=|\.download_address=|\.domain_resolver_dns=|\.domain_resolver_dns_https=)" | cut -d "'" -f 2 | grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -Ev "$EXCLUDE_VPSIP" | insert_nftset $NFTSET_VPS
|
||||
echolog " - [$?]加入所有IPv4节点到nftset[$NFTSET_VPS]直连完成"
|
||||
uci show $CONFIG | grep -E "(\.address=|\.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_VPS6
|
||||
uci show $CONFIG | grep -E "(\.address=|\.download_address=|\.domain_resolver_dns=|\.domain_resolver_dns_https=)" | cut -d "'" -f 2 | grep -Eo "\[?[A-Fa-f0-9:]*:[A-Fa-f0-9:]+\]?" | insert_nftset $NFTSET_VPS6
|
||||
echolog " - [$?]加入所有IPv6节点到nftset[$NFTSET_VPS6]直连完成"
|
||||
#订阅方式为直连时
|
||||
get_subscribe_host | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -Ev "$EXCLUDE_VPSIP" | insert_nftset $NFTSET_VPS
|
||||
get_subscribe_host | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_VPS6
|
||||
get_subscribe_host | grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -Ev "$EXCLUDE_VPSIP" | insert_nftset $NFTSET_VPS
|
||||
get_subscribe_host | grep -Eo "\[?[A-Fa-f0-9:]*:[A-Fa-f0-9:]+\]?" | insert_nftset $NFTSET_VPS6
|
||||
}
|
||||
|
||||
filter_server_port() {
|
||||
|
||||
@ -4,7 +4,7 @@ LUCI_TITLE:=Web UI for QEMU Virtual Machine Simple (VMS)
|
||||
LUCI_DEPENDS:=@TARGET_x86_64 +qemu-vms +ttyd +usbutils +pciutils
|
||||
PKG_LICENSE:=GPLv3
|
||||
PKG_VERSION:=0.0.1
|
||||
PKG_RELEASE:=5
|
||||
PKG_RELEASE:=6
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@ -20,7 +20,8 @@ return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
callListNetDrivers(),
|
||||
uci.load('qemu-vms')
|
||||
uci.load('qemu-vms'),
|
||||
uci.load('network')
|
||||
]);
|
||||
},
|
||||
|
||||
@ -63,8 +64,21 @@ return view.extend({
|
||||
return true;
|
||||
};
|
||||
|
||||
// В render:
|
||||
var bridgeList = [];
|
||||
uci.sections('network', 'device').forEach(function(s) {
|
||||
if (s.type === 'bridge') {
|
||||
bridgeList.push(s.name);
|
||||
}
|
||||
});
|
||||
|
||||
var bridge = s.option(form.Value, 'bridge', _('Bridge (optional)'));
|
||||
bridge.placeholder = _('leave empty to keep the interface standalone');
|
||||
bridge.value('', _('leave empty'));
|
||||
bridgeList.forEach(function(name) {
|
||||
bridge.value(name, name);
|
||||
});
|
||||
bridge.rmempty = true;
|
||||
//bridge.nocreate = false;
|
||||
|
||||
var driver = s.option(form.ListValue, 'driver', _('Network driver'));
|
||||
|
||||
@ -81,4 +95,4 @@ return view.extend({
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -432,7 +432,7 @@ return view.extend({
|
||||
_('PCI passthrough requires IOMMU enabled in the host BIOS and kernel command line (intel_iommu=on / amd_iommu=on), and the vfio-pci kernel module loaded.') +
|
||||
'<br />' + _('Creating a passthrough section will bind the device to vfio-pci (if possible). Removing it will attempt to re-bind to the original driver.') +
|
||||
'<br />' + _('Note: Some devices (especially Intel network cards) may require the vfio-pci module option "disable_idle_d3=1" to prevent them from entering a low-power state.') +
|
||||
'<br />' + _('Add "options vfio-pci disable_idle_d3=1" to /etc/modprobe.d/vfio-pci.conf if you encounter "No such device" errors.')),
|
||||
'<br />' + _('Change to "vfio-pci disable_idle_d3=1" in /etc/modules.d/vfio-pci if you encounter "No such device" errors.')),
|
||||
this.renderPciTable(hw.pci || [])
|
||||
]);
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
. /lib/functions.sh
|
||||
|
||||
STATUS_DIR="/var/run/qemu-vms"
|
||||
LOG_DIR="/var/log/qemu-vms"
|
||||
|
||||
is_anonymous_section() {
|
||||
# UCI auto-generated names look like: cfgXXXXXX (cfg + hex)
|
||||
@ -85,7 +86,7 @@ find_free_port() {
|
||||
|
||||
console_open() {
|
||||
local vm="$1"
|
||||
local sock="/var/run/qemu-$vm.sock"
|
||||
local sock="$STATUS_DIR/qemu-$vm.sock"
|
||||
local pidfile="$STATUS_DIR/$vm.ttyd.pid"
|
||||
local portfile="$STATUS_DIR/$vm.ttyd.port"
|
||||
|
||||
@ -145,7 +146,7 @@ console_close() {
|
||||
readlog() {
|
||||
local vm="$1"
|
||||
local lines="${2:-200}"
|
||||
local log="/var/log/qemu-vms/$vm.log"
|
||||
local log="$LOG_DIR/$vm.log"
|
||||
|
||||
json_init
|
||||
if [ -f "$log" ]; then
|
||||
|
||||
@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=qemu-vms
|
||||
PKG_VERSION:=0.0.1
|
||||
PKG_RELEASE:=5
|
||||
PKG_RELEASE:=6
|
||||
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
@ -15,6 +15,7 @@ config network 'tap_example_bridge'
|
||||
option mac '52:54:00:31:6c:4e'
|
||||
option ifname 'tap1'
|
||||
option bridge 'br-tap'
|
||||
option driver 'pcnet'
|
||||
|
||||
config vm 'vm_example'
|
||||
option enabled '1'
|
||||
@ -34,6 +35,7 @@ config vm 'winxp_example'
|
||||
option display_type 'vnc'
|
||||
option vnc_port '5902'
|
||||
list network 'tap_example_bridge'
|
||||
list usb_passthrough 'example_usb'
|
||||
option cdrom '/mnt/disk/vm/wxp.iso'
|
||||
option image '/mnt/disk/vm/xp.img'
|
||||
option disk_bus 'ide'
|
||||
|
||||
@ -244,9 +244,9 @@ start_vm() {
|
||||
config_get vnc_port "$cfg" vnc_port
|
||||
config_get machine "$cfg" machine "q35"
|
||||
|
||||
local console_sock="/var/run/qemu-$cfg.sock"
|
||||
local console_log="/var/log/qemu-$cfg-console.log"
|
||||
local qmp_sock="/var/run/qemu-$cfg.qmp"
|
||||
local console_sock="$STAUS_DIR/qemu-$cfg.sock"
|
||||
local console_log="$LOG_DIR/qemu-$cfg-console.log"
|
||||
local qmp_sock="$STATUS_DIR/qemu-$cfg.qmp"
|
||||
|
||||
build_netargs "$cfg"
|
||||
build_usbargs "$cfg"
|
||||
|
||||
@ -1,22 +1,63 @@
|
||||
#!/bin/sh
|
||||
# /etc/qemu-ifup
|
||||
#
|
||||
# Invoked by QEMU itself (via -netdev tap,...,script=/etc/qemu-ifup) right
|
||||
# after it creates the tap interface. $1 is the tap ifname QEMU created;
|
||||
# $2 (bridge) is passed through via the netdev's "downscript"-adjacent
|
||||
# vhost/helper convention used by build_netargs in /etc/init.d/qemu-vms,
|
||||
# which appends the bridge name as a second positional argument.
|
||||
#
|
||||
# We only enslave the interface into a bridge if one was actually
|
||||
# configured on the network section - a tap interface with no "bridge"
|
||||
# option stays standalone (e.g. for point-to-point or manually managed
|
||||
# setups).
|
||||
# $1 - tap iface name
|
||||
# $2 - bridge name (optional)
|
||||
|
||||
ifname="$1"
|
||||
bridge="$2"
|
||||
. /lib/functions.sh
|
||||
|
||||
ip link set "$ifname" up
|
||||
|
||||
if [ -n "$bridge" ]; then
|
||||
ip link set "$ifname" master "$bridge"
|
||||
if [ -n "$2" ]; then
|
||||
# if custom user bridge
|
||||
if ! ip link show "$2" >/dev/null 2>&1; then
|
||||
ip link add "$2" type bridge
|
||||
ip link set "$2" up
|
||||
logger -t qemu-ifup "Bridge $2 created"
|
||||
fi
|
||||
ip link set "$1" master "$2"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
network_bridges=""
|
||||
|
||||
collect_network_bridge() {
|
||||
local cfg="$1"
|
||||
local type
|
||||
config_get type "$cfg" type
|
||||
[ "$type" = "bridge" ] || return 0
|
||||
local name
|
||||
config_get name "$cfg" name
|
||||
[ -n "$name" ] && network_bridges="$network_bridges $name"
|
||||
}
|
||||
|
||||
config_load network
|
||||
config_foreach collect_network_bridge device
|
||||
|
||||
qemu_bridges=""
|
||||
|
||||
collect_qemu_bridge() {
|
||||
local cfg="$1"
|
||||
local bridge
|
||||
config_get bridge "$cfg" bridge
|
||||
[ -n "$bridge" ] && qemu_bridges="$qemu_bridges $bridge"
|
||||
}
|
||||
|
||||
config_load qemu-vms
|
||||
config_foreach collect_qemu_bridge network
|
||||
|
||||
defined_bridges="$network_bridges $qemu_bridges"
|
||||
|
||||
existing_bridges=$(ls -d /sys/devices/virtual/net/*/brif/ 2>/dev/null | awk -F '/' '{print $(NF-2)}')
|
||||
|
||||
echo "$existing_bridges" | awk -v def="$defined_bridges" '
|
||||
BEGIN {
|
||||
split(def, arr, " ");
|
||||
for (i in arr) declared[arr[i]] = 1;
|
||||
}
|
||||
!($0 in declared) { print }
|
||||
' | while read bridge; do
|
||||
if [ -z "$(ls -A "/sys/devices/virtual/net/$bridge/brif" 2>/dev/null)" ]; then
|
||||
ip link set "$bridge" down 2>/dev/null
|
||||
ip link delete "$bridge" 2>/dev/null
|
||||
logger -t qemu-ifup "Removed orphan bridge $bridge"
|
||||
fi
|
||||
done
|
||||
|
||||
@ -10,6 +10,9 @@
|
||||
# log: just tails the always-on boot/console logfile (safe to run at
|
||||
# any time, does not touch the live console socket at all).
|
||||
|
||||
STATUS_DIR=/var/run/qemu-vms
|
||||
LOG_DIR=/var/log/qemu-vms
|
||||
|
||||
VM="$1"
|
||||
MODE="${2:-attach}"
|
||||
|
||||
@ -18,9 +21,9 @@ if [ -z "$VM" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONSOLE_SOCK="/var/run/qemu-$VM.sock"
|
||||
CONSOLE_LOG="/var/log/qemu-$VM-console.log"
|
||||
TMP_PTY="/var/run/qemu-$VM.console-pty.$$"
|
||||
CONSOLE_SOCK="$STATUS_DIR/qemu-$VM.sock"
|
||||
CONSOLE_LOG="$LOG_DIR/qemu-$VM-console.log"
|
||||
TMP_PTY="$STATUS_DIR/qemu-$VM.console-pty.$$"
|
||||
|
||||
case "$MODE" in
|
||||
log)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user