diff --git a/ambientd.c3 b/ambientd.c3 new file mode 100644 index 0000000..6aff2e6 --- /dev/null +++ b/ambientd.c3 @@ -0,0 +1,198 @@ +import std::io; +import std::io::file; +import std::thread; + +// import some functions from the C standard library +extern fn long strtol(char *buffer, char **endptr, int base); + +const int MEDIAN_SAMPLES = 4; + +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"; + +char[128] str_buffer; + +// max luminance that the display can output +const int MAX_LUMINANCE = 700; + +// minimum display brightness in percent +const float MIN_BRIGHTNESS_PERCENT = 0.05; + +macro abs(x) +{ + if (x < 0) { + return -x; + } else { + return x; + } +} + +// l <= x <= h +macro clamp(x, l, h) +{ + return max(min(x, h), l); +} + +// 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'; +} + +// 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; +} + +// 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)!; +} + +// returns the luminance value in percentage between 0 and 1 +macro get_luminance_raw() +{ + return read_int(LUMINANCE_DEV); +} + +fn 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() +{ + // 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); + } + + // get the max brightness + int! _x = read_int(MAX_BRIGHTNESS_DEV); + if (catch excuse = _x) { + io::eprintfn("Error reading max brightness on %s: %s", MAX_BRIGHTNESS_DEV, excuse); + thread::exit(1); + } + int max_brightness = _x!!; + int min_brightness = (int)(max_brightness * MIN_BRIGHTNESS_PERCENT); + + // try writing to brightness device + _x = read_int(BRIGHTNESS_DEV); + if (catch excuse = _x) { + io::eprintfn("Error reading from %s: %s", BRIGHTNESS_DEV, excuse); + thread::exit(1); + } + if (catch excuse = write_int(BRIGHTNESS_DEV, _x)) { + io::eprintfn("Error writing to brightness device %s: %s", BRIGHTNESS_DEV, excuse); + thread::exit(1); + } + + // 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); + thread::exit(1); + } + + _x = get_luminance_raw()!!; + int[<*>] luminance_samples = {_x!!, _x!!, _x!!, _x!!}; + int idx; + int user_offset; + int brightness_target = min_brightness; + int brightness_old = min_brightness; + int brightness_adjusted = min_brightness; + int brightness_delta; + int luminance_median; + float luminance_percent; + while (1) { + _x = get_luminance_raw(); + if (catch excuse = _x) { + io::eprintfn("Error reading luminance: %s", excuse); + } else { + idx = (idx + 1) % MEDIAN_SAMPLES; + } + + luminance_median = luminance_samples.sum() / MEDIAN_SAMPLES; + luminance_percent = bezier(clamp(luminance_median/MAX_LUMINANCE, 0.0f, 1.0f)); + + 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); + + // 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); + if (catch excuse = write_int(BRIGHTNESS_DEV, brightness_adjusted)) { + io::eprintfn("Error writing brightness: %s", excuse); + } + + thread::sleep_ms(3000); + + // read actual brightness to get the user offset + int! actual_brightness; + int off; + actual_brightness = read_int(ACTUAL_BRIGHTNESS_DEV); + if (catch excuse = actual_brightness) { + io::eprintfn("Error reading actual brightness: %s", excuse); + actual_brightness = brightness_adjusted; + } + + // 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; user offset: %d; brightness adjusted: %d", brightness_target, user_offset, brightness_adjusted); + } +}