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); } }