From 36c4aff4570d14a7a6d21170e6f40c2d52f9cf2a Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Wed, 5 Aug 2026 15:34:38 +0200 Subject: [PATCH] rewritten, much simpler with config file now --- TODO | 14 -- ambientd.c3 | 388 ++++++++++++++++++++++++++++++---------------------- 2 files changed, 225 insertions(+), 177 deletions(-) delete mode 100644 TODO diff --git a/TODO b/TODO deleted file mode 100644 index 5bd205a..0000000 --- a/TODO +++ /dev/null @@ -1,14 +0,0 @@ -[ ] Add a config file -[ ] Use a better filter than a simple median, maybe a kalman filter -[x] 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 - [x] Handle kill by closing all file descriptors - (every read is done by only keeping the file open for as long as it is needed) - [ ] Handle suspend and resume, usually suspending means that the screen is - turned off so do not apply user offset -[ ] Account for DPMS and do not apply offset in that case diff --git a/ambientd.c3 b/ambientd.c3 index 4c7c0d4..675d911 100644 --- a/ambientd.c3 +++ b/ambientd.c3 @@ -1,206 +1,268 @@ +module ambientd; + import std::io; -import std::io::file; +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; -// import some functions from the C standard library -extern fn long strtol(char *buffer, char **endptr, int base); -const String LUMINANCE_DEV = "/sys/bus/iio/devices/iio:device0/in_illuminance_raw"; -const String MAX_BRIGHTNESS_DEV = "/sys/class/backlight/intel_backlight/max_brightness"; -const String BRIGHTNESS_DEV = "/sys/class/backlight/intel_backlight/brightness"; -const String ACTUAL_BRIGHTNESS_DEV = "/sys/class/backlight/intel_backlight/actual_brightness"; +alias StrList = List{String}; -char[128] str_buffer; -// max luminance that the display can output -const int MAX_LUMINANCE = 700; +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; -// minimum display brightness in percent -const float MIN_BRIGHTNESS_PERCENT = 0.05; +const String[*] CONFIG_PATHS = { + "$XDG_CONFIG_HOME/ambientd/ambientd.ini", + "$HOME/.config/ambientd/ambientd.ini", + "/etc/ambientd/ambientd.ini", +}; -const int SLEEP_DELAY = 2500; -const int SMOOTH_DURATION = 500; -const int SMOOTH_SAMPLES = 20; -macro abs(x) { return x < 0 ? -x : x; } -macro clamp(x, l, h) { return max(min(x, h), l); } +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; -/** - * read a string to the buffer up until the end of the buffer or the first '\n' - * @require fp != null -**/ -fn void! io::File.gets(&self, char[] buffer) -{ - usz i = 0; - char !c; - for (; i < buffer.len-1; i++) { - c = self.read_byte(); - if (catch excuse = c) { - if (excuse == io::IoError.EOF) { - break; - } else { - return excuse?; - } - } - buffer[i] = c; - } - buffer[i] = '\0'; + +struct LuminanceFilter { + float x; // estimate + float p; // error + float r; // mesurement noise + float q; // process noise } -/** - * read a string from a file and convert it to int, should only be used for - * positive numbers since negative numbers mean an error - * @ensure return > 0, return < 1000000 -**/ -fn int! read_int(String path) -{ - static io::File! fp; - fp = io::file::open((String)path, "r")!; - defer (void)fp.close(); - fp.gets(str_buffer[..])!; - int r = (int)strtol(&str_buffer, null, 10); - return r; +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; } -// write an intger value to a file as a string -fn void! write_int(String path, int value) -{ - static io::File! fp; - fp = io::file::open(path, "w")!; - defer (void)fp.close(); - char[] b = io::bprintf(str_buffer[..], "%d\n", value)!; - fp.write(b)!; +fn void LuminanceFilter.init(&self, int reading) +{ + self.p = 0.0; + self.x = (float)reading; } -// returns the raw luminance value -macro get_luminance_raw() { return read_int(LUMINANCE_DEV); } -// write the brightness value, on fail log on stderr -macro write_brightness(int b) +// 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 = {}) { - anyfault e = @catch(write_int(BRIGHTNESS_DEV, b)); - if (e) { - io::eprintfn("Error writing brightness: %s", e); + @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"); } } -fn float bezier(float t) + +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; } -// TODO: add some command line options like: -// -v: verbose/logging -// -c file; specify a config file -fn int main() + +// 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() ?? 0; +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; + + +fn void sigusr1_handler(CInt sig, void* act, void* oldact) { - // open the luminance device and try reading from it - if (catch excuse = get_luminance_raw()) { - io::eprintfn("Error on initial opening of %s: %s", LUMINANCE_DEV, excuse); - thread::exit(1); - } + (void)sig; + (void)act; + (void)oldact; + update_config(); +} - // get the max brightness - int max_brightness; - int min_brightness; - int! opt = read_int(MAX_BRIGHTNESS_DEV); - if (try unwrapped = opt) { - max_brightness = unwrapped; - } if (catch excuse = opt) { - io::eprintfn("Error reading max brightness on %s: %s", MAX_BRIGHTNESS_DEV, excuse); - return 1; - } - min_brightness = (int)(max_brightness * MIN_BRIGHTNESS_PERCENT); - // try writing to brightness device - int! br = read_int(BRIGHTNESS_DEV); - if (catch excuse = br) { - io::eprintfn("Error reading from %s: %s", BRIGHTNESS_DEV, excuse); - return 1; - } - if (catch excuse = write_int(BRIGHTNESS_DEV, br)) { - io::eprintfn("Error writing to brightness device %s: %s", BRIGHTNESS_DEV, excuse); - return 1; - } +// TODO: add command line arguments, like -h, -v and -d +// TODO: daemonization +// TODO: override config file with arguments +fn void main(String[] args) +{ + update_config(); - // try reading from the actual brightness device - if (catch excuse = read_int(ACTUAL_BRIGHTNESS_DEV)) { - io::eprintfn("Error reading from %s: %s", ACTUAL_BRIGHTNESS_DEV, excuse); - return 1; - } + posix::install_signal_handler(libc::SIGUSR1, &sigusr1_handler); - int x = get_luminance_raw() ?? MAX_LUMINANCE; - int[<4>] luminance_samples = x; - usz idx; - int user_offset; - int brightness_target = min_brightness; - int brightness_old = min_brightness; - int brightness_adjusted = min_brightness; - int brightness_delta; - float luminance_median; - float luminance_percent; - while (1) { - int! tmp = get_luminance_raw(); - if (catch excuse = tmp) { - io::eprintfn("Error reading luminance: %s", excuse); - } else { - luminance_samples[idx] = tmp; - idx = (idx + 1) % luminance_samples.len; + filter.init(@get_luminance()); + float luminance_old = filter.update(@get_luminance(), 0.0); + int brightness_set = @get_backlight(); + int user_offset = 0; + int backlight_max = @get_max_backlight(); + Time start = time::now(); + + while (true) { + if (!@is_screen_on()) { + thread::sleep_ms(5000); + continue; } - luminance_median = (float)luminance_samples.sum() / luminance_samples.len; - luminance_percent = bezier(clamp(luminance_median/MAX_LUMINANCE, 0.0f, 1.0f)); + Time now = time::now(); + Time dt = (Time)(now - start); - brightness_old = brightness_target; - brightness_target = max((int)(luminance_percent * max_brightness), min_brightness); - brightness_delta = (int)abs((float)(brightness_target - brightness_old)/brightness_old); + // Check user offset + user_offset = @get_backlight() - brightness_set; - // 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 * clamp(1-(int)(brightness_delta*1.2), 0, 1)); - } - brightness_adjusted = clamp(brightness_target + user_offset, min_brightness, max_brightness); + // Filtered luminance, depends heavily on the type of sensor, for the CF-SV7 + // seems to be in lux + float luminance = filter.update(@get_luminance(), (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); - // animate the transition to the brightness target - {| - int brightness_now = read_int(ACTUAL_BRIGHTNESS_DEV) ?? brightness_adjusted; - if (brightness_now == brightness_adjusted) { return; } - int step = (brightness_adjusted-brightness_now)/(SMOOTH_SAMPLES); - for (int i; i < SMOOTH_SAMPLES; i++) { - write_brightness(brightness_now + step*i); - thread::sleep_ms(SMOOTH_DURATION/SMOOTH_SAMPLES); - } - write_brightness(brightness_adjusted); - |}; + // 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(brightness_set + user_offset); - thread::sleep_ms(SLEEP_DELAY); + // io::printfn("%f, %d, %d", luminance, brightness_set, user_offset); - // read actual brightness to get the user offset - int actual_brightness; - int off; - tmp = read_int(ACTUAL_BRIGHTNESS_DEV); - actual_brightness = tmp ?? brightness_adjusted; - if (catch excuse = tmp) { - io::eprintfn("Error reading actual brightness: %s", excuse); - } - - // if the actual brightness is zero, it means that the screen is turned - // off for some reason, usually DPMS. As such it should not count for user - // offset. Look at the TODO file. - if (actual_brightness != 0) { - off = actual_brightness - brightness_target; - if (brightness_target != max_brightness || off < 0){ - user_offset = off; - } - } - - // TODO: option to turn off logging - io::printfn("brightness {target=%d, adjusted=%d}, offset=%d, luminance=%d (%.1f%%)", brightness_target, brightness_adjusted, user_offset, luminance_median, luminance_percent*100); + start = now; + thread::sleep_ms(update_interval_ms); } }