Add fancontrol-ng

This commit is contained in:
Přemysl Eric Janouch 2016-01-22 08:56:34 +01:00
parent 80e14db520
commit 8146e336d7
6 changed files with 722 additions and 4 deletions

View File

@ -40,11 +40,22 @@ add_threads (dwmstatus)
add_executable (brightness brightness.c) add_executable (brightness brightness.c)
target_link_libraries (brightness ${project_libraries}) target_link_libraries (brightness ${project_libraries})
add_threads (brightness)
add_executable (fancontrol-ng fancontrol-ng.c)
target_link_libraries (fancontrol-ng ${project_libraries})
# The files to be installed # The files to be installed
include (GNUInstallDirs) include (GNUInstallDirs)
install (TARGETS dwmstatus brightness DESTINATION ${CMAKE_INSTALL_BINDIR})
configure_file (${PROJECT_SOURCE_DIR}/fancontrol-ng.service.in
${PROJECT_BINARY_DIR}/fancontrol-ng.service @ONLY)
install (FILES ${PROJECT_BINARY_DIR}/fancontrol-ng.service
DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/systemd/system)
install (FILES fancontrol-ng.conf.example
DESTINATION ${CMAKE_INSTALL_DATADIR}/fancontrol-ng)
install (TARGETS dwmstatus brightness fancontrol-ng
DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
# CPack # CPack

View File

@ -7,8 +7,11 @@ to other people as well.
- 'dwmstatus' does literally everything my dwm doesn't but I'd like it to. It - 'dwmstatus' does literally everything my dwm doesn't but I'd like it to. It
includes PulseAudio volume management and hand-written NUT and MPD clients, includes PulseAudio volume management and hand-written NUT and MPD clients,
all in the name of liberation from GPL-licensed software of course all in the name of liberation from GPL-licensed software of course.
- 'brightness' allows me to change the brightness of w/e display device I have. - 'brightness' allows me to change the brightness of w/e display device I have.
- 'fancontrol-ng' is a clone of fancontrol that can handle errors on resume
from suspend instead of setting fans to maximum speed and quitting;
in general it doesn't handle everything the original does
Don't expect them to work under any OS that isn't Linux. Don't expect them to work under any OS that isn't Linux.

680
fancontrol-ng.c Normal file
View File

@ -0,0 +1,680 @@
/*
* fancontrol-ng.c: clone of fancontrol from lm_sensors
*
* Copyright (c) 2016, Přemysl Janouch <p.janouch@gmail.com>
* All rights reserved.
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#define LIBERTY_WANT_POLLER
#include "config.h"
#undef PROGRAM_NAME
#define PROGRAM_NAME "fancontrol-ng"
#include "liberty/liberty.c"
/// Shorthand to set an error and return failure from the function
#define FAIL(...) \
BLOCK_START \
error_set (e, __VA_ARGS__); \
return false; \
BLOCK_END
// --- Main program ------------------------------------------------------------
struct device
{
LIST_HEADER (struct device)
struct app_context *ctx; ///< Application context
struct config_item *config; ///< Configuration root for the device
char *path; ///< Base path
struct poller_timer timer; ///< Refresh timer
};
struct app_context
{
struct poller poller; ///< Poller
bool polling; ///< The event loop is running
struct config_item *config; ///< Program configuration
struct device *devices; ///< All devices
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
log_message_custom (void *user_data, const char *quote, const char *fmt,
va_list ap)
{
(void) user_data;
FILE *stream = stdout;
// TODO: sd-daemon.h log level prefixes?
fputs (quote, stream);
vfprintf (stream, fmt, ap);
fputs ("\n", stream);
}
static char *
read_file_cstr (const char *path, struct error **e)
{
struct str s;
str_init (&s);
if (read_file (path, &s, e))
return str_steal (&s);
str_free (&s);
return NULL;
}
static int64_t
read_file_unsigned (const char *path, struct error **e)
{
char *s, *end;
if (!(s = read_file_cstr (path, e)))
return -1;
if ((end = strpbrk (s, "\r\n")))
*end = 0;
unsigned long num;
bool ok = xstrtoul (&num, s, 10);
free (s);
if (!ok || num > INT64_MAX)
{
error_set (e, "error reading `%s': %s", path, "invalid integer value");
return -1;
}
return num;
}
static bool
write_file_printf (const char *path, struct error **e, const char *format, ...)
ATTRIBUTE_PRINTF (3, 4);
static bool
write_file_printf (const char *path, struct error **e, const char *format, ...)
{
struct str s;
str_init (&s);
va_list ap;
va_start (ap, format);
str_append_vprintf (&s, format, ap);
va_end (ap);
bool success = write_file (path, s.str, s.len, e);
str_free (&s);
return success;
}
// --- Configuration -----------------------------------------------------------
static bool
config_validate_nonnegative (const struct config_item *item, struct error **e)
{
if (item->type == CONFIG_ITEM_NULL)
return true;
hard_assert (item->type == CONFIG_ITEM_INTEGER);
if (item->value.integer >= 0)
return true;
error_set (e, "must be non-negative");
return false;
}
static struct config_schema g_config_device[] =
{
{ .name = "name",
.comment = "Device identifier",
.type = CONFIG_ITEM_STRING },
{ .name = "interval",
.comment = "Temperature checking interval",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative,
.default_ = "5" },
{}
};
static struct config_schema g_config_pwm[] =
{
{ .name = "temp",
.comment = "Path to temperature sensor output",
.type = CONFIG_ITEM_STRING },
{ .name = "min_temp",
.comment = "Temperature for no fan operation",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative,
.default_ = "40" },
{ .name = "max_temp",
.comment = "Temperature for maximum fan operation",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative,
.default_ = "80" },
{ .name = "min_start",
.comment = "Minimum value for the fan to start spinning",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative,
.default_ = "0" },
{ .name = "min_stop",
.comment = "Mimimum value for the fan to stop spinning",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative,
.default_ = "0" },
{ .name = "pwm_min",
.comment = "Minimum PWM value to use",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative },
{ .name = "pwm_max",
.comment = "Maximum PWM value to use",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative },
{}
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int64_t
get_config_integer (struct config_item *root, const char *key)
{
struct config_item *item = config_item_get (root, key, NULL);
hard_assert (item && item->type == CONFIG_ITEM_INTEGER);
return item->value.integer;
}
static const char *
get_config_string (struct config_item *root, const char *key)
{
struct config_item *item = config_item_get (root, key, NULL);
hard_assert (item);
if (item->type == CONFIG_ITEM_NULL)
return NULL;
hard_assert (config_item_type_is_string (item->type));
return item->value.string.str;
}
// --- Fan control -------------------------------------------------------------
// Consider this a failed attempt to avoid creating special PWM objects
// based on the configuration. The complexity just moved somewhere else.
struct paths
{
char *temp; ///< Current temperature
char *pwm; ///< Current PWM value
char *pwm_enable; ///< PWM control state
char *pwm_min; ///< Minimum PWM value
char *pwm_max; ///< Maximum PWM value
};
static struct paths *
paths_new (const char *device_path, const char *path, struct config_item *pwm)
{
struct paths *self = xcalloc (1, sizeof *self);
self->temp = xstrdup_printf
("%s/%s", device_path, get_config_string (pwm, "temp"));
self->pwm = xstrdup_printf ("%s/%s", device_path, path);
self->pwm_enable = xstrdup_printf ("%s/%s_enable", device_path, path);
self->pwm_min = xstrdup_printf ("%s/%s_min", device_path, path);
self->pwm_max = xstrdup_printf ("%s/%s_max", device_path, path);
return self;
}
static void
paths_destroy (struct paths *self)
{
free (self->temp);
free (self->pwm);
free (self->pwm_enable);
free (self->pwm_min);
free (self->pwm_max);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
pwm_update (struct paths *paths, struct config_item *pwm, struct error **e)
{
int64_t cur_enable, cur_temp, cur_pwm, pwm_min, pwm_max;
if ((cur_enable = read_file_unsigned (paths->pwm_enable, e)) < 0
|| (cur_temp = read_file_unsigned (paths->temp, e)) < 0
|| (cur_pwm = read_file_unsigned (paths->pwm, e)) < 0)
return false;
struct config_item *pwm_min_item = config_item_get (pwm, "pwm_min", NULL);
if (pwm_min_item->type == CONFIG_ITEM_INTEGER)
pwm_min = pwm_min_item->value.integer;
else if ((pwm_min = read_file_unsigned (paths->pwm_min, NULL)) < 0)
pwm_min = 0;
struct config_item *pwm_max_item = config_item_get (pwm, "pwm_max", NULL);
if (pwm_max_item->type == CONFIG_ITEM_INTEGER)
pwm_max = pwm_max_item->value.integer;
else if ((pwm_max = read_file_unsigned (paths->pwm_max, NULL)) < 0)
pwm_max = 255;
int64_t min_temp = get_config_integer (pwm, "min_temp");
int64_t max_temp = get_config_integer (pwm, "max_temp");
int64_t min_start = get_config_integer (pwm, "min_start");
int64_t min_stop = get_config_integer (pwm, "min_stop");
if (min_temp >= max_temp) FAIL ("min_temp must be less than max_temp");
if (pwm_max > 255) FAIL ("pwm_max must be at most 255");
if (min_stop >= pwm_max) FAIL ("min_stop must be less than pwm_max");
if (min_stop < pwm_min) FAIL ("min_stop must be at least pwm_min");
// I'm not sure if this strangely complicated computation is justifiable
double where
= ((double) cur_temp / 1000 - min_temp)
/ ((double) max_temp - min_temp);
int64_t new_pwm;
if (where <= 0) new_pwm = pwm_min;
else if (where >= 1) new_pwm = pwm_max;
else
{
new_pwm = min_stop + where * (pwm_max - min_stop);
// If needed, we start the fan until next iteration
if (cur_pwm <= min_stop)
new_pwm = MAX (new_pwm, min_start);
}
new_pwm = MAX (new_pwm, pwm_min);
new_pwm = MIN (new_pwm, pwm_max);
if (cur_enable != 1 && !write_file_printf (paths->pwm_enable, e, "1"))
return false;
if (!write_file_printf (paths->pwm, e, "%" PRId64, new_pwm))
return false;
return true;
}
static bool
pwm_set_enable (struct paths *paths, char value)
{
struct error *e = NULL;
if (write_file (paths->pwm_enable, &value, 1, &e))
return true;
print_error ("failed to change PWM mode to %c: %s",
value, e->message);
error_free (e);
return false;
}
static bool
pwm_give_up (struct paths *paths)
{
// Try automatic control, and if that fails, go full speed
return pwm_set_enable (paths, '2') || pwm_set_enable (paths, '0');
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct pwm_iter
{
struct str_map_iter object_iter; ///< Configuration iterator
struct device *device; ///< Device
struct config_item *pwm; ///< PWM
const char *pwm_path; ///< PWM path
struct paths *paths; ///< Paths
};
static void
pwm_iter_init (struct pwm_iter *self, struct device *device)
{
str_map_iter_init (&self->object_iter,
&config_item_get (device->config, "pwms", NULL)->value.object);
self->device = device;
self->paths = NULL;
}
static void
pwm_iter_free (struct pwm_iter *self)
{
if (self->paths)
{
paths_destroy (self->paths);
self->paths = NULL;
}
}
static bool
pwm_iter_next (struct pwm_iter *self)
{
pwm_iter_free (self);
if (!(self->pwm = str_map_iter_next (&self->object_iter)))
return false;
self->pwm_path = self->object_iter.link->key;
self->paths = paths_new (self->device->path, self->pwm_path, self->pwm);
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
device_run (struct device *self)
{
struct pwm_iter iter;
pwm_iter_init (&iter, self);
while (pwm_iter_next (&iter))
{
struct error *e = NULL;
if (pwm_update (iter.paths, iter.pwm, &e))
continue;
print_error ("pwm `%s': %s", iter.pwm_path, e->message);
error_free (e);
pwm_give_up (iter.paths);
}
pwm_iter_free (&iter);
poller_timer_set (&self->timer,
1000 * get_config_integer (self->config, "interval"));
}
static void
device_stop (struct device *self)
{
struct pwm_iter iter;
pwm_iter_init (&iter, self);
while (pwm_iter_next (&iter))
pwm_give_up (iter.paths);
pwm_iter_free (&iter);
}
static void
device_create (struct app_context *ctx, const char *path,
struct config_item *root)
{
struct device *self = xcalloc (1, sizeof *self);
self->config = root;
self->path = xstrdup (path);
poller_timer_init (&self->timer, &ctx->poller);
self->timer.dispatcher = (poller_timer_fn) device_run;
self->timer.user_data = self;
LIST_PREPEND (ctx->devices, self);
}
// --- Configuration -----------------------------------------------------------
// TODO: consider moving to liberty,
// degesch and json-rpc-shell have exactly the same function
static struct config_item *
load_configuration_file (const char *filename, struct error **e)
{
struct config_item *root = NULL;
struct str data;
str_init (&data);
if (!read_file (filename, &data, e))
goto end;
struct error *error = NULL;
if (!(root = config_item_parse (data.str, data.len, false, &error)))
{
error_set (e, "parse error: %s", error->message);
error_free (error);
}
end:
str_free (&data);
return root;
}
// There is no room for errors in the configuration, everything must be valid.
// Thus the reset to defaults on invalid values is effectively disabled here.
static bool
apply_schema (struct config_schema *schema, struct config_item *object,
struct error **e)
{
struct error *warning = NULL, *error = NULL;
config_schema_initialize_item (schema, object, NULL, &warning, &error);
if (error && warning)
{
error_free (warning);
error_propagate (e, error);
return false;
}
if (error)
{
error_propagate (e, error);
return false;
}
if (warning)
{
// The standard warning is inappropriate here
error_free (warning);
FAIL ("invalid item `%s'", schema->name);
}
return true;
}
static bool
check_device_configuration (struct config_item *subtree, struct error **e)
{
// Check regular fields in the device object
for (struct config_schema *s = g_config_device; s->name; s++)
if (!apply_schema (s, subtree, e))
return false;
// Check for a subobject with PWMs to control
struct config_item *pwms;
if (!(pwms = config_item_get (subtree, "pwms", e)))
return false;
if (pwms->type != CONFIG_ITEM_OBJECT)
FAIL ("`%s' is not an object", "pwms");
if (!pwms->value.object.len)
FAIL ("no PWMs defined");
// Check regular fields in all PWM subobjects
struct str_map_iter iter;
str_map_iter_init (&iter, &pwms->value.object);
struct config_item *pwm;
struct error *error = NULL;
while ((pwm = str_map_iter_next (&iter)))
{
const char *subpath = iter.link->key;
for (struct config_schema *s = g_config_pwm; s->name; s++)
if (!apply_schema (s, pwm, &error))
{
error_set (e, "PWM `%s': %s", subpath, error->message);
error_free (error);
return false;
}
if (!get_config_string (pwm, "temp"))
FAIL ("PWM `%s': %s", subpath, "`temp' cannot be null");
}
return true;
}
static void
load_configuration (struct app_context *ctx, const char *config_path)
{
struct error *e = NULL;
struct config_item *root = load_configuration_file (config_path, &e);
if (e)
{
print_error ("error loading configuration: %s", e->message);
error_free (e);
exit (EXIT_FAILURE);
}
struct str_map_iter iter;
str_map_iter_init (&iter, &(ctx->config = root)->value.object);
struct config_item *subtree;
while ((subtree = str_map_iter_next (&iter)))
{
const char *path = iter.link->key;
if (subtree->type != CONFIG_ITEM_OBJECT)
print_fatal ("device `%s' in configuration is not an object", path);
else if (!check_device_configuration (subtree, &e))
print_fatal ("device `%s': %s", path, e->message);
else
device_create (ctx, path, subtree);
}
}
// --- Signals -----------------------------------------------------------------
static int g_signal_pipe[2]; ///< A pipe used to signal... signals
static void
sigterm_handler (int signum)
{
(void) signum;
int original_errno = errno;
if (write (g_signal_pipe[1], "", 1) == -1)
soft_assert (errno == EAGAIN);
errno = original_errno;
}
static void
setup_signal_handlers (void)
{
if (pipe (g_signal_pipe) == -1)
exit_fatal ("%s: %s", "pipe", strerror (errno));
set_cloexec (g_signal_pipe[0]);
set_cloexec (g_signal_pipe[1]);
// So that the pipe cannot overflow; it would make write() block within
// the signal handler, which is something we really don't want to happen.
// The same holds true for read().
set_blocking (g_signal_pipe[0], false);
set_blocking (g_signal_pipe[1], false);
(void) signal (SIGPIPE, SIG_IGN);
struct sigaction sa;
sa.sa_flags = SA_RESTART;
sa.sa_handler = sigterm_handler;
sigemptyset (&sa.sa_mask);
if (sigaction (SIGINT, &sa, NULL) == -1
|| sigaction (SIGTERM, &sa, NULL) == -1)
exit_fatal ("sigaction: %s", strerror (errno));
}
// --- Main program ------------------------------------------------------------
static void
on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
{
char id = 0;
(void) read (fd->fd, &id, 1);
ctx->polling = false;
}
static const char *
parse_program_arguments (int argc, char **argv)
{
static const struct opt opts[] =
{
{ 'd', "debug", NULL, 0, "run in debug mode" },
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 0, NULL, NULL, 0, NULL }
};
struct opt_handler oh;
opt_handler_init (&oh, argc, argv, opts, "CONFIG", "Fan controller.");
int c;
while ((c = opt_handler_get (&oh)) != -1)
switch (c)
{
case 'd':
g_debug_mode = true;
break;
case 'h':
opt_handler_usage (&oh, stdout);
exit (EXIT_SUCCESS);
case 'V':
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
exit (EXIT_SUCCESS);
default:
print_error ("wrong options");
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
argc -= optind;
argv += optind;
if (argc != 1)
{
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
opt_handler_free (&oh);
return argv[0];
}
int
main (int argc, char *argv[])
{
g_log_message_real = log_message_custom;
const char *config_path = parse_program_arguments (argc, argv);
struct app_context ctx;
memset (&ctx, 0, sizeof ctx);
poller_init (&ctx.poller);
setup_signal_handlers ();
struct poller_fd signal_event;
poller_fd_init (&signal_event, &ctx.poller, g_signal_pipe[0]);
signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
signal_event.user_data = &ctx;
poller_fd_set (&signal_event, POLLIN);
load_configuration (&ctx, config_path);
if (!ctx.devices)
exit_fatal ("no devices present in configuration");
LIST_FOR_EACH (struct device, iter, ctx.devices)
device_run (iter);
ctx.polling = true;
while (ctx.polling)
poller_run (&ctx.poller);
LIST_FOR_EACH (struct device, iter, ctx.devices)
device_stop (iter);
config_item_destroy (ctx.config);
poller_free (&ctx.poller);
return EXIT_SUCCESS;
}

View File

@ -0,0 +1,15 @@
"/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0" = {
name = "radeon"
interval = 10
pwms = {
"hwmon/hwmon2/pwm1" = {
temp = "hwmon/hwmon2/temp1_input"
min_temp = 40
max_temp = 80
min_start = 50
min_stop = 40
# min_pwm = 0
# max_pwm = 255
}
}
}

9
fancontrol-ng.service.in Normal file
View File

@ -0,0 +1,9 @@
[Unit]
Description=fancontrol-ng
[Service]
ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/fancontrol-ng @CMAKE_INSTALL_FULL_SYSCONFDIR@/fancontrol-ng.conf
Restart=on-abort
[Install]
WantedBy=default.target

@ -1 +1 @@
Subproject commit 9e3cb2b6aa2db3ca0ea6a854fb3f89a163c84235 Subproject commit 052d2ffc9a3141ef2bb771f70190ed7a0bb9da44