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

This commit is contained in:
github-actions[bot] 2026-07-30 20:36:50 +08:00
parent 68d255846f
commit 48cc1b888b
27 changed files with 1668 additions and 357 deletions

View File

@ -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

View File

@ -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'

View File

@ -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
}

View File

@ -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 *~

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 <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;
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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: 1255.');
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();
}
});
});

View File

@ -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 "温控曲线允许的最高 PWM255 表示 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: 1255."
msgstr "风扇运行时使用的最低 PWM。如果风扇停转或无法持续旋转请提高此值。范围1255。"
#: /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 数值写入系统日志。"

View File

@ -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" ]
}
}
}
}

View File

@ -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" ]
}
}
}
}

View File

@ -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:= \

View File

@ -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

View File

@ -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 "")

View File

@ -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 "")

View File

@ -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 "")

View File

@ -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() {

View File

@ -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() {

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
PKG_LICENSE:=GPLv3
PKG_VERSION:=0.0.1
PKG_RELEASE:=5
PKG_RELEASE:=6
include $(TOPDIR)/feeds/luci/luci.mk

View File

@ -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();
}
});
});

View File

@ -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 || [])
]);

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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"

View File

@ -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

View File

@ -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)