💋 Sync 2026-07-30 20:36:50

This commit is contained in:
github-actions[bot] committed 2026-07-30 20:36:50 +08:00
1 parent 68d255846f
commit 48cc1b888b
27 files changed
+1661 -350

No files matched your search

+6 -7
View File
@@ -1,9 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=fancontrol PKG_NAME:=fancontrol
PKG_VERSION:=1 PKG_VERSION:=2.0.0
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_LICENSE:=MIT PKG_LICENSE:=MIT
PKG_LICENSE_FILES:=LICENSE PKG_LICENSE_FILES:=LICENSE
@@ -16,16 +15,16 @@ include $(INCLUDE_DIR)/package.mk
define Package/fancontrol define Package/fancontrol
SECTION:=utils SECTION:=utils
CATEGORY:=Utilities CATEGORY:=Utilities
TITLE:=FanControl for OpenWRT TITLE:=Generic hwmon PWM fan controller
endef endef
define Package/fancontrol/description define Package/fancontrol/description
FanControl for OpenWRT. Temperature-based continuous PWM fan controller using the Linux hwmon ABI.
endef endef
define Build/Prepare define Build/Prepare
mkdir -p $(PKG_BUILD_DIR) mkdir -p $(PKG_BUILD_DIR)
$(CP) ./src/* $(PKG_BUILD_DIR) $(CP) ./src/Makefile ./src/fancontrol.c $(PKG_BUILD_DIR)
endef endef
MAKE_FLAGS += \ MAKE_FLAGS += \
@@ -35,7 +34,7 @@ define Package/fancontrol/install
$(INSTALL_DIR) $(1)/usr/bin $(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/fancontrol $(1)/usr/bin/fancontrol $(INSTALL_BIN) $(PKG_BUILD_DIR)/fancontrol $(1)/usr/bin/fancontrol
$(INSTALL_DIR) $(1)/etc/config $(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_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/fancontrol.init $(1)/etc/init.d/fancontrol $(INSTALL_BIN) ./files/fancontrol.init $(1)/etc/init.d/fancontrol
endef endef
+19 -8
View File
@@ -1,8 +1,19 @@
config fancontrol settings config fancontrol 'settings'
option enabled '0' option enabled '0'
option thermal_file '/sys/devices/virtual/thermal/thermal_zone0/temp' option thermal_file 'auto'
option fan_file '/sys/devices/virtual/thermal/cooling_device0/cur_state' option thermal_zone 'auto'
option start_temp '45' option fan_file 'auto'
option start_speed '35' option fan_hwmon 'auto'
option max_speed '255' option enable_file 'auto'
option temp_div '1000' 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'
+40 -42
View File
@@ -5,53 +5,51 @@ STOP=01
USE_PROCD=1 USE_PROCD=1
NAME=fancontrol 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() { start_service() {
config_load "$NAME" local enabled
local debug
local enabled config_load "$NAME"
config_get enabled settings enabled config_get_bool enabled settings enabled 0
[ "$enabled" = "1" ] || { _info "Instance \"$NAME\" disabled."; return 1; } [ "$enabled" -eq 1 ] || return 0
local thermal_file procd_open_instance
local fan_file procd_set_param command "$PROG"
local start_temp append_option thermal_file -T
local start_speed append_option thermal_zone -Z
local max_speed append_option fan_file -F
local temp_div append_option fan_hwmon -N
append_option enable_file -E
config_get thermal_file settings thermal_file append_option start_temp -t
config_get fan_file settings fan_file append_option full_speed_temp -x
config_get start_temp settings start_temp append_option hysteresis -H
config_get start_speed settings start_speed append_option start_speed -s
config_get max_speed settings max_speed append_option max_speed -m
config_get temp_div settings temp_div append_option kick_speed -k
append_option kick_ms -K
procd_open_instance append_option fail_safe_speed -f
procd_set_param command $PROG append_option exit_speed -q
[ -n "$thermal_file" ] && procd_append_param command -T "$thermal_file" append_option temp_div -d
[ -n "$fan_file" ] && procd_append_param command -F "$fan_file" append_option interval -i
[ -n "$start_temp" ] && procd_append_param command -t "$start_temp" config_get_bool debug settings debug 0
[ -n "$start_speed" ] && procd_append_param command -s "$start_speed" [ "$debug" -eq 1 ] && procd_append_param command -D
[ -n "$max_speed" ] && procd_append_param command -m "$max_speed" procd_set_param respawn 3600 5 5
[ -n "$temp_div" ] && procd_append_param command -d "$temp_div" procd_set_param term_timeout 10
procd_set_param respawn procd_set_param file /etc/config/fancontrol
procd_close_instance procd_close_instance
}
start() {
start_service
}
stop() {
procd_kill_instance
}
reload() {
procd_reload_instance
} }
service_triggers() { service_triggers() {
procd_add_reload_trigger "fancontrol" procd_add_reload_trigger fancontrol
} }
+4 -3
View File
@@ -1,5 +1,6 @@
CC := gcc CC ?= cc
CFLAGS := -O3 -Wall -fomit-frame-pointer CFLAGS ?= -O2
CFLAGS += -Wall -Wextra -Wformat=2
all: fancontrol all: fancontrol
@@ -7,7 +8,7 @@ fancontrol: %: %.o
$(CC) $(LDFLAGS) -o $@ $^ $(CC) $(LDFLAGS) -o $@ $^
%.o: %.c %.o: %.c
$(CC) $(CFLAGS) $(INCLUDE) -o $@ -c $< $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ -c $<
clean: clean:
rm -f fancontrol *.o *~ rm -f fancontrol *.o *~
+762 -171
View File
@@ -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 <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <signal.h> #include <syslog.h>
#include <time.h>
#include <unistd.h>
#define MAX_LENGTH 200 #define PROGRAM_NAME "fancontrol"
#define MAX_TEMP 120 #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
// 定义全局变量 struct fan_config {
char thermal_file[MAX_LENGTH] = "/sys/devices/virtual/thermal/thermal_zone0/temp"; // -T char thermal_file[PATH_MAX];
char fan_file[MAX_LENGTH] = "/sys/devices/virtual/thermal/cooling_device0/cur_state"; // -F 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 struct fan_runtime {
int start_temp = 45; // -t char thermal_file[PATH_MAX];
int max_speed = 255; // -m char fan_file[PATH_MAX];
int temp_div = 1000; // -d char enable_file[PATH_MAX];
int debug_mode = 0; // -D char rpm_file[PATH_MAX];
bool fan_active;
};
/** static const char *sysfs_root = DEFAULT_SYSFS_ROOT;
* 底层读文件 static const char *status_file = DEFAULT_STATUS_FILE;
*/ static volatile sig_atomic_t stop_requested;
static int read_file(const char* path ,char* result ,size_t size) {
FILE* fp;
char* line = NULL;
size_t len = 0;
ssize_t read;
fp = fopen(path ,"r"); static void usage(FILE *stream, const char *name)
if (fp == NULL) {
return -1; fprintf(stream,
"Usage: %s [options]\n"
if (( read = getline(&line ,&len ,fp) ) != -1) { " -T path temperature sysfs path or 'auto'\n"
if (size != 0) " -Z type preferred thermal zone type or 'auto'\n"
memcpy(result ,line ,size); " -F path PWM sysfs path or 'auto'\n"
else " -N name preferred hwmon name or 'auto'\n"
memcpy(result ,line ,read - 1); " -E path pwm enable sysfs path or 'auto'\n"
} " -t degrees fan start temperature\n"
" -x degrees full-speed temperature\n"
fclose(fp); " -H degrees stop hysteresis\n"
if (line) " -s pwm minimum running PWM (1-255)\n"
free(line); " -m pwm maximum PWM (1-255)\n"
return 0; " -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 int copy_string(char *dest, size_t size, const char *source)
* 底层写文件 {
*/ int len;
static size_t write_file(const char* path ,char* buf ,size_t len) {
FILE* fp = NULL; if (!source || strpbrk(source, "\r\n"))
size_t size = 0; return -1;
fp = fopen(path ,"w+");
if (fp == NULL) { len = snprintf(dest, size, "%s", source);
return 0; return len < 0 || (size_t)len >= size ? -1 : 0;
}
size = fwrite(buf ,len ,1 ,fp);
fclose(fp);
return size;
} }
/** static bool is_auto(const char *value)
* 读取温度 {
*/ return !value[0] || strcmp(value, AUTO_VALUE) == 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 int parse_int(const char *text, int min, int max, const char *name)
* 读取风扇速度 {
*/ char *end = NULL;
int get_fanspeed(char* fan_file) { long value;
char buf[8] = { 0 };
if (read_file(fan_file ,buf ,0) == 0) { errno = 0;
return atoi(buf); value = strtol(text, &end, 10);
} if (errno || !end || *end || value < min || value > max) {
return -1; fprintf(stderr, "Invalid %s: %s\n", name, text);
exit(EXIT_FAILURE);
}
return (int)value;
} }
/** static int read_text_file(const char *path, char *buffer, size_t size)
* 设置风扇转速 {
*/ ssize_t length;
int set_fanspeed(int fan_speed ,char* fan_file) { int fd;
char buf[8] = { 0 };
sprintf(buf ,"%d\n" ,fan_speed); if (!size)
return write_file(fan_file ,buf ,strlen(buf)); 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;
} }
/** static int read_long_file(const char *path, long *value)
* 计算风扇转速 {
*/ char buffer[64];
int calculate_speed(int current_temp ,int max_temp ,int min_temp ,int max_speed ,int min_speed) { char *end = NULL;
if (current_temp < min_temp) { long parsed;
return 0;
} if (read_text_file(path, buffer, sizeof(buffer)))
int fan_speed = ( current_temp - min_temp ) * ( max_speed - min_speed ) / ( max_temp - min_temp ) + min_speed; return -1;
if (fan_speed > max_speed) {
fan_speed = max_speed; errno = 0;
} parsed = strtol(buffer, &end, 10);
return fan_speed; if (errno || end == buffer)
return -1;
while (*end == ' ' || *end == '\t')
end++;
if (*end)
return -1;
*value = parsed;
return 0;
} }
/** static int write_long_file(const char *path, int value)
* 判断文件是否存在方法 {
*/ char buffer[32];
static int file_exist(const char* name) { ssize_t written;
struct stat buffer; int length;
return stat(name ,&buffer); 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;
} }
/** static void sleep_ms(unsigned int milliseconds)
* 信号处理函数 {
*/ struct timespec remaining = {
void handle_termination(int signum) { .tv_sec = milliseconds / 1000,
// 设置风扇转速为 0 .tv_nsec = (long)(milliseconds % 1000) * 1000000L,
set_fanspeed(0 ,fan_file); };
exit(EXIT_SUCCESS); // 优雅地退出程序
while (!stop_requested && nanosleep(&remaining, &remaining) && errno == EINTR)
;
} }
/** static int preferred_thermal_score(const char *type)
* 注册信号处理函数 {
*/ static const char * const preferred[] = {
void register_signal_handlers( ) { "cpu-top-thermal", "cpu_top_thermal", "cpu-thermal",
struct sigaction sa; "cpu_thermal", "soc-thermal", "soc_thermal",
memset(&sa ,0 ,sizeof(sa)); };
sa.sa_handler = handle_termination; size_t i;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT ,&sa ,NULL); for (i = 0; i < sizeof(preferred) / sizeof(preferred[0]); i++)
sigaction(SIGTERM ,&sa ,NULL); if (strcmp(type, preferred[i]) == 0)
return (int)i;
return 100;
} }
/** static int preferred_hwmon_temp_score(const char *name)
* 主函数 {
*/ if (strstr(name, "cpu") || strstr(name, "CPU"))
int main(int argc ,char* argv[ ]) { return 0;
// 解析命令行选项 if (strstr(name, "soc") || strstr(name, "SoC"))
int opt; return 1;
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);
}
// 注册退出信号 return 100;
register_signal_handlers( ); }
// 监控风扇 static int discover_hwmon_temperature(char *result, size_t result_size)
while (1) { {
int temperature = get_temperature(thermal_file ,temp_div); char pattern[PATH_MAX];
// 有效温度时设置风扇速度 char name_file[PATH_MAX];
if (temperature > 0) { char hwmon_name[128];
int fan_speed = calculate_speed(temperature ,MAX_TEMP ,start_temp ,max_speed ,start_speed); char candidate[PATH_MAX];
set_fanspeed(fan_speed ,fan_file); char best[PATH_MAX] = "";
} glob_t paths = { 0 };
if (debug_mode) { int best_score = INT_MAX;
fprintf(stdout ,"Temperature: %d°C, Fan Speed: %d\n" ,get_temperature(thermal_file ,temp_div) ,get_fanspeed(fan_file)); size_t i;
}
sleep(5); if (snprintf(pattern, sizeof(pattern), "%s/class/hwmon/hwmon*", sysfs_root) >=
} (int)sizeof(pattern))
return 0; 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;
} }
+7 -4
View File
@@ -6,13 +6,16 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for Fan Controller LUCI_TITLE:=LuCI support for the generic PWM fan controller
LUCI_DEPENDS:=+luci +fancontrol LUCI_DEPENDS:=+luci-base +fancontrol
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=JiaY-shi <shi05275@163.com>
PKG_NAME:=luci-app-fancontrol PKG_NAME:=luci-app-fancontrol
PKG_VERSION:=1.0 PKG_VERSION:=2.0.0
PKG_RELEASE:=1 PKG_RELEASE:=2
include $(TOPDIR)/feeds/luci/luci.mk 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,49 +1,269 @@
'use strict'; 'use strict';
'require view'; 'require dom';
'require fs';
'require form'; 'require form';
'require fs';
'require poll';
'require rpc';
'require uci'; '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({ return view.extend({
load: function () { load() {
return Promise.all([ return uci.load('fancontrol');
uci.load('fancontrol')
]);
}, },
render: async function (data) {
var m, s, o;
m = new form.Map('fancontrol', _('Fan General Control')); render() {
s = m.section(form.TypedSection, 'fancontrol', _('Settings')); 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.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.rmempty = false;
o.description = _('Run the controller and apply this temperature curve.');
o = s.option(form.Value, 'thermal_file', _('Thermal File'), _('Thermal File')); startTemp = s.taboption('curve', form.Value, 'start_temp', _('Start temperature'));
o.placeholder = '/sys/devices/virtual/thermal/thermal_zone0/temp'; startTemp.default = '45';
var temp_div = uci.get('fancontrol', 'settings', 'temp_div'); startTemp.datatype = 'range(-40,200)';
var temp = parseInt(await fs.read_direct(uci.get('fancontrol', 'settings', 'thermal_file'))); startTemp.rmempty = false;
if (temp_div > 0 && temp > 0) { startTemp.description = _('The fan remains off below this temperature and starts at the minimum running PWM when it is reached. Unit: °C.');
o.description = _('Current temperature:') + ' <b>' + (temp / temp_div) + '°C</b>';
} else {
o.description = _('Thermal File');
}
o = s.option(form.Value, 'fan_file', _('Fan File'), _('Fan Speed File')); fullTemp = s.taboption('curve', form.Value, 'full_speed_temp', _('Full-speed temperature'));
o.placeholder = '/sys/devices/virtual/thermal/cooling_device0/cur_state'; fullTemp.default = '85';
var speed = parseInt(await fs.read_direct(uci.get('fancontrol', 'settings', 'fan_file'))); fullTemp.datatype = 'range(-39,250)';
o.description = _('Current speed:') + ' <b>' + (speed) + '</b>'; 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 = s.taboption('curve', form.Value, 'hysteresis', _('Stop hysteresis'));
o.placeholder = '35'; 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.')); startSpeed = s.taboption('curve', form.Value, 'start_speed', _('Minimum running PWM'));
o.placeholder = '255'; 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.')); maxSpeed = s.taboption('curve', form.Value, 'max_speed', _('Maximum PWM'));
o.placeholder = '45'; 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(); return m.render();
} }
+275 -26
View File
@@ -1,38 +1,287 @@
msgid "Fan General Control" msgid ""
msgstr "风扇通用控制小程序" 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" #: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:173
msgstr "是否开启" 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" msgid "Settings"
msgstr "设置" msgstr "设置"
msgid "Thermal File" #: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:234
msgstr "温度虚拟文件" msgid "Start kick PWM"
msgstr "启动助推 PWM"
msgid "Fan Speed File" #: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:247
msgstr "风扇速度虚拟文件" msgid "Start kick PWM must be non-zero when a kick duration is configured."
msgstr "配置启动助推时长时,启动助推 PWM 不能为零。"
msgid "Initial Speed" #: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:240
msgstr "启动初始速度" msgid "Start kick duration"
msgstr "启动助推时长"
msgid "Please enter the initial speed for fan startup." #: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:152
msgstr "请输入风扇启动的初始速度。" msgid "Start temperature"
msgid "Max Speed"
msgstr "最大速度"
msgid "Please enter maximum fan speed."
msgstr "请输入风扇最大速度"
msgid "Start Temperature"
msgstr "启动温度" msgstr "启动温度"
msgid "Please enter the fan start temperature." #: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:169
msgstr "请输入风扇启动温度。" msgid "Stop hysteresis"
msgstr "停转回差"
msgid "Current speed:" #: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:77
msgstr "当前速度:" msgid "Stopped"
msgstr "已停止"
msgid "Current temperature:" #: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:80
msgstr "当前温度:" 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": { "admin/services/fancontrol": {
"title": "Fan General Control", "title": "Fan Control",
"order": 90, "order": 90,
"action": { "action": {
"type": "view", "type": "view",
@@ -1,20 +1,17 @@
{ {
"luci-app-fancontrol": { "luci-app-fancontrol": {
"description": "Fan General Control", "description": "Grant access to Fan Control",
"read": { "read": {
"uci": [ "uci": [ "fancontrol" ],
"fancontrol" "ubus": {
], "service": [ "list" ]
},
"file": { "file": {
"/sys/devices/virtual/thermal/*/*": [ "/var/run/fancontrol.status": [ "read" ]
"read"
]
} }
}, },
"write": { "write": {
"uci": [ "uci": [ "fancontrol" ]
"fancontrol"
]
} }
} }
} }
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.7.24 PKG_VERSION:=26.7.24
PKG_RELEASE:=196 PKG_RELEASE:=197
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
+13 -8
View File
@@ -1745,16 +1745,21 @@ function fetch_cert_sha256(host, port, sni, timeout, http3)
end end
function vps_domain_exclude(domain) function vps_domain_exclude(domain)
if trim(domain) == "" then return true end domain = trim(domain)
if domain == "" then return true end
local map = { local map = {
["engage.cloudflareclient.com"] = 1, ["engage.cloudflareclient.com"] = 1, ["google.com"] = 1, ["youtube.com"] = 1,
["google.com"] = 1, ["www.google.com"] = 1, ["github.com"] = 1, ["telegram.org"] = 1, ["cloudflare.com"] = 1,
["youtube.com"] = 1, ["www.youtube.com"] = 1, ["bing.com"] = 1, ["x.com"] = 1, ["dns.google"] = 1, ["opendns.com"] = 1,
["github.com"] = 1, ["telegram.org"] = 1, ["cloudflare-dns.com"] = 1, ["one.one.one.one"] = 1, ["quad9.net"] = 1,
["cloudflare.com"] = 1, ["www.cloudflare.com"] = 1, ["adguard-dns.com"] = 1, ["nextdns.io"] = 1, ["libredns.gr"] = 1
["bing.com"] = 1, ["www.bing.com"] = 1, ["x.com"] = 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 return false
end end
@@ -178,6 +178,10 @@ if not is_file_nonzero(file_vpslist) then
uci:foreach(appname, "nodes", function(t) uci:foreach(appname, "nodes", function(t)
process_address(t.address) process_address(t.address)
process_address(t.download_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) end)
uci:foreach(appname, "subscribe_list", function(t) --订阅链接 uci:foreach(appname, "subscribe_list", function(t) --订阅链接
local url, _ = api.get_domain_port_from_url(t.url or "") 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) uci:foreach(appname, "nodes", function(t)
process_address(t.address) process_address(t.address)
process_address(t.download_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) end)
uci:foreach(appname, "subscribe_list", function(t) --订阅链接 uci:foreach(appname, "subscribe_list", function(t) --订阅链接
local url, _ = api.get_domain_port_from_url(t.url or "") 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) uci:foreach(appname, "nodes", function(t)
process_address(t.address) process_address(t.address)
process_address(t.download_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) end)
uci:foreach(appname, "subscribe_list", function(t) --订阅链接 uci:foreach(appname, "subscribe_list", function(t) --订阅链接
local url, _ = api.get_domain_port_from_url(t.url or "") local url, _ = api.get_domain_port_from_url(t.url or "")
@@ -747,13 +747,13 @@ filter_haproxy() {
filter_vpsip() { 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)$" 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]直连完成" 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]直连完成" 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 -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 -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 "\[?[A-Fa-f0-9:]*:[A-Fa-f0-9:]+\]?" | sed "s/^/add $IPSET_VPS6 /" | awk '{print $0} END{print "COMMIT"}' | ipset -! -R
} }
filter_server_port() { filter_server_port() {
@@ -817,13 +817,13 @@ filter_vps_addr() {
filter_vpsip() { 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)$" 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]直连完成" 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]直连完成" 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 -Eo "([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 "\[?[A-Fa-f0-9:]*:[A-Fa-f0-9:]+\]?" | insert_nftset $NFTSET_VPS6
} }
filter_server_port() { filter_server_port() {
+1 -1
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=Web UI for QEMU Virtual Machine Simple (VMS)
LUCI_DEPENDS:=@TARGET_x86_64 +qemu-vms +ttyd +usbutils +pciutils LUCI_DEPENDS:=@TARGET_x86_64 +qemu-vms +ttyd +usbutils +pciutils
PKG_LICENSE:=GPLv3 PKG_LICENSE:=GPLv3
PKG_VERSION:=0.0.1 PKG_VERSION:=0.0.1
PKG_RELEASE:=5 PKG_RELEASE:=6
include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk
@@ -20,7 +20,8 @@ return view.extend({
load: function() { load: function() {
return Promise.all([ return Promise.all([
callListNetDrivers(), callListNetDrivers(),
uci.load('qemu-vms') uci.load('qemu-vms'),
uci.load('network')
]); ]);
}, },
@@ -63,8 +64,21 @@ return view.extend({
return true; 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)')); 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')); var driver = s.option(form.ListValue, 'driver', _('Network driver'));
@@ -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.') + _('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 />' + _('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 />' + _('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 || []) this.renderPciTable(hw.pci || [])
]); ]);
@@ -4,6 +4,7 @@
. /lib/functions.sh . /lib/functions.sh
STATUS_DIR="/var/run/qemu-vms" STATUS_DIR="/var/run/qemu-vms"
LOG_DIR="/var/log/qemu-vms"
is_anonymous_section() { is_anonymous_section() {
# UCI auto-generated names look like: cfgXXXXXX (cfg + hex) # UCI auto-generated names look like: cfgXXXXXX (cfg + hex)
@@ -85,7 +86,7 @@ find_free_port() {
console_open() { console_open() {
local vm="$1" 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 pidfile="$STATUS_DIR/$vm.ttyd.pid"
local portfile="$STATUS_DIR/$vm.ttyd.port" local portfile="$STATUS_DIR/$vm.ttyd.port"
@@ -145,7 +146,7 @@ console_close() {
readlog() { readlog() {
local vm="$1" local vm="$1"
local lines="${2:-200}" local lines="${2:-200}"
local log="/var/log/qemu-vms/$vm.log" local log="$LOG_DIR/$vm.log"
json_init json_init
if [ -f "$log" ]; then if [ -f "$log" ]; then
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=qemu-vms PKG_NAME:=qemu-vms
PKG_VERSION:=0.0.1 PKG_VERSION:=0.0.1
PKG_RELEASE:=5 PKG_RELEASE:=6
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru> PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
+2
View File
@@ -15,6 +15,7 @@ config network 'tap_example_bridge'
option mac '52:54:00:31:6c:4e' option mac '52:54:00:31:6c:4e'
option ifname 'tap1' option ifname 'tap1'
option bridge 'br-tap' option bridge 'br-tap'
option driver 'pcnet'
config vm 'vm_example' config vm 'vm_example'
option enabled '1' option enabled '1'
@@ -34,6 +35,7 @@ config vm 'winxp_example'
option display_type 'vnc' option display_type 'vnc'
option vnc_port '5902' option vnc_port '5902'
list network 'tap_example_bridge' list network 'tap_example_bridge'
list usb_passthrough 'example_usb'
option cdrom '/mnt/disk/vm/wxp.iso' option cdrom '/mnt/disk/vm/wxp.iso'
option image '/mnt/disk/vm/xp.img' option image '/mnt/disk/vm/xp.img'
option disk_bus 'ide' option disk_bus 'ide'
+3 -3
View File
@@ -244,9 +244,9 @@ start_vm() {
config_get vnc_port "$cfg" vnc_port config_get vnc_port "$cfg" vnc_port
config_get machine "$cfg" machine "q35" config_get machine "$cfg" machine "q35"
local console_sock="/var/run/qemu-$cfg.sock" local console_sock="$STAUS_DIR/qemu-$cfg.sock"
local console_log="/var/log/qemu-$cfg-console.log" local console_log="$LOG_DIR/qemu-$cfg-console.log"
local qmp_sock="/var/run/qemu-$cfg.qmp" local qmp_sock="$STATUS_DIR/qemu-$cfg.qmp"
build_netargs "$cfg" build_netargs "$cfg"
build_usbargs "$cfg" build_usbargs "$cfg"
+58 -17
View File
@@ -1,22 +1,63 @@
#!/bin/sh #!/bin/sh
# /etc/qemu-ifup # /etc/qemu-ifup
# # $1 - tap iface name
# Invoked by QEMU itself (via -netdev tap,...,script=/etc/qemu-ifup) right # $2 - bridge name (optional)
# 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).
ifname="$1" . /lib/functions.sh
bridge="$2"
ip link set "$ifname" up if [ -n "$2" ]; then
# if custom user bridge
if [ -n "$bridge" ]; then if ! ip link show "$2" >/dev/null 2>&1; then
ip link set "$ifname" master "$bridge" 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 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
+6 -3
View File
@@ -10,6 +10,9 @@
# log: just tails the always-on boot/console logfile (safe to run at # log: just tails the always-on boot/console logfile (safe to run at
# any time, does not touch the live console socket at all). # 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" VM="$1"
MODE="${2:-attach}" MODE="${2:-attach}"
@@ -18,9 +21,9 @@ if [ -z "$VM" ]; then
exit 1 exit 1
fi fi
CONSOLE_SOCK="/var/run/qemu-$VM.sock" CONSOLE_SOCK="$STATUS_DIR/qemu-$VM.sock"
CONSOLE_LOG="/var/log/qemu-$VM-console.log" CONSOLE_LOG="$LOG_DIR/qemu-$VM-console.log"
TMP_PTY="/var/run/qemu-$VM.console-pty.$$" TMP_PTY="$STATUS_DIR/qemu-$VM.console-pty.$$"
case "$MODE" in case "$MODE" in
log) log)