diff --git a/.gitignore b/.gitignore index b330eb6..058ec61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ ambientd !sv/** .ccls-cache +.zed diff --git a/Makefile b/Makefile index 74fa8f7..3251055 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,7 @@ -CC = gcc -CFLAGS = -Wall -Wextra -pedantic -std=c99 -O2 PREFIX = /usr/local -ambientd: ambientd.c +ambientd: ambientd.c3 + c3c compile -O1 -g0 --optsize=small --fp-math=fast --x86cpu=native ambientd.c3 install: cp ambientd ${PREFIX}/bin/ diff --git a/TODO b/TODO deleted file mode 100644 index 2c6d014..0000000 --- a/TODO +++ /dev/null @@ -1,10 +0,0 @@ -[ ] 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 deleted file mode 100644 index 7bd413e..0000000 --- a/ambientd.c +++ /dev/null @@ -1,178 +0,0 @@ -#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 new file mode 100644 index 0000000..6cfd808 --- /dev/null +++ b/ambientd.c3 @@ -0,0 +1,297 @@ +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 deleted file mode 100755 index c8bfb5e..0000000 --- a/ambientd.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/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)