rewritten, much simpler with config file now
This commit is contained in:
parent
3efc3b1974
commit
36c4aff457
2 changed files with 225 additions and 177 deletions
388
ambientd.c3
388
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue