diff --git a/.gitignore b/.gitignore index 058ec61..b330eb6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ ambientd !sv/** .ccls-cache -.zed diff --git a/Makefile b/Makefile index 3251055..74fa8f7 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ +CC = gcc +CFLAGS = -Wall -Wextra -pedantic -std=c99 -O2 PREFIX = /usr/local -ambientd: ambientd.c3 - c3c compile -O1 -g0 --optsize=small --fp-math=fast --x86cpu=native ambientd.c3 +ambientd: ambientd.c install: cp ambientd ${PREFIX}/bin/ diff --git a/TODO b/TODO new file mode 100644 index 0000000..2c6d014 --- /dev/null +++ b/TODO @@ -0,0 +1,10 @@ +[ ] Add a config file +[ ] Use a better filter than a simple median, maybe a kalman filter +[ ] Add a way to customise the brightness curve + Percieved brightness is not linear, on low light you need a slightly higher + brightness and on high light you can get away with a lower brightness + than the max, the goal is to find the right curve for the screen +[ ] Automatically detect backlight device, or at least make it configurable +[ ] Add signal handlers + [ ] SIGUSR1 to reset the user offset + [ ] Handle kill by closing all file descriptors diff --git a/ambientd.c b/ambientd.c new file mode 100644 index 0000000..7bd413e --- /dev/null +++ b/ambientd.c @@ -0,0 +1,178 @@ +#include +#include +#include +#include +#include +#include + +#define MAX(x, y) ((x) > (y) ? (x) : (y)) +#define MIN(x, y) ((x) < (y) ? (x) : (y)) +#define ABS(x) ((x) < 0 ? -(x) : (x)) +#define MEDIAN_SAMPLES 4 + +const char *LUMINANCE_DEV = "/sys/bus/iio/devices/iio:device0/in_illuminance_raw"; +const char *MAX_BRIGHTNESS_DEV = "/sys/class/backlight/intel_backlight/max_brightness"; +const char *BRIGHTNESS_DEV = "/sys/class/backlight/intel_backlight/brightness"; +const char *ACTUAL_BRIGHTNESS_DEV = "/sys/class/backlight/intel_backlight/actual_brightness"; + +char STR_BUFFER[128] = {0}; + +// file pointer to the luminance device +FILE *luminance_fp = NULL; + +// max luminance that the display can output +int max_luminance = 550; + +// minimum display brightness in percent +float min_brightness_percent = 0.05; + +// returns the luminance value in percentage between 0 and 1 +int get_luminance_raw (void) +{ + freopen(NULL, "r", luminance_fp); + const char *x = fgets(STR_BUFFER, sizeof(STR_BUFFER), luminance_fp); + if (x == NULL) { + return -1; + } + int luminance = strtol(STR_BUFFER, NULL, 10); + + return luminance; +} + +// read a string from a file and convert it to int, should only be used for +// positive numbers since negative numbers mean an error +int read_int(FILE *fp) +{ + freopen(NULL, "r", fp); + const char *p = fgets(STR_BUFFER, sizeof(STR_BUFFER), fp); + if (p == NULL) { + return -1; + } + long r = strtol(STR_BUFFER, NULL, 10); + if (r == LONG_MIN || r == LONG_MAX) { + return -1; + } + return r; +} + +float bezier(float t) +{ + const float B = 0.66; + const float D = 0.28; + return 3*B*t+(3*D-6*B)*t*t+(1+3*B-3*D)*t*t*t; +} + +// TODO: add some command line options like: +// -v: verbose/logging +// -c file; specify a config file +int main (void) +{ + // open the luminance device + luminance_fp = fopen(LUMINANCE_DEV, "r"); + if (luminance_fp == NULL) { + fprintf(stderr, "Could not open luminance device '%s': %s\n", + LUMINANCE_DEV, strerror(errno)); + return 1; + } + + // get max and min brightness + FILE *fp = fopen(MAX_BRIGHTNESS_DEV, "r"); + if (fp == NULL) { + fprintf(stderr, "Could not open max_brightness file '%s': %s\n", + MAX_BRIGHTNESS_DEV, strerror(errno)); + return 1; + } + int max_brightness = read_int(fp); + if (max_brightness < 0) { + fprintf(stderr, "Could not read max brightness value: %s\n", strerror(errno)); + return 1; + } + int min_brightness = max_brightness * min_brightness_percent; + fclose(fp); + + + // open brightness device + FILE *brightness_fp = fopen(BRIGHTNESS_DEV, "w"); + if (brightness_fp == NULL) { + fprintf(stderr, "Could not open brightness device '%s': %s\n", + BRIGHTNESS_DEV, strerror(errno)); + return 1; + } + + // open actual brightness device + FILE *actual_brightness_fp = fopen(ACTUAL_BRIGHTNESS_DEV, "r"); + if (brightness_fp == NULL) { + fprintf(stderr, "Could not open actual brightness device '%s': %s\n", + ACTUAL_BRIGHTNESS_DEV, strerror(errno)); + return 1; + } + + int x = get_luminance_raw(); + if (x < 0) { + fprintf(stderr, "Could not read luminance value: %s\n", strerror(errno)); + } + int luminance_samples[MEDIAN_SAMPLES] = {x, x, x, x}; + int idx = 0; + int user_offset = 0; + int brightness_target = min_brightness; + int brightness_old = min_brightness; + int brightness_adjusted = min_brightness; + int brightness_delta = 0; + while (1) { + static float luminance; + static float luminance_percent; + + x = get_luminance_raw(); + if (x >= 0) luminance_samples[idx] = x; + idx = (idx + 1) % MEDIAN_SAMPLES; + + luminance = 0; + for (int i = 0; i < MEDIAN_SAMPLES; i++) { + luminance += luminance_samples[i] / MEDIAN_SAMPLES; + } + luminance_percent = bezier(MIN(luminance/max_luminance, 1.0)); + + brightness_old = brightness_target; + brightness_target = MAX((int)(luminance_percent * max_brightness), min_brightness); + brightness_delta = ABS((float)(brightness_target - brightness_old)/brightness_old); + + // if the brightness changes by more than 10% reduce the user offset, scaling + // with the delta, this accounts for large changes in brightness no longer + // corresponding to the adjustments that the user made to the brightness + if (brightness_delta > 0.1) { + user_offset = user_offset * MAX(MIN(1-(brightness_delta*1.2), 1), 0); + } + + brightness_adjusted = MIN(MAX(brightness_target + user_offset, min_brightness), max_brightness); + // TODO: put this in write_int() function + x = snprintf(STR_BUFFER, sizeof(STR_BUFFER), "%d", brightness_adjusted); + if (x <= 0) { + fprintf(stderr, "Error in snprintf(): %s\n", strerror(errno)); + } + if (fputs(STR_BUFFER, brightness_fp) == EOF) { + fprintf(stderr, "error writing brightness: %s\n", strerror(errno)); + } + rewind(brightness_fp); + + sleep(3); + + // read actual brightness to get the user offset + int actual_brightness = 0; + int off = 0; + actual_brightness = read_int(actual_brightness_fp); + if (actual_brightness < 0) { + fprintf(stderr, "error reading actual brightness: %s\n", strerror(errno)); + actual_brightness = brightness_adjusted; + } + + off = actual_brightness - brightness_target; + if (brightness_target != max_brightness || off < 0){ + user_offset = off; + } + + // TODO: option to turn off logging + printf("brightness target: %d; user offset: %d; brightness adjusted: %d\n", brightness_target, user_offset, brightness_adjusted); + } + + return 0; +} diff --git a/ambientd.c3 b/ambientd.c3 deleted file mode 100644 index 6cfd808..0000000 --- a/ambientd.c3 +++ /dev/null @@ -1,297 +0,0 @@ -module ambientd; - -import std::io; -import std::io::path; -import std::os::env; -import std::os::posix; -import std::collections::list; -import std::collections::object; -import std::encoding::ini; -import std::thread; -import std::time; -import std::math; -import libc; - - -alias StrList = List{String}; - - -const String DEF_LIGHT_SENSOR_PATH = "/sys/bus/iio/devices/iio:device0/in_illuminance_raw"; -const String DEF_BACKLIGHT_PATH = "/sys/class/backlight/intel_backlight"; -const float DEF_SCREEN_NIT = 400.0; -const float DEF_SENSOR_K = 0.98; -const int DEF_UPDATE_INTERVAL_MS = 200; - -const String[*] CONFIG_PATHS = { - "$XDG_CONFIG_HOME/ambientd/ambientd.ini", - "$HOME/.config/ambientd/ambientd.ini", - "/etc/ambientd/ambientd.ini", -}; - - -String light_sensor_path = DEF_LIGHT_SENSOR_PATH; -String backlight_path = DEF_BACKLIGHT_PATH; -float backlight_max_luminance = DEF_SCREEN_NIT; -float light_sensor_k = DEF_SENSOR_K; -int update_interval_ms = DEF_UPDATE_INTERVAL_MS; - - -struct LuminanceFilter { - float x; // estimate - float p; // error - float r; // mesurement noise - float q; // process noise -} - - -fn float LuminanceFilter.update(&self, int reading, float dt) -{ - float x_1 = self.x; - float p_1 = self.p + self.q * dt; - - float k = p_1 / (p_1 + self.r); - - self.x = x_1 + k * ((float)reading - x_1); - self.p = (1.0 - k) * p_1; - - return self.x; -} - - -fn void LuminanceFilter.init(&self, int reading) -{ - self.p = 0.0; - self.x = (float)reading; -} - - -// TODO: this should be configurable -LuminanceFilter filter = {.r = 5.0, .q = 0.1}; - - -fn String? String.to_expanded_path(&str, Allocator allocator, String[] extra_env = {}) -{ - @pool() { - StrList l; - l.tinit(); - Path str_path = str.to_tpath()!; - - l.push(str_path.basename()); - Path? p = str_path.parent(); - while (true) { - Path? x; - if (try p) { - String n = p.basename(); - l.push("/"); - - // env variable - if (n.starts_with("$")) { - String? e = env::tget_var(n[1..]); - if (catch e) { - // var is not in regular environment, check extra_env - bool in_extra = false; - foreach_r (j: extra_env) { - if (j.starts_with(n[1..])) { - l.push(j.strip_suffix(n[1..])[1..]); - in_extra = true; - break; - } - } - if (!in_extra) { - return NOT_FOUND~; - } - } else { - l.push(e); - } - } else { - l.push(n); - } - - x = p.parent(); - } else { - break; - } - p = x; - } - l.reverse(); - String path; - path = string::tjoin(l.array_view(), path); - - return (String)path::new(allocator, path); - }; -} - - -fn void update_config() -{ - bool config_found = false; - foreach (c: CONFIG_PATHS) { - String? p = c.to_expanded_path(tmem); - if (catch e = p) { - io::eprintfn("Error expanding path %s: %s", c, e); - continue; - } - if (!file::exists(p)) { - io::printfn("%s not present", p); - continue; - } - - // Read file - File f = file::open(p, "r")!!; - Object*? o = ini::load_temp(&f).get(ini::GLOBAL); - if (catch e = o) { - io::eprintfn("Error reading config file %s: %s", p, e); - continue; - } - - // Read backlight device folder - String? backlight = o.get_string("backlight"); - if (catch e = backlight) { - io::eprintfn("Error reading backlight path from config: %s", e); - continue; - } - if (!file::is_dir(backlight)) { - io::eprintfn("Error setting the backlight path: path should be a directory"); - continue; - } - backlight_path = backlight; - - // Read light sensor device path - String? sensor = o.get_string("sensor"); - if (catch e = sensor) { - io::eprintfn("Error reading luminance sensor path from config: %s", e); - continue; - } - light_sensor_path = sensor; - - // Optional configurations - - // Read the max screen brightness - if (try x = o.get_string("max_brightness").to_float()) { - backlight_max_luminance = (float)x; - } - - // Read the light sensor factor - if (try x = o.get_string("sensor_factor").to_float()) { - light_sensor_k = (float)x; - } - - // Read the light sensor factor - if (try x = o.get_string("update_interval").to_int()) { - update_interval_ms = x; - } - - config_found = true; - break; - } - - if (!config_found) { - io::eprintfn("Keping previous configuration"); - } -} - - -macro float @bezier(float t) -{ - const float B = 0.66; - const float D = 0.28; - return 3*B*t+(3*D-6*B)*t*t+(1+3*B-3*D)*t*t*t; -} - - -// https://docs.kernel.org/admin-guide/abi-stable-files.html#abi-file-stable-sysfs-class-backlight -macro int? @read_int_from_file(String path) => ((String)file::load_temp(path)[..^2]).to_int(); -macro void @write_int_to_file(String path, int i) => (void)file::save(path, string::tformat("%d\n", i)); -macro int? @get_luminance() => @read_int_from_file(light_sensor_path); -macro int? @get_max_backlight() => @read_int_from_file(backlight_path.tconcat("/max_brightness")); -macro int? @get_backlight() => @read_int_from_file(backlight_path.tconcat("/brightness")); -macro void @set_backlight(int v) => @write_int_to_file(backlight_path.tconcat("/brightness"), v); -macro bool? @is_screen_on() => @read_int_from_file(backlight_path.tconcat("/bl_power")) == 0; - -macro @retry_every(#f, int ms = 100) -{ - const sz TRIES = 10; - for (sz i = 0; i < TRIES; i++) { - if (try x = #f) return x; - thread::sleep_ms(ms); - } - io::eprintfn("ERROR: tried to execute '%s' but it failed too many times (%d)", $stringify(#f), TRIES); - std::os::exit(1); -} - - -fn void sigusr1_handler(CInt sig, void* act, void* oldact) -{ - (void)sig; - (void)act; - (void)oldact; - update_config(); -} - - -// TODO: add command line arguments, like -h, -v and -d -// TODO: daemonization -// TODO: override config file with arguments -fn void main(String[] args) -{ - bool verbose = false; - foreach (a : args[1..]) { - switch (a) { - case "-v": - verbose = true; - break; - case "-h": - io::printfn("ambientd [-hv]"); - std::os::exit(0); - default: - io::eprintfn("Unrecognized argument: '%s'", a); - std::os::exit(1); - } - } - - update_config(); - - posix::install_signal_handler(libc::SIGUSR1, &sigusr1_handler); - - int _l = @get_luminance() ?? 0; - filter.init(_l); - float luminance_old = filter.update(_l, 0.0); - int brightness_set = @retry_every(@get_backlight()); - int user_offset = 0; - int backlight_max = @get_max_backlight() ?? 0; - Time start = time::now(); - - while (true) { - if (!@is_screen_on() ?? false) { - thread::sleep_ms(5000); - continue; - } - - - // Check user offset - user_offset = @retry_every(@get_backlight(), update_interval_ms/2) - brightness_set; - int _lx = @retry_every(@get_luminance(), update_interval_ms/2); - - Time now = time::now(); - Time dt = (Time)(now - start); - - // Filtered luminance, depends heavily on the type of sensor, for the CF-SV7 - // seems to be in lux - float luminance = filter.update(_lx, (float)dt.to_seconds()) * light_sensor_k; - // Percent of the luminance received compared to what the panel can output - float p = min((luminance/backlight_max_luminance), 1.0f); - - // int new_brightness = (int)(@bezier(p)*backlight_max); - // The panel should output slightly more light than the ambient, moreover the - // relationship between a percent increase in the backlight power and the - // perceived luminance is non-linear, I think the inverse square law is correct - int new_brightness = (int)(math::pow(p*1.1f, 0.5f)*backlight_max); - brightness_set = new_brightness; - @set_backlight(max(brightness_set + user_offset, 0)); - - if (verbose) io::printfn("%f, %d, %d", luminance, brightness_set, user_offset); - - start = now; - thread::sleep_ms(update_interval_ms); - } -} diff --git a/ambientd.py b/ambientd.py new file mode 100755 index 0000000..c8bfb5e --- /dev/null +++ b/ambientd.py @@ -0,0 +1,73 @@ +#!/usr/bin/python + +# Simple daemon to change brightness based on luminance +# TODO: add a config +# TODO: add debug option +# TODO: add dry-run +# TODO automatically find the right brightness path and do not assume intel_backlight + +import time + +# Open the luminance sensor device file +try: + luminance_file = open('/sys/bus/iio/devices/iio:device0/in_illuminance_raw', 'r') +except OSError as err: + print(err) + exit(1) + +# max luminance of the screen - 100 (maybe) +max_luminance = 400 + +# get luminance value, on fail return the max +def get_luminance(): + try: + l = int(luminance_file.read()) + luminance_file.seek(0) + except: + l = max_luminance + return l + +# Get the max brightness +max_brightness = 100 +with open('/sys/class/backlight/intel_backlight/max_brightness', 'r') as f: + max_brightness = int(f.read()) +min_brightness = int(max_brightness * 0.05) + +# loop delay in seconds +delay = 3 + +lum = [get_luminance()]*4 +user_offset = 0 +brightness_target = min_brightness +brightness_old = min_brightness +while True: + lum.append(lum.pop(0)) + lum[0] = get_luminance() + + luminance = sum(lum)/len(lum) + luminance_percent = min(luminance/max_luminance, 1.0) + brightness_old = brightness_target + brightness_target = max(int(luminance_percent * max_brightness), min_brightness) + brightness_delta = abs((brightness_target - brightness_old)/brightness_old) + + # if the brightness changes by more than 10% reduce the user offset, scaling + # with the delta, this accounts for large changes in brightness no longer + # corresponding to the adjustments that the user made to the brightness + if brightness_delta > 0.1: + user_offset = int(user_offset * max(min(1-(brightness_delta*1.2), 1), 0)) + + brightness_adjusted = min(max(brightness_target + user_offset, min_brightness), max_brightness) + b = open('/sys/class/backlight/intel_backlight/brightness', 'w') + b.write(str(brightness_adjusted)) + b.close() + + time.sleep(delay) + + # if the value of the brightness changed then update the user offset + a = open('/sys/class/backlight/intel_backlight/actual_brightness') + x = int(a.read()) - brightness_target + a.close() + if brightness_target != max_brightness or x < 0: + user_offset = x + + print("brightness target: ", brightness_target, "user offset: ", user_offset, "brightness adjusted: ", brightness_adjusted)