💋 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
+1668 -357

No files matched your search

+6 -7
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
+19 -8
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'
+41 -43
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
}
+5 -4
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 *~
+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 <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;
}