diff --git a/.build.yml b/.build.yml index 09283ce..398eb3e 100644 --- a/.build.yml +++ b/.build.yml @@ -13,18 +13,20 @@ packages: - libjpeg-turbo - wayland - libdrm + - pipewire sources: - https://github.com/FedeDP/Clightd tasks: - prepare: | cd Clightd - mkdir build build-no-gamma build-no-dpms build-no-ddc build-no-screen build-no-yoctolight build-no-extras - (cd build && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_GAMMA=1 -DENABLE_DPMS=1 -DENABLE_DDC=1 -DENABLE_SCREEN=1 -DENABLE_YOCTOLIGHT=1 ..) - (cd build-no-gamma && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_DPMS=1 -DENABLE_DDC=1 -DENABLE_SCREEN=1 -DENABLE_YOCTOLIGHT=1 ..) - (cd build-no-dpms && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_GAMMA=1 -DENABLE_DDC=1 -DENABLE_SCREEN=1 -DENABLE_YOCTOLIGHT=1 ..) - (cd build-no-ddc && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_GAMMA=1 -DENABLE_DPMS=1 -DENABLE_SCREEN=1 -DENABLE_YOCTOLIGHT=1 ..) - (cd build-no-screen && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_GAMMA=1 -DENABLE_DPMS=1 -DENABLE_DDC=1 -DENABLE_YOCTOLIGHT=1 ..) - (cd build-no-yoctolight && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_GAMMA=1 -DENABLE_DPMS=1 -DENABLE_DDC=1 -DENABLE_SCREEN=1 ..) + mkdir build build-no-gamma build-no-dpms build-no-ddc build-no-screen build-no-yoctolight build-no-pipewire build-no-extras + (cd build && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_GAMMA=1 -DENABLE_DPMS=1 -DENABLE_DDC=1 -DENABLE_SCREEN=1 -DENABLE_YOCTOLIGHT=1 -DENABLE_PIPEWIRE=1 ..) + (cd build-no-gamma && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_DPMS=1 -DENABLE_DDC=1 -DENABLE_SCREEN=1 -DENABLE_YOCTOLIGHT=1 -DENABLE_PIPEWIRE=1 ..) + (cd build-no-dpms && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_GAMMA=1 -DENABLE_DDC=1 -DENABLE_SCREEN=1 -DENABLE_YOCTOLIGHT=1 -DENABLE_PIPEWIRE=1 ..) + (cd build-no-ddc && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_GAMMA=1 -DENABLE_DPMS=1 -DENABLE_SCREEN=1 -DENABLE_YOCTOLIGHT=1 -DENABLE_PIPEWIRE=1 ..) + (cd build-no-screen && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_GAMMA=1 -DENABLE_DPMS=1 -DENABLE_DDC=1 -DENABLE_YOCTOLIGHT=1 -DENABLE_PIPEWIRE=1 ..) + (cd build-no-yoctolight && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_GAMMA=1 -DENABLE_DPMS=1 -DENABLE_DDC=1 -DENABLE_SCREEN=1 -DENABLE_PIPEWIRE=1 ..) + (cd build-no-pipewire && cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_GAMMA=1 -DENABLE_DPMS=1 -DENABLE_DDC=1 -DENABLE_SCREEN=1 -DENABLE_YOCTOLIGHT=1 ..) (cd build-no-extras && cmake -DCMAKE_BUILD_TYPE=Debug ..) - build: | cd Clightd @@ -34,6 +36,7 @@ tasks: (cd build-no-ddc && make) (cd build-no-screen && make) (cd build-no-yoctolight && make) + (cd build-no-pipewire && make) (cd build-no-extras && make) triggers: - action: email diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e3aedb..aa8fb8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,6 +99,7 @@ optional_dep(DPMS "x11;xext;libdrm;wayland-client" "DPMS" protocol/org_kde_kwin_ optional_dep(SCREEN "x11" "screen emitted brightness" protocol/wlr-screencopy-unstable-v1.xml src/modules/screen_plugins) optional_dep(DDC "ddcutil>=0.9.5" "external monitor backlight") optional_dep(YOCTOLIGHT "libusb-1.0" "Yoctolight usb als devices support") +optional_dep(PIPEWIRE "libpipewire-0.3" "Enable pipewire camera sensor support") # Convert ld flag list from list to space separated string. string(REPLACE ";" " " COMBINED_LDFLAGS "${COMBINED_LDFLAGS}") @@ -111,7 +112,7 @@ set_target_properties( # Installation of targets (must be before file configuration to work) install(TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION "${CMAKE_INSTALL_LIBEXECDIR}") + RUNTIME DESTINATION "${CMAKE_INSTALL_FULL_LIBEXECDIR}") set(SCRIPT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/Scripts") @@ -142,6 +143,8 @@ if(SYSTEMD_BASE_FOUND) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/clightd.service DESTINATION ${SYSTEMD_SERVICE_DIR}) + + set(SYSTEMD_SERVICE "SystemdService=clightd.service") endif() # Install dbus service diff --git a/Scripts/clightd.service b/Scripts/clightd.service index 5ea5b8c..f229b27 100644 --- a/Scripts/clightd.service +++ b/Scripts/clightd.service @@ -7,8 +7,14 @@ Requires=@POLKIT_NAME@.service Type=dbus BusName=org.clightd.clightd User=root -# Default backlight vcp code; update if needed +# Default backlight vcp code; update if needed. +# Moreover, you can also specify per-monitor BL VCP value, +# using CLIGHTD_BL_VCP_$mon_id, where mon_id is the monitor identifier +# as seen by Clightd; you can explore them using: +# $ busctl call org.clightd.clightd /org/clightd/clightd/Backlight2 org.clightd.clightd.Backlight2 Get Environment=CLIGHTD_BL_VCP=0x10 +# Default pipewire runtime dir watched by Clightd +Environment=CLIGHTD_PIPEWIRE_RUNTIME_DIR=/run/user/1000/ ExecStart=@CMAKE_INSTALL_FULL_LIBEXECDIR@/clightd Restart=on-failure RestartSec=5 diff --git a/Scripts/org.clightd.clightd.service b/Scripts/org.clightd.clightd.service index e80f75e..4adf6da 100644 --- a/Scripts/org.clightd.clightd.service +++ b/Scripts/org.clightd.clightd.service @@ -2,4 +2,4 @@ Name=org.clightd.clightd Exec=@CMAKE_INSTALL_FULL_LIBEXECDIR@/clightd User=root -SystemdService=clightd.service +@SYSTEMD_SERVICE@ diff --git a/TODO.md b/TODO.md index e8b6599..b96f908 100644 --- a/TODO.md +++ b/TODO.md @@ -30,22 +30,59 @@ - [x] Drop bl_store_vpcode() and only load vpcode from CLIGHTD_BL_VCP env? - [x] Add CLIGHTD_BL_VCP Environment variable to systemd script with a comment thus it is simple to update it if needed - [x] Expose Max and Internal properties -- [ ] Update dbus api wiki +- [x] Update dbus api wiki - [x] add a page about monitor hotplugging (dep on ddcutil >= 1.2.0 and refresh time!) - [x] Investigate memleaks (related to ddca_redetect_displays()?) -> see here: https://github.com/rockowitz/ddcutil/issues/202 - [x] Instead of 30s sleep, use an udev_monitor on drm subsystem? +- [x] Add support for monitor id specific CLIGHTD_BL_VCP env ### KbdBacklight - [x] call sd_bus_emit_object_added() sd_bus_emit_object_removed() When object path are created/deleted - [x] Fix: udev_reference is a snapshot of an udev device at a current time. Wrong! +- [x] Fixed (small) memleak + +### ALS +- [x] Fix: avoid using cached udev_dev reference in loop (thus always returning same ambient brightness read during a Capture request) +- [x] Fixed EIO errors + +### Sensor +- [x] Only emit Sensor.Changed signal for added/removed devices + +### Pipewire +- [x] Support pipewire for Camera sensor? This would allow multiple application sharing camera +- [x] Pipewire run as root needs XDG_RUNTIME_DIR env -> workaround: find the first /run/user folder and use that +- [x] Unify camera settings between camera and pipewire sensors... ? +- [x] Support monitor sensor api for pipewire +- [x] Fix segfault +- [x] Fix subsequent Capture +- [x] Add a CLIGHTD_PW_RUNTIME_DIR env variable (in clightd.service, see CLIGHTD_BL_CODE) that defaults to /run/user/1000/. If the env variable is empty -> disable pipewire. If folder does not exist: disable pipewire. Otherwise: inotify on folder to wait for socket to appear. If socket is already there, immediately start monitoring. +- [x] Document the new env variable! +- [x] Use caller uid instead of defaulting to first found user during Capture! +- [x] Use a map to store list of nodes? +- [x] Free list of nodes upon exit! +- [x] Fix xdg_runtime_dir set to create monitor +- [x] Fix memleaks +- [x] Support crop settings +- [x] Test crop +- [x] Drop crop API support for both pipewire and webcam; they add lots of complexity while giving no real perf improvements considering we are using small frames +- [x] Fix: pipewire capture while webcam is already owned by another app freezes during pw_loop_iterate() ### Generic - [x] When built with ddcutil, clightd.service should be started after systemd-modules-load.service - [x] Show commit hash in version +- [x] All api that require eg Xauth or xdg rutime user, fallback at automatically fetching a default value given the caller: +- [x] test X +- [x] test wl +- [x] Document the new behavior! +- [x] Fix clightd not cleanly exiting when built with DDC or YOCTOLIGHT (most probably libusb or whatever is creating another thread that is stealing the signal!) +- [x] do not use strlen() to only check that strign is not empty ## 5.x - [ ] Keep it up to date with possible ddcutil api changes +### Pipewire +- [ ] Fix set_camera_setting() impl -> how to get current value? how to set a new value? + ## 6.x (api break release) ### Generic @@ -54,9 +91,15 @@ - [ ] Drop old BACKLIGHT module -> in case, drop {Lower,Raise,Set}All from clightd polkit policy - [ ] Rename Backlight2 to Backlight -### Pipewire -- [ ] merge pipewire work +### Move to user service (?) - [ ] move clightd to user service +- [ ] Drop polkit and use sd_session_is_active() +- [ ] Add udev rules for yoctolight, als, keyboard and backlight modules for "clightd" +- [ ] Drop useless API params (eg: DISPLAY, XAUTHORITY, XDG_RUNTIME_DIR etc etc) +- [ ] # groupadd clightd +- [ ] # usermod -aG clightd myusername +- [ ] # echo 'KERNEL=="i2c-[0-9]*", GROUP="clightd"' >> /etc/udev/rules.d/10-local_i2c_group.rules +... would break https://github.com/FedeDP/Clight/issues/144 ... ## Ideas - [ ] follow ddcci kernel driver and in case, drop ddcutil and add the kernel driver as clightd opt-dep diff --git a/src/commons.h b/src/commons.h index 4bf83b2..27dc600 100644 --- a/src/commons.h +++ b/src/commons.h @@ -20,8 +20,9 @@ #include #define SIZE(x) (sizeof(x) / sizeof(*x)) -#define _ctor_ __attribute__((constructor (101))) // Used for Sensors registering -#define _dtor_ __attribute__((destructor (101))) // Used for libusb dtor + +#define _ctor_ __attribute__((constructor (101))) // Used for plugins registering (sensor, gamma, dpms, screen) and libusb/libpipewire init +#define _dtor_ __attribute__((destructor (101))) // Used for libusb and libpipewire dtor /* Used by dpms, gamma and screen*/ #define UNSUPPORTED INT_MIN diff --git a/src/modules/backlight2.c b/src/modules/backlight2.c index 529076b..ffae2fd 100644 --- a/src/modules/backlight2.c +++ b/src/modules/backlight2.c @@ -6,6 +6,7 @@ #define BL_SUBSYSTEM "backlight" #define DRM_SUBSYSTEM "drm" +#define BL_VCP_ENV "CLIGHTD_BL_VCP" typedef struct { double target_pct; @@ -104,14 +105,14 @@ MODULE("BACKLIGHT2"); static DDCA_Vcp_Feature_Code br_code = 0x10; static void bl_load_vpcode(void) { - if (getenv("CLIGHTD_BL_VCP")) { - br_code = strtol(getenv("CLIGHTD_BL_VCP"), NULL, 16); - m_log("Set 0x%x vcp code.\n", br_code); + if (getenv(BL_VCP_ENV)) { + br_code = strtol(getenv(BL_VCP_ENV), NULL, 16); + m_log("Set default 0x%x vcp code.\n", br_code); } } static void get_info_id(char *id, const int size, const DDCA_Display_Info *dinfo) { - if (!strlen(dinfo->sn) || !strcasecmp(dinfo->sn, "Unspecified")) { + if ((dinfo->sn[0] == '\0') || !strcasecmp(dinfo->sn, "Unspecified")) { switch(dinfo->path.io_mode) { case DDCA_IO_I2C: snprintf(id, size, "/dev/i2c-%d", dinfo->path.path.i2c_busno); @@ -423,9 +424,17 @@ static int set_external_backlight(bl_t *bl, int value) { #ifdef DDC_PRESENT DDCA_Display_Handle dh = NULL; if (!ddca_open_display2(bl->dev, false, &dh)) { + DDCA_Vcp_Feature_Code specific_br_code; + char specific_br_env[64]; + snprintf(specific_br_env, sizeof(specific_br_env), BL_VCP_ENV"_%s", bl->sn); + if (getenv(specific_br_env)) { + specific_br_code = strtol(getenv(specific_br_env), NULL, 16); + } else { + specific_br_code = br_code; + } int8_t new_sh = (value >> 8) & 0xff; int8_t new_sl = value & 0xff; - ret = ddca_set_non_table_vcp_value(dh, br_code, new_sh, new_sl); + ret = ddca_set_non_table_vcp_value(dh, specific_br_code, new_sh, new_sl); ddca_close_display(dh); } #endif diff --git a/src/modules/dpms.c b/src/modules/dpms.c index e58bb8b..069265c 100644 --- a/src/modules/dpms.c +++ b/src/modules/dpms.c @@ -2,6 +2,7 @@ #include "dpms.h" #include "polkit.h" +#include "bus_utils.h" static int method_getdpms(sd_bus_message *m, void *userdata, sd_bus_error *ret_error); static int method_setdpms(sd_bus_message *m, void *userdata, sd_bus_error *ret_error); @@ -94,6 +95,8 @@ static int method_getdpms(sd_bus_message *m, void *userdata, sd_bus_error *ret_e return r; } + bus_sender_fill_creds(m); // used by PW plugin + /* * Note: this is freed by drm plugin if it is an empty string * to get a default drm device. @@ -148,6 +151,8 @@ static int method_setdpms(sd_bus_message *m, void *userdata, sd_bus_error *ret_e return -EINVAL; } + bus_sender_fill_creds(m); // used by PW plugin + /* * Note: this is freed by drm plugin if it is an empty string * to get a default drm device. diff --git a/src/modules/dpms_plugins/xorg.c b/src/modules/dpms_plugins/xorg.c index deeba07..9e9961c 100644 --- a/src/modules/dpms_plugins/xorg.c +++ b/src/modules/dpms_plugins/xorg.c @@ -1,6 +1,7 @@ -#include +#include "xorg_utils.h" #include #include "dpms.h" +#include "bus_utils.h" DPMS("Xorg"); @@ -17,9 +18,7 @@ static int get(const char **display, const char *xauthority) { CARD16 s; int ret = WRONG_PLUGIN; - setenv("XAUTHORITY", xauthority, 1); - - Display *dpy = XOpenDisplay(*display); + Display *dpy = fetch_xorg_display(display, xauthority); if (dpy) { if (DPMSCapable(dpy)) { DPMSInfo(dpy, &s, &onoff); @@ -28,19 +27,14 @@ static int get(const char **display, const char *xauthority) { ret = UNSUPPORTED; } XCloseDisplay(dpy); - } - - unsetenv("XAUTHORITY"); + } return ret; } static int set(const char **display, const char *xauthority, int dpms_level) { int ret = WRONG_PLUGIN; - /* set xauthority cookie */ - setenv("XAUTHORITY", xauthority, 1); - - Display *dpy = XOpenDisplay(*display); + Display *dpy = fetch_xorg_display(display, xauthority); if (dpy) { if (DPMSCapable(dpy)) { DPMSEnable(dpy); @@ -52,8 +46,5 @@ static int set(const char **display, const char *xauthority, int dpms_level) { } XCloseDisplay(dpy); } - - /* Drop xauthority cookie */ - unsetenv("XAUTHORITY"); return ret; } diff --git a/src/modules/gamma.c b/src/modules/gamma.c index e52bde6..6b0714f 100644 --- a/src/modules/gamma.c +++ b/src/modules/gamma.c @@ -10,6 +10,7 @@ #include #include #include "gamma.h" +#include "bus_utils.h" static unsigned short get_red(int temp); static unsigned short get_green(int temp); @@ -269,6 +270,8 @@ static int method_setgamma(sd_bus_message *m, void *userdata, sd_bus_error *ret_ if (temp < 1000 || temp > 10000) { error = EINVAL; } else { + bus_sender_fill_creds(m); + gamma_client *sc = map_get(clients, display); if (!sc) { sc = fetch_client(userdata, display, env, &error); @@ -311,6 +314,8 @@ static int method_getgamma(sd_bus_message *m, void *userdata, sd_bus_error *ret_ return r; } + bus_sender_fill_creds(m); // used by PW plugin + gamma_client *cl = map_get(clients, display); if (cl) { temp = cl->current_temp; diff --git a/src/modules/gamma_plugins/xorg.c b/src/modules/gamma_plugins/xorg.c index ab83b42..ff72ea1 100644 --- a/src/modules/gamma_plugins/xorg.c +++ b/src/modules/gamma_plugins/xorg.c @@ -1,5 +1,7 @@ #include "gamma.h" #include +#include "bus_utils.h" +#include "xorg_utils.h" #include typedef struct { @@ -12,9 +14,7 @@ GAMMA("Xorg"); static int validate(const char **id, const char *env, void **priv_data) { int ret = WRONG_PLUGIN; - setenv("XAUTHORITY", env, 1); - - Display *dpy = XOpenDisplay(*id); + Display *dpy = fetch_xorg_display(id, env); if (dpy) { int screen = DefaultScreen(dpy); Window root = RootWindow(dpy, screen); @@ -30,8 +30,6 @@ static int validate(const char **id, const char *env, void **priv_data) { XCloseDisplay(dpy); } } - - unsetenv("XAUTHORITY"); return ret; } diff --git a/src/modules/keyboard.c b/src/modules/keyboard.c index c5816da..3e25fdf 100644 --- a/src/modules/keyboard.c +++ b/src/modules/keyboard.c @@ -72,7 +72,7 @@ static bool evaluate(void) { } static void init(void) { - kbds = map_new(true, dtor_kbd); + kbds = map_new(false, dtor_kbd); sd_bus_add_object_manager(bus, NULL, object_path); int r = sd_bus_add_object_vtable(bus, NULL, @@ -141,6 +141,7 @@ static void destroy(void) { static void dtor_kbd(void *data) { kbd_t *k = (kbd_t *)data; sd_bus_slot_unref(k->slot); + free((void *)k->sysname); free(k); } diff --git a/src/modules/screen.c b/src/modules/screen.c index f3c0d87..b69f44f 100644 --- a/src/modules/screen.c +++ b/src/modules/screen.c @@ -1,6 +1,7 @@ #ifdef SCREEN_PRESENT #include "screen.h" +#include "bus_utils.h" #define MONITOR_ILL_MAX 255 @@ -117,6 +118,8 @@ static int method_getbrightness(sd_bus_message* m, void* userdata, sd_bus_error* return r; } + bus_sender_fill_creds(m); + screen_plugin *plugin = userdata; int br = WRONG_PLUGIN; if (!plugin) { diff --git a/src/modules/screen_plugins/fb.c b/src/modules/screen_plugins/fb.c index c5d7d5d..24c6fff 100644 --- a/src/modules/screen_plugins/fb.c +++ b/src/modules/screen_plugins/fb.c @@ -15,7 +15,7 @@ static int get_frame_brightness(const char *id, const char *env) { int ret = WRONG_PLUGIN; struct udev_device *dev = NULL; - if (!id || !strlen(id)) { + if (id == NULL || id[0] == '\0') { /* Fetch first matching device from udev */ get_udev_device(NULL, FB_SUBSYSTEM, NULL, NULL, &dev); if (!dev) { diff --git a/src/modules/screen_plugins/xorg.c b/src/modules/screen_plugins/xorg.c index 21d1dea..8c8ad7b 100644 --- a/src/modules/screen_plugins/xorg.c +++ b/src/modules/screen_plugins/xorg.c @@ -1,20 +1,13 @@ #include "screen.h" +#include "bus_utils.h" +#include "xorg_utils.h" #include -static int getRootBrightness(const char *screen_name); - SCREEN("Xorg"); -static int get_frame_brightness(const char *id, const char *env) { - setenv("XAUTHORITY", env, 1); - const int br = getRootBrightness(id); - unsetenv("XAUTHORITY"); - return br; -} - /* Robbed from calise source code, thanks!! */ -static int getRootBrightness(const char *screen_name) { - Display *dpy = XOpenDisplay(screen_name); +static int get_frame_brightness(const char *id, const char *env) { + Display *dpy = fetch_xorg_display(&id, env); if (!dpy) { return WRONG_PLUGIN; } diff --git a/src/modules/sensor.c b/src/modules/sensor.c index b920a23..bd07e28 100644 --- a/src/modules/sensor.c +++ b/src/modules/sensor.c @@ -1,5 +1,6 @@ #include #include +#include "bus_utils.h" #define SENSOR_MAX_CAPTURES 20 @@ -44,14 +45,20 @@ static void init(void) { NULL); for (int i = 0; i < SENSOR_NUM && !r; i++) { if (sensors[i]) { - snprintf(sensors[i]->obj_path, sizeof(sensors[i]->obj_path) - 1, "%s/%s", object_path, sensors[i]->name); - r += sd_bus_add_object_vtable(bus, - NULL, - sensors[i]->obj_path, - bus_interface, - vtable, - sensors[i]); - r += m_register_fd(sensors[i]->init_monitor(), false, sensors[i]); + int fd = sensors[i]->init_monitor(); + if (fd != -1) { + snprintf(sensors[i]->obj_path, sizeof(sensors[i]->obj_path) - 1, "%s/%s", object_path, sensors[i]->name); + r += sd_bus_add_object_vtable(bus, + NULL, + sensors[i]->obj_path, + bus_interface, + vtable, + sensors[i]); + r += m_register_fd(fd, false, sensors[i]); + } else { + fprintf(stderr, "Sensor '%s' unsupported.\n", sensors[i]->name); + sensors[i] = NULL; + } } } if (r < 0) { @@ -90,7 +97,7 @@ static void destroy(void) { void sensor_register_new(sensor_t *sensor) { const char *sensor_names[] = { - #define X(name, val) #name, + #define X(name) #name, _SENSORS #undef X }; @@ -180,7 +187,7 @@ static int method_issensoravailable(sd_bus_message *m, void *userdata, sd_bus_er static int method_capturesensor(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { ASSERT_AUTH(); - + const char *interface = NULL; char *settings = NULL; const int num_captures; @@ -195,6 +202,8 @@ static int method_capturesensor(sd_bus_message *m, void *userdata, sd_bus_error return -EINVAL; } + bus_sender_fill_creds(m); // used by PW plugin + void *dev = NULL; sensor_t *sensor = NULL; double *pct = calloc(num_captures, sizeof(double)); diff --git a/src/modules/sensor.h b/src/modules/sensor.h index 9ad2b14..77e97cb 100644 --- a/src/modules/sensor.h +++ b/src/modules/sensor.h @@ -6,7 +6,7 @@ * - a bunch of callbacks: * * -> validate_dev() called when retrieving a device from a monitor to validate retrieved device. - * -> fetch_dev() called by is_sensor_available(), to fetch a device (with requqested interface, if set) + * -> fetch_dev() called by is_sensor_available(), to fetch a device (with requested interface, if set) * -> fetch_props_dev() to retrieve a device properties, ie: its node and, if (action), an associated action * -> destroy_dev() to free dev resources * @@ -24,13 +24,14 @@ /* Sensor->name must match its enumeration stringified value */ #define _SENSORS \ - X(ALS, 0) \ - X(YOCTOLIGHT, 1) \ - X(CAMERA, 2) \ - X(CUSTOM, 3) + X(ALS) \ + X(YOCTOLIGHT) \ + X(PIPEWIRE) \ + X(CAMERA) \ + X(CUSTOM) enum sensors { -#define X(name, val) name = val, +#define X(name) name, _SENSORS #undef X SENSOR_NUM diff --git a/src/modules/sensors/als.h b/src/modules/sensors/als.h index 33df5fa..1a3dc40 100644 --- a/src/modules/sensors/als.h +++ b/src/modules/sensors/als.h @@ -25,7 +25,7 @@ static inline void parse_settings(char *settings, int *interval) { /* Default value */ *interval = ALS_INTERVAL; - if (settings && strlen(settings)) { + if (settings && settings[0] != '\0') { char *token; char *rest = settings; diff --git a/src/modules/sensors/camera.c b/src/modules/sensors/camera.c index 41ba899..2a64384 100644 --- a/src/modules/sensors/camera.c +++ b/src/modules/sensors/camera.c @@ -1,73 +1,33 @@ #include -#include #include #include #include -#include -#include +#include "camera.h" #include -#include #define CAMERA_NAME "Camera" -#define CAMERA_ILL_MAX 255 #define CAMERA_SUBSYSTEM "video4linux" -#define HISTOGRAM_STEPS 40 - -#define SET_V4L2(id, val) set_v4l2_control(id, val, #id, true) -#define SET_V4L2_DEF(id) set_v4l2_control_def(id, #id) -#ifndef NDEBUG - #define INFO(fmt, ...) printf(fmt, ##__VA_ARGS__); -#else - #define INFO(fmt, ...) -#endif struct buffer { uint8_t *start; size_t length; }; -struct histogram { - double count; - double sum; -}; - struct mjpeg_dec { struct jpeg_decompress_struct cinfo; struct jpeg_error_mgr err; int (*dec_cb)(uint8_t **frame, int len); }; -typedef enum { X_AXIS, Y_AXIS, MAX_AXIS } crop_axis; -typedef enum { DISABLED, CROP_API, SELECTION_API, MANUAL} crop_type_t; - -typedef struct { - bool enabled; - double area_pct[2]; // start - end -} crop_info_t; - struct state { int device_fd; uint32_t pixelformat; - uint32_t width; - uint32_t height; - crop_info_t crop[MAX_AXIS]; - crop_type_t crop_type; + uint32_t width; // real width, can be cropped + uint32_t height; // real height, can be cropped struct buffer buf; - struct histogram hist[HISTOGRAM_STEPS]; - char *settings; struct mjpeg_dec *decoder; - map_t *stored_values; }; -static void set_v4l2_control_def(uint32_t id, const char *name); -static void set_v4l2_control(uint32_t id, int32_t val, const char *name, bool store); -static void set_camera_settings_def(void); -static void inline fill_crop_rect(crop_info_t *cr, struct v4l2_rect *rect); -static int set_selection(crop_info_t *cr); -static int set_crop(crop_info_t *cr); -static int try_set_crop(crop_info_t *crop); -static void set_camera_settings(void); -static void restore_camera_settings(void); static int set_camera_fmt(void); static int check_camera_caps(void); static void create_decoder(void); @@ -120,7 +80,6 @@ static void destroy_dev(void *dev) { if (state.device_fd >= 0) { close(state.device_fd); } - map_free(state.stored_values); /* reset state */ memset(&state, 0, sizeof(struct state)); state.device_fd = -1; @@ -139,237 +98,78 @@ static void destroy_monitor(void) { } static int capture(void *dev, double *pct, const int num_captures, char *settings) { - state.settings = settings; - state.stored_values = map_new(true, free); int ctr = 0; if (set_camera_fmt() == 0 && init_mmap() == 0 && start_stream() == 0) { - set_camera_settings(); + set_camera_settings(&state, settings); create_decoder(); for (int i = 0; i < num_captures; i++) { struct v4l2_buffer buf = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, .memory = V4L2_MEMORY_MMAP}; - memset(state.hist, 0, HISTOGRAM_STEPS * sizeof(struct histogram)); - if (send_frame(&buf) == 0 && recv_frame(&buf) == 0) { - pct[ctr++] = compute_brightness(buf.bytesused) / CAMERA_ILL_MAX; + pct[ctr++] = compute_brightness(buf.bytesused); } } destroy_decoder(); stop_stream(); - restore_camera_settings(); + restore_camera_settings(&state); } destroy_mmap(); return ctr; } -static void set_v4l2_control_def(uint32_t id, const char *name) { - struct v4l2_queryctrl arg = {0}; - arg.id = id; - if (-1 == xioctl(VIDIOC_QUERYCTRL, &arg)) { - INFO("%s unsupported\n", name); - } else { - INFO("%s (%u) default val: %d\n", name, id, arg.default_value); - set_v4l2_control(id, arg.default_value, name, true); - } -} - -static void set_v4l2_control(uint32_t id, int32_t val, const char *name, bool store) { +static struct v4l2_control *set_camera_setting(void *priv, uint32_t id, float val, const char *name, bool store) { struct v4l2_control old_ctrl = {0}; old_ctrl.id = id; + int32_t v = (int32_t)val; + /* Store initial value, if set. */ if (-1 == xioctl(VIDIOC_G_CTRL, &old_ctrl)) { INFO("'%s' unsupported\n", name); - return; + return NULL; } - if (old_ctrl.value != val) { + if (v < 0) { + /* Set default value */ + struct v4l2_queryctrl arg = {0}; + arg.id = id; + xioctl(VIDIOC_QUERYCTRL, &arg); + INFO("%s (%u) default val: %d\n", name, id, arg.default_value); + v = arg.default_value; + } + + if (old_ctrl.value != v) { struct v4l2_control ctrl ={0}; ctrl.id = id; - ctrl.value = val; + ctrl.value = v; if (-1 == xioctl(VIDIOC_S_CTRL, &ctrl)) { INFO("%s unsupported\n", name); } else { - INFO("Set '%s' val: %d\n", name, val); + INFO("Set '%s' val: %d\n", name, v); if (store) { struct v4l2_control *store_ctrl = calloc(1, sizeof(struct v4l2_control)); if (store_ctrl) { memcpy(store_ctrl, &old_ctrl, sizeof(struct v4l2_control)); - INFO("Storing initial setting for '%s': %d\n", name, val); - map_put(state.stored_values, name, (void *)store_ctrl); + INFO("Storing initial setting for '%s': %d\n", name, v); + return store_ctrl; } else { INFO("failed to store initial setting for '%s'\n", name) } } } } else { - INFO("Value %d for '%s' already set.\n", val, name); + INFO("Value %d for '%s' already set.\n", v, name); } -} - -/* Properly set everything to default value */ -static void set_camera_settings_def(void) { - SET_V4L2_DEF(V4L2_CID_SCENE_MODE); - SET_V4L2_DEF(V4L2_CID_AUTO_WHITE_BALANCE); - SET_V4L2_DEF(V4L2_CID_EXPOSURE_AUTO); - SET_V4L2_DEF(V4L2_CID_AUTOGAIN); - SET_V4L2_DEF(V4L2_CID_ISO_SENSITIVITY_AUTO); - SET_V4L2_DEF(V4L2_CID_BACKLIGHT_COMPENSATION); - SET_V4L2_DEF(V4L2_CID_AUTOBRIGHTNESS); - - SET_V4L2_DEF(V4L2_CID_WHITE_BALANCE_TEMPERATURE); - SET_V4L2_DEF(V4L2_CID_EXPOSURE_ABSOLUTE); - SET_V4L2_DEF(V4L2_CID_IRIS_ABSOLUTE); - SET_V4L2_DEF(V4L2_CID_GAIN); - SET_V4L2_DEF(V4L2_CID_ISO_SENSITIVITY); - SET_V4L2_DEF(V4L2_CID_BRIGHTNESS); + return NULL; } static void inline fill_crop_rect(crop_info_t *cr, struct v4l2_rect *rect) { - if (state.crop[Y_AXIS].enabled) { - const double *a_pct = cr[Y_AXIS].area_pct; - const double height_pct = a_pct[1] - a_pct[0]; - rect->height = height_pct * state.height; - rect->top = a_pct[0] * state.height; - } - if (state.crop[X_AXIS].enabled) { - const double *a_pct = cr[X_AXIS].area_pct; - const double width_pct = a_pct[1] - a_pct[0]; - rect->width = width_pct * state.width; - rect->left = a_pct[0] * state.width; - } -} - -// https://www.kernel.org/doc/html/v4.12/media/uapi/v4l/vidioc-g-selection.html -static int set_selection(crop_info_t *cr) { - struct v4l2_selection selection = {0}; - selection.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - selection.target = V4L2_SEL_TGT_CROP; - if (-1 == xioctl(VIDIOC_G_SELECTION, &selection)) { - INFO("VIDIOC_G_SELECTION failed: %m\n"); - return -errno; - } - if (cr) { - fill_crop_rect(cr, &selection.r); - } else { - // Reset default - selection.target = V4L2_SEL_TGT_CROP_DEFAULT; - } - if (-1 == xioctl(VIDIOC_S_SELECTION, &selection)) { - INFO("VIDIOC_S_SELECTION failed: %m\n"); - return -errno; - } - state.crop_type = SELECTION_API; - return 0; -} - -// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/crop.html -static int set_crop(crop_info_t *cr) { - struct v4l2_crop crop = {0}; - crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - if (-1 == xioctl(VIDIOC_G_CROP, &crop)) { - INFO("VIDIOC_G_CROP failed: %m\n"); - return -errno; - } - - if (cr) { - fill_crop_rect(cr, &crop.c); - } else { - // Reset default - struct v4l2_cropcap cropcap = {0}; - cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - if (-1 == xioctl(VIDIOC_CROPCAP, &cropcap)) { - INFO("VIDIOC_CROPCAP failed: %m\n"); - return -errno; - } - crop.c = cropcap.defrect; - } - if (-1 == xioctl(VIDIOC_S_CROP, &crop)) { - INFO("VIDIOC_S_CROP failed: %m\n"); - return -errno; - } - state.crop_type = CROP_API; - return 0; -} - -static int try_set_crop(crop_info_t *crop) { - /** Try "new" selection API **/ - if (set_selection(crop) != 0) { - /** Try "old" crop API **/ - return set_crop(crop); - } - return 0; -} + const double height_pct = cr[Y_AXIS].area_pct[1] - cr[Y_AXIS].area_pct[0]; + rect->height = height_pct * state.height; + rect->top = cr[Y_AXIS].area_pct[0] * state.height; -/* Parse settings string! */ -static void set_camera_settings(void) { - /* Set default values */ - set_camera_settings_def(); - if (state.settings && strlen(state.settings)) { - char *token; - char *rest = state.settings; - - while ((token = strtok_r(rest, ",", &rest))) { - uint32_t v4l2_op; - int32_t v4l2_val; - char axis; - double area_pct[2]; - - if (sscanf(token, "%u=%d", &v4l2_op, &v4l2_val) == 2) { - SET_V4L2(v4l2_op, v4l2_val); - } else if (sscanf(token, "%c=%lf-%lf", &axis, &area_pct[0], &area_pct[1]) == 3) { - int8_t crop_idx = -1; - if (area_pct[0] >= area_pct[1]) { - fprintf(stderr, "Start should be lesser than end: %lf-%lf\n", area_pct[0], area_pct[1]); - } else { - switch (axis) { - case 'x': - crop_idx = X_AXIS; - break; - case 'y': - crop_idx = Y_AXIS; - break; - default: - fprintf(stderr, "wrong axis specified: %c; 'x' or 'y' supported.\n", axis); - break; - } - } - if (crop_idx != -1 && !state.crop[crop_idx].enabled) { - state.crop[crop_idx].enabled = true; - state.crop[crop_idx].area_pct[0] = area_pct[0]; - state.crop[crop_idx].area_pct[1] = area_pct[1]; - } - } else { - fprintf(stderr, "Expected a=b format in '%s' token.\n", token); - } - } - if (state.crop[X_AXIS].enabled || state.crop[Y_AXIS].enabled) { - if (try_set_crop(state.crop) != 0) { - INFO("Unsupported crop/selection v4l2 API; fallback at manually skipping pixels.\n") - state.crop_type = MANUAL; - } - } - } -} - -static void restore_camera_settings(void) { - for (map_itr_t *itr = map_itr_new(state.stored_values); itr; itr = map_itr_next(itr)) { - struct v4l2_control *old_ctrl = map_itr_get_data(itr); - const char *ctrl_name = map_itr_get_key(itr); - INFO("Restoring setting for '%s'\n", ctrl_name) - set_v4l2_control(old_ctrl->id, old_ctrl->value, ctrl_name, false); - } - - // Restore crop if needed - switch (state.crop_type) { - case SELECTION_API: - set_selection(NULL); - break; - case CROP_API: - set_crop(NULL); - break; - default: - break; - } + const double width_pct = cr[X_AXIS].area_pct[1] - cr[X_AXIS].area_pct[0]; + rect->width = width_pct * state.width; + rect->left = cr[X_AXIS].area_pct[0] * state.width; } static int set_camera_fmt(void) { @@ -559,7 +359,7 @@ static int send_frame(struct v4l2_buffer *buf) { return 0; } -static int recv_frame(struct v4l2_buffer *buf) { +static int recv_frame(struct v4l2_buffer *buf) { /* Dequeue the buffer */ if(-1 == xioctl(VIDIOC_DQBUF, buf)) { perror("VIDIOC_DQBUF"); @@ -570,9 +370,6 @@ static int recv_frame(struct v4l2_buffer *buf) { static double compute_brightness(unsigned int size) { double brightness = 0.0; - double min = CAMERA_ILL_MAX; - double max = 0.0; - uint8_t *img_data = state.buf.start; if (state.decoder) { size = state.decoder->dec_cb(&img_data, size); @@ -581,107 +378,13 @@ static double compute_brightness(unsigned int size) { } } - /* - * If greyscale -> increment by 1. - * If YUYV -> increment by 2: we only want Y! - */ - const int inc = 1 + (state.pixelformat == V4L2_PIX_FMT_YUYV); - - // Manual crop if needed - int col_start = 0; - int col_end = state.width; - int row_start = 0; - int row_end = state.height; - if (state.crop_type == MANUAL) { - if (state.crop[X_AXIS].enabled) { - col_start = state.crop[X_AXIS].area_pct[0] * state.width; - col_end = state.crop[X_AXIS].area_pct[1] * state.width; - } - if (state.crop[Y_AXIS].enabled) { - row_start = state.crop[Y_AXIS].area_pct[0] * state.height; - row_end = state.crop[Y_AXIS].area_pct[1] * state.height; - } - INFO("Manual crop: rows[%d-%d], cols[%d-%d]\n", row_start, row_end, col_start, col_end); - } - - /* Find minimum and maximum brightness */ - int total = 0; // compute total used pixels - for (int row = row_start; row < row_end; row++) { - for (int col = col_start; col < col_end * inc; col += inc) { - const int idx = (row * state.width * inc) + col; - if (img_data[idx] < min) { - min = img_data[idx]; - } - if (img_data[idx] > max) { - max = img_data[idx]; - } - total++; - } - } - INFO("Total computed pixels: %d\n", total); - - /* Ok, we should never get in here */ - if (max == 0.0) { - goto end; - } - - /* Calculate histogram */ - const double step_size = (max - min) / HISTOGRAM_STEPS; - for (int row = row_start; row < row_end; row++) { - for (int col = col_start; col < col_end * inc; col += inc) { - const int idx = (row * state.width * inc) + col; - int bucket = (img_data[idx] - min) / step_size; - if (bucket >= 0 && bucket < HISTOGRAM_STEPS) { - state.hist[bucket].sum += img_data[idx]; - state.hist[bucket].count++; - } - } - } - - /* Find (approximate) quartiles for histogram steps */ - const double quartile_size = (double)total / 4; - double quartiles[3] = {0}; - int j = 0; - for (int i = 0; i < HISTOGRAM_STEPS && j < 3; i++) { - quartiles[j] += state.hist[i].count; - if (quartiles[j] >= quartile_size) { - quartiles[j] = (quartile_size / quartiles[j]) + i; - j++; - } - } - - /* - * Results may be clustered in a single estimated quartile, - * in which case consider full range. - */ - int min_bucket = 0; - int max_bucket = HISTOGRAM_STEPS - 1; - if (quartiles[2] > quartiles[0]) { - /* Trim outlier buckets via interquartile range */ - const double iqr = (quartiles[2] - quartiles[0]) * 1.5; - min_bucket = quartiles[0] - iqr; - max_bucket = quartiles[2] + iqr; - if (min_bucket < 0) { - min_bucket = 0; - } - if (max_bucket > HISTOGRAM_STEPS - 1) { - max_bucket = HISTOGRAM_STEPS - 1; - } - } - - /* - * Find the highest brightness bucket with - * a significant sample count - * and return the average brightness for it. - */ - for (int i = max_bucket; i >= min_bucket; i--) { - if (state.hist[i].count > step_size) { - brightness = state.hist[i].sum / state.hist[i].count; - break; - } - } - -end: + rect_info_t full = { + .row_start = 0, + .row_end = state.height, + .col_start = 0, + .col_end = state.width, + }; + brightness = get_frame_brightness(img_data, &full, (state.pixelformat == V4L2_PIX_FMT_YUYV)); if (img_data != state.buf.start) { free(img_data); } diff --git a/src/modules/sensors/camera.h b/src/modules/sensors/camera.h new file mode 100644 index 0000000..b99cf02 --- /dev/null +++ b/src/modules/sensors/camera.h @@ -0,0 +1,239 @@ +#pragma once + +#include +#include +#include +#include + +#ifndef NDEBUG + #define INFO(fmt, ...) printf(fmt, ##__VA_ARGS__); +#else + #define INFO(fmt, ...) +#endif + +#define CAMERA_ILL_MAX 255 +#define HISTOGRAM_STEPS 40 + +#define SET_V4L2(op, val) \ +do { \ + struct v4l2_control *data = set_camera_setting(priv, op, val, #op, true); \ + if (data) { \ + map_put(stored_values, #op, data); \ + } \ +} while(false) + +typedef struct { + int row_start; + int row_end; + int col_start; + int col_end; +} rect_info_t; + +struct histogram { + double count; + double sum; +}; + +typedef enum { X_AXIS, Y_AXIS, MAX_AXIS } crop_axis; + +typedef struct { + bool enabled; + double area_pct[2]; // start - end +} crop_info_t; + +static struct v4l2_control *set_camera_setting(void *priv, uint32_t op, float val, const char *op_name, bool store); + +static map_t *stored_values; +static crop_info_t crop[MAX_AXIS]; +static bool camera_set; + +static double get_frame_brightness(uint8_t *img_data, rect_info_t *full, bool is_yuv) { + double brightness = 0.0; + double min = CAMERA_ILL_MAX; + double max = 0.0; + + /* + * If greyscale (rgb is converted to grey) -> increment by 1. + * If YUYV -> increment by 2: we only want Y! + */ + const int inc = 1 + is_yuv; + + rect_info_t crop_rect = *full; + if (crop[X_AXIS].enabled) { + crop_rect.col_start = crop[X_AXIS].area_pct[0] * full->col_end; + crop_rect.col_end = crop[X_AXIS].area_pct[1] * full->col_end; + } + if (crop[Y_AXIS].enabled) { + crop_rect.row_start = crop[Y_AXIS].area_pct[0] * full->row_end; + crop_rect.row_end = crop[Y_AXIS].area_pct[1] * full->row_end; + } + INFO("Rect: rows[%d-%d], cols[%d-%d]\n", crop_rect.row_start, crop_rect.row_end, + crop_rect.col_start, crop_rect.col_end); + + /* Find minimum and maximum brightness */ + int total = 0; // compute total used pixels + for (int row = crop_rect.row_start; row < crop_rect.row_end; row++) { + for (int col = crop_rect.col_start * inc; col < crop_rect.col_end * inc; col += inc) { + const int idx = (row * full->col_end * inc) + col; + if (img_data[idx] < min) { + min = img_data[idx]; + } + if (img_data[idx] > max) { + max = img_data[idx]; + } + total++; + } + } + INFO("Total computed pixels: %d\n", total); + + /* Ok, we should never get in here */ + if (max == 0.0) { + return brightness; + } + + /* Calculate histogram */ + struct histogram hist[HISTOGRAM_STEPS] = {0}; + const double step_size = (max - min) / HISTOGRAM_STEPS; + for (int row = crop_rect.row_start; row < crop_rect.row_end; row++) { + for (int col = crop_rect.col_start * inc; col < crop_rect.col_end * inc; col += inc) { + const int idx = (row * full->col_end * inc) + col; + int bucket = (img_data[idx] - min) / step_size; + if (bucket >= 0 && bucket < HISTOGRAM_STEPS) { + hist[bucket].sum += img_data[idx]; + hist[bucket].count++; + } + } + } + + /* Find (approximate) quartiles for histogram steps */ + const double quartile_size = (double)total / 4; + double quartiles[3] = {0}; + int j = 0; + for (int i = 0; i < HISTOGRAM_STEPS && j < 3; i++) { + quartiles[j] += hist[i].count; + if (quartiles[j] >= quartile_size) { + quartiles[j] = (quartile_size / quartiles[j]) + i; + j++; + } + } + + /* + * Results may be clustered in a single estimated quartile, + * in which case consider full range. + */ + int min_bucket = 0; + int max_bucket = HISTOGRAM_STEPS - 1; + if (quartiles[2] > quartiles[0]) { + /* Trim outlier buckets via interquartile range */ + const double iqr = (quartiles[2] - quartiles[0]) * 1.5; + min_bucket = quartiles[0] - iqr; + max_bucket = quartiles[2] + iqr; + if (min_bucket < 0) { + min_bucket = 0; + } + if (max_bucket > HISTOGRAM_STEPS - 1) { + max_bucket = HISTOGRAM_STEPS - 1; + } + } + + /* + * Find the highest brightness bucket with + * a significant sample count + * and return the average brightness for it. + */ + for (int i = max_bucket; i >= min_bucket; i--) { + if (hist[i].count > step_size) { + brightness = hist[i].sum / hist[i].count; + break; + } + } + return (double)brightness / CAMERA_ILL_MAX; +} + +static void set_camera_settings_def(void *priv) { + SET_V4L2(V4L2_CID_SCENE_MODE, -1); + SET_V4L2(V4L2_CID_AUTO_WHITE_BALANCE, -1); + SET_V4L2(V4L2_CID_EXPOSURE_AUTO, -1); + SET_V4L2(V4L2_CID_AUTOGAIN, -1); + SET_V4L2(V4L2_CID_ISO_SENSITIVITY_AUTO, -1); + SET_V4L2(V4L2_CID_BACKLIGHT_COMPENSATION, -1); + SET_V4L2(V4L2_CID_AUTOBRIGHTNESS, -1); + + SET_V4L2(V4L2_CID_WHITE_BALANCE_TEMPERATURE, -1); + SET_V4L2(V4L2_CID_EXPOSURE_ABSOLUTE, -1); + SET_V4L2(V4L2_CID_IRIS_ABSOLUTE, -1); + SET_V4L2(V4L2_CID_GAIN, -1); + SET_V4L2(V4L2_CID_ISO_SENSITIVITY, -1); + SET_V4L2(V4L2_CID_BRIGHTNESS, -1); +} + +static void set_camera_settings(void *priv, char *settings) { + // Already called; expect a restore_camera_settings() call + if (camera_set) { + return; + } + + /* Default values: until end of axis */ + crop[X_AXIS].area_pct[1] = 1.0; + crop[Y_AXIS].area_pct[1] = 1.0; + + stored_values = map_new(true, free); + + /* Set default values */ + set_camera_settings_def(priv); + if (settings && settings[0] != '\0') { + char *token; + char *rest = settings; + + while ((token = strtok_r(rest, ",", &rest))) { + uint32_t v4l2_op; + int32_t v4l2_val; + char axis; + double area_pct[2]; + + if (sscanf(token, "%u=%d", &v4l2_op, &v4l2_val) == 2) { + SET_V4L2(v4l2_op, v4l2_val); + } else if (sscanf(token, "%c=%lf-%lf", &axis, &area_pct[0], &area_pct[1]) == 3) { + int8_t crop_idx = -1; + if (area_pct[0] >= area_pct[1]) { + fprintf(stderr, "Start should be lesser than end: %lf-%lf\n", area_pct[0], area_pct[1]); + } else { + switch (axis) { + case 'x': + crop_idx = X_AXIS; + break; + case 'y': + crop_idx = Y_AXIS; + break; + default: + fprintf(stderr, "wrong axis specified: %c; 'x' or 'y' supported.\n", axis); + break; + } + } + if (crop_idx != -1 && !crop[crop_idx].enabled) { + crop[crop_idx].enabled = true; + crop[crop_idx].area_pct[0] = area_pct[0]; + crop[crop_idx].area_pct[1] = area_pct[1]; + } + } else { + fprintf(stderr, "Expected a=b format in '%s' token.\n", token); + } + } + } + camera_set = true; +} + +static void restore_camera_settings(void *priv) { + for (map_itr_t *itr = map_itr_new(stored_values); itr; itr = map_itr_next(itr)) { + struct v4l2_control *old_ctrl = map_itr_get_data(itr); + const char *ctrl_name = map_itr_get_key(itr); + INFO("Restoring setting for '%s'\n", ctrl_name) + set_camera_setting(priv, old_ctrl->id, old_ctrl->value, ctrl_name, false); + } + + map_free(stored_values); + stored_values = NULL; + + memset(crop, 0, sizeof(crop)); + camera_set = false; +} diff --git a/src/modules/sensors/custom.c b/src/modules/sensors/custom.c index c45d53c..dab17ca 100644 --- a/src/modules/sensors/custom.c +++ b/src/modules/sensors/custom.c @@ -22,7 +22,7 @@ static bool validate_dev(void *dev) { static void fetch_dev(const char *interface, void **dev) { *dev = NULL; - if (interface && strlen(interface) > 0) { + if (interface && interface[0] != '\0') { char fullpath[PATH_MAX + 1] = {0}; /* User gave us a relative path! prepend CUSTOM_FLD as prefix! */ @@ -104,7 +104,7 @@ static void parse_settings(char *settings, int *min, int *max, int *interval) { *max = CUSTOM_ILL_MAX; *interval = CUSTOM_INTERVAL; - if (settings && strlen(settings)) { + if (settings && settings[0] != '\0') { char *token; char *rest = settings; diff --git a/src/modules/sensors/pipewire.c b/src/modules/sensors/pipewire.c new file mode 100644 index 0000000..3d691c7 --- /dev/null +++ b/src/modules/sensors/pipewire.c @@ -0,0 +1,677 @@ +#ifdef PIPEWIRE_PRESENT + +#warning "Experimental support. Camera settings are not supported. Possible freezes." + +#include "camera.h" +#include "bus_utils.h" +#include +#include +#include +#include +#include +#include + +#define EVENT_SIZE ( sizeof (struct inotify_event) ) +#define EVENT_BUF_LEN ( 1024 * ( EVENT_SIZE + 16 ) ) + +#define PW_NAME "Pipewire" +#define PW_ENV_NAME "CLIGHTD_PIPEWIRE_RUNTIME_DIR" +#define BUILD_KEY(id) \ + char key[10]; \ + snprintf(key, sizeof(key), "%d", id); + +typedef struct { + uint32_t id; + struct spa_hook proxy_listener; + struct spa_hook object_listener; + struct pw_proxy *proxy; + const char *action; +} pw_node_t; + +typedef struct { + double *pct; + int capture_idx; + bool with_err; + char *settings; +} capture_settings_t; + +typedef struct { + pw_node_t node; + struct pw_loop *loop; + struct pw_stream *stream; + struct spa_source *timer; + struct spa_video_info format; + capture_settings_t cap_set; +} pw_data_t; + +typedef struct { + struct pw_loop *loop; + struct pw_context *context; + struct pw_core *core; + struct pw_registry *registry; + struct spa_hook registry_listener; +} pw_mon_t; + +static void free_node(void *dev); +static void build_format(struct spa_pod_builder *b, const struct spa_pod **params); +static uint32_t control_to_prop_id(uint32_t control_id); +static int register_monitor_fd(const char *pw_runtime_dir); + +SENSOR(PW_NAME); + +MODULE(PW_NAME); + +static pw_mon_t pw_mon; +static pw_data_t *last_recved; +static map_t *nodes; +static int efd = -1; +static bool pw_inited; + +static void module_pre_start(void) { + +} + +static bool check(void) { + return true; +} + +static bool evaluate(void) { + return true; +} + +static void init(void) { + nodes = map_new(true, free_node); +} + +static void receive(const msg_t *msg, const void *userdata) { + if (!msg->is_pubsub) { + if (msg->fd_msg->userptr != NULL) { + // Pipewire monitor event! Notify Sensor + uint64_t u = 1; + write(efd, &u, sizeof(uint64_t)); + } else { + // Inotify event! + char buffer[EVENT_BUF_LEN]; + size_t len = read(msg->fd_msg->fd, buffer, EVENT_BUF_LEN); + int i = 0; + while (i < len) { + struct inotify_event *event = (struct inotify_event *)&buffer[i]; + if (event->len && strcmp(event->name, "pipewire-0") == 0) { + if (register_monitor_fd(getenv(PW_ENV_NAME)) != 0) { + fprintf(stderr, "Failed to start pipewire monitor.\n"); + } else { + // We're done with the inotify fd; close it. + m_deregister_fd(msg->fd_msg->fd); + break; + } + } + i += EVENT_SIZE + event->len; + } + } + } +} + +static void destroy(void) { + map_free(nodes); +} + +static void free_node(void *dev) { + pw_data_t *pw = (pw_data_t *)dev; + pw->node.action = NULL; // see destroy_dev() impl + destroy_dev(pw); + pw_proxy_destroy(pw->node.proxy); + free(pw); +} + +static void on_state_changed(void *_data, enum pw_stream_state old, + enum pw_stream_state state, const char *error) { + pw_data_t *pw = _data; + if (state == PW_STREAM_STATE_STREAMING) { + /* Camera entered streaming mode; set settings. TODO: unsupported atm */ +// set_camera_settings(pw, pw->cap_set.settings); + } else if (state == PW_STREAM_STATE_ERROR) { + pw->cap_set.with_err = true; + } +} + +static void on_process(void *_data) { + pw_data_t *pw = _data; + + struct pw_buffer *b = pw_stream_dequeue_buffer(pw->stream); + if (b == NULL) { + fprintf(stderr, "out of buffers: %m"); + goto err; + } + + const bool is_yuv = pw->format.info.raw.format == SPA_VIDEO_FORMAT_YUY2; + struct spa_buffer *buf = b->buffer; + uint8_t *sdata = buf->datas[0].data; + if (sdata == NULL) { + goto err; + } + + rect_info_t full = { + .row_start = 0, + .row_end = pw->format.info.raw.size.height, + .col_start = 0, + .col_end = pw->format.info.raw.size.width, + }; + pw->cap_set.pct[pw->cap_set.capture_idx++] = get_frame_brightness(sdata, &full, is_yuv); + pw_stream_queue_buffer(pw->stream, b); + return; + +err: + pw->cap_set.with_err = true; +} + +static void on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) { + pw_data_t *pw = _data; + + if (param == NULL || id != SPA_PARAM_Format) { + return; + } + + if (spa_format_parse(param, + &pw->format.media_type, + &pw->format.media_subtype) < 0) { + + pw->cap_set.with_err = true; + return; + } + + if (pw->format.media_type != SPA_MEDIA_TYPE_video || + pw->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) { + + pw->cap_set.with_err = true; + return; + } + + if (spa_format_video_raw_parse(param, &pw->format.info.raw) < 0) { + pw->cap_set.with_err = true; + return; + } + + uint8_t params_buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); + const struct spa_pod *params = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<stream, ¶ms, 1); + + INFO("Image fmt: %d\n", pw->format.info.raw.format); + INFO("Image res: %d x %d\n", pw->format.info.raw.size.width, pw->format.info.raw.size.height); +} + +/* these are the stream events we listen for */ +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .state_changed = on_state_changed, + .param_changed = on_stream_param_changed, + .process = on_process, +}; + +static void build_format(struct spa_pod_builder *b, const struct spa_pod **params) { + *params = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(2, + SPA_VIDEO_FORMAT_GRAY8, // V4L2_PIX_FMT_GREY + SPA_VIDEO_FORMAT_YUY2), // V4L2_PIX_FMT_YUYV + SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( + &SPA_RECTANGLE(160, 120), + &SPA_RECTANGLE(1, 1), + &SPA_RECTANGLE(640, 480)), + SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction( + &SPA_FRACTION(25, 1), + &SPA_FRACTION(0, 1), + &SPA_FRACTION(120, 1))); +} + +static bool validate_dev(void *dev) { + return true; +} + +static void fetch_dev(const char *interface, void **dev) { + pw_data_t *pw = NULL; + if (interface && interface[0] != '\0') { + pw = map_get(nodes, interface); + } else { + // Peek first pw node found + map_itr_t *itr = map_itr_new(nodes); + if (itr) { + pw = map_itr_get_data(itr); + free(itr); + } + } + *dev = pw; +} + +static void fetch_props_dev(void *dev, const char **node, const char **action) { + static char str_id[32]; + pw_data_t *pw = (pw_data_t *)dev; + sprintf(str_id, "%d", pw->node.id); + *node = str_id; + if (action) { + *action = pw->node.action; + } +} + +static void destroy_dev(void *dev) { + pw_data_t *pw = (pw_data_t *)dev; + if (pw->timer) { + pw_loop_destroy_source(pw->loop, pw->timer); + } + if (pw->stream) { + pw_stream_destroy(pw->stream); + pw->stream = NULL; + } + if (pw->loop) { + pw_loop_destroy(pw->loop); + pw->loop = NULL; + } + + memset(&pw->cap_set, 0, sizeof(pw->cap_set)); + + if (pw->node.action && !strcmp(pw->node.action, UDEV_ACTION_RM)) { + BUILD_KEY(pw->node.id); + map_remove(nodes, key); + } +} + +static void removed_proxy(void *data) { + pw_data_t *pw = (pw_data_t *)data; + pw->node.action = UDEV_ACTION_RM; // this will kill our module during destroy_dev() call! + INFO("Removed node %d\n", pw->node.id); + last_recved = pw; +} + +static const struct pw_proxy_events proxy_events = { + PW_VERSION_PROXY_EVENTS, + .removed = removed_proxy, +}; + +// see https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/spa/include/spa/debug/pod.h +static void parse_props(pw_data_t *pw, const struct spa_pod *pod) +{ + struct spa_pod_prop *prop; + struct spa_pod_object *obj = (struct spa_pod_object *)pod; + SPA_POD_OBJECT_FOREACH(obj, prop) { + printf("top %d\n", prop->key); + switch (prop->key) { + case SPA_PROP_device: { + const char *str = NULL; + spa_pod_get_string(&prop->value, &str); + printf("String \"%s\"\n", str); + break; + } + case SPA_PROP_INFO_id: { + uint32_t id; + if (spa_pod_get_id(&prop->value, &id) < 0) { + break; + } + printf("ID: %d\n", id); + break; + } + case SPA_PROP_INFO_name: { + const char *name; + if (spa_pod_get_string(&prop->value, &name) < 0) { + break; + } + printf("Name: %s\n", name); + break; + } + case SPA_PROP_INFO_type: { + if (spa_pod_is_choice(&prop->value)) { + struct spa_pod_object_body *body = (struct spa_pod_object_body *)SPA_POD_BODY(&prop->value); + struct spa_pod_choice_body *b = (struct spa_pod_choice_body *)body; + const struct spa_type_info *ti = spa_debug_type_find(spa_type_choice, b->type); + void *p; + uint32_t size = SPA_POD_BODY_SIZE(&prop->value); + + // TODO: store default values somewhere (easy) + // TODO: find current values?? + printf("Choice: type %s\n", ti ? ti->name : "unknown"); + SPA_POD_CHOICE_BODY_FOREACH(b, size, p) { + switch (b->child.type) { + case SPA_TYPE_Bool: + printf("\tBool %s\n", (*(int32_t *) p) ? "true" : "false"); + break; + case SPA_TYPE_Int: + printf("\tInt %d\n", *(int32_t *) p); + break; + default: + INFO("Unmanaged type: %d\n", b->child.type); + break; + } + } + } + break; + } + default: + break; + } + } + printf("\n"); +} + +static void node_event_param(void *object, int seq, + uint32_t id, uint32_t index, uint32_t next, + const struct spa_pod *param) +{ + switch (id) { + case SPA_PARAM_PropInfo: + parse_props(object, param); + break; + default: + break; + } +} + +static void node_event_info(void *object, const struct pw_node_info *info) { + pw_data_t *pw = (pw_data_t *)object; + const struct spa_dict_item *prop; + spa_dict_for_each(prop, info->props) { + printf("'%s' -> '%s'\n", prop->key, prop->value); + } + printf("\n"); + + // TODO: store the device path and use it before calling capture to check + // if device is actually available? Shouldn't pipewire handle this? + const char *str; + if ((str = spa_dict_lookup(info->props, PW_KEY_OBJECT_PATH))) { + printf("ObjectPath: %s\n", str); + } +} + +static const struct pw_node_events node_events = { + PW_VERSION_NODE_EVENTS, + .info = node_event_info, + .param = node_event_param, +}; + +static void registry_event_global(void *data, uint32_t id, + uint32_t permissions, const char *type, uint32_t version, + const struct spa_dict *props) { + + if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) { + const char *mc = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + const char *mr = spa_dict_lookup(props, PW_KEY_MEDIA_ROLE); + if (mc && mr && !strcmp(mc, "Video/Source") && !strcmp(mr, "Camera")) { + pw_data_t *pw = calloc(1, sizeof(pw_data_t)); + if (!pw) { + return; + } + INFO("Added node %d\n", id); + pw->node.id = id; + pw->node.action = UDEV_ACTION_ADD; + pw->node.proxy = pw_registry_bind(pw_mon.registry, id, type, PW_VERSION_NODE, sizeof(pw_data_t)); + pw_proxy_add_listener(pw->node.proxy, &pw->node.proxy_listener, &proxy_events, pw); +// pw_proxy_add_object_listener(pw->node.proxy, +// &pw->node.object_listener, +// &node_events, pw); + + last_recved = pw; // used by recv_monitor + BUILD_KEY(id); + map_put(nodes, key, pw); + +// pw_node_enum_params((struct pw_node*)pw->node.proxy, 0, +// SPA_PARAM_PropInfo, 0, 0, NULL); + } + } +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, +}; + +static int register_monitor_fd(const char *pw_runtime_dir) { + setenv("XDG_RUNTIME_DIR", pw_runtime_dir, 1); + + pw_init(NULL, NULL); + pw_inited = true; + int ret = -1; + do { + pw_mon.loop = pw_loop_new(NULL); + if (!pw_mon.loop) { + break; + } + pw_mon.context = pw_context_new(pw_mon.loop, NULL, 0); + if (!pw_mon.context) { + break; + } + pw_mon.core = pw_context_connect(pw_mon.context, NULL, 0); + if (!pw_mon.core) { + break; + } + pw_mon.registry = pw_core_get_registry(pw_mon.core, PW_VERSION_REGISTRY, 0); + if (!pw_mon.registry) { + break; + } + + spa_zero(pw_mon.registry_listener); + pw_registry_add_listener(pw_mon.registry, &pw_mon.registry_listener, ®istry_events, NULL); + + int fd = pw_loop_get_fd(pw_mon.loop); + m_register_fd(fd, false, &pw_mon); + + pw_loop_enter(pw_mon.loop); + ret = 0; + } while (false); + + unsetenv("XDG_RUNTIME_DIR"); + if (ret == -1) { + destroy_monitor(); + } + return ret; +} + +static int init_monitor(void) { + const char *pw_runtime_dir = getenv(PW_ENV_NAME); + if (pw_runtime_dir && pw_runtime_dir[0] != '\0') { + struct stat s; + if (stat(pw_runtime_dir, &s) == -1 || !S_ISDIR(s.st_mode)) { + fprintf(stderr, "Failed to stat '%s' or not a folder. Killing pipewire.\n", pw_runtime_dir); + goto err; + } + + /* + * Pipewire needs an XDG_RUNTIME_DIR set; + * at this phase, we are not being called by anyone; + * thus we need to workaround this by fetching the pipewire socket + * for the XDG_RUNTIME_DIR specified in CLIGHTD_PIPEWIRE_RUNTIME_DIR env variable; + * eventually being notified of socket creation using inotify mechanism. + */ + char path[256]; + snprintf(path, sizeof(path), "%s/pipewire-0", pw_runtime_dir); + if (access(path, F_OK) == 0) { + // Pipewire socket already present! Register the monitor right away + if (register_monitor_fd(pw_runtime_dir) == -1) { + fprintf(stderr, "Failed to register monitor. Killing pipewire.\n"); + goto err; + } + } else { + // Register an inotify watcher + int inot_fd = inotify_init(); + if (inotify_add_watch(inot_fd, pw_runtime_dir, IN_CREATE) >= 0) { + m_register_fd(inot_fd, true, NULL); + } else { + fprintf(stderr, "Failed to watch folder '%s': %m\n", pw_runtime_dir); + close(inot_fd); + goto err; + } + } + + // Return an eventfd to notify Sensor for new devices + efd = eventfd(0, 0); + return efd; + } + // Env not found. Disable. + fprintf(stderr, "No '" PW_ENV_NAME "' env found. Killing pipewire.\n"); + +err: + return -1; +} + +static void recv_monitor(void **dev) { + // Consume the eventfd + uint64_t u; + read(efd, &u, sizeof(uint64_t)); + + // Actually search for new nodes + last_recved = NULL; + pw_loop_iterate(pw_mon.loop, 0); + *dev = last_recved; +} + +static void destroy_monitor(void) { + if (!pw_inited) { + return; + } + + if (pw_mon.registry) { + pw_proxy_destroy((struct pw_proxy*)pw_mon.registry); + } + if (pw_mon.core) { + pw_core_disconnect(pw_mon.core); + } + if (pw_mon.context) { + pw_context_destroy(pw_mon.context); + } + if (pw_mon.loop) { + pw_loop_destroy(pw_mon.loop); + } + memset(&pw_mon, 0, sizeof(pw_mon)); + + if (efd != -1) { + close(efd); + } + + pw_deinit(); +} + +static void on_timeout(void *userdata, uint64_t expirations) { + pw_data_t *pw = (pw_data_t *)userdata; + INFO("Stream timed out. Leaving.\n"); + pw->cap_set.with_err = true; +} + +static int capture(void *dev, double *pct, const int num_captures, char *settings) { + pw_data_t *pw = (pw_data_t *)dev; + pw->cap_set.pct = pct; + pw->cap_set.settings = settings; + + setenv("XDG_RUNTIME_DIR", bus_sender_runtime_dir(), 1); + + const struct spa_pod *params; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + pw->loop = pw_loop_new(NULL); + pw->stream = pw_stream_new_simple(pw->loop, + "clightd-camera-pw", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Video", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Camera", + NULL), + &stream_events, + pw); + build_format(&b, ¶ms); + int res; + if ((res = pw_stream_connect(pw->stream, + PW_DIRECTION_INPUT, + pw->node.id, + PW_STREAM_FLAG_AUTOCONNECT | /* try to automatically connect this stream */ + PW_STREAM_FLAG_MAP_BUFFERS, /* mmap the buffer data for us */ + ¶ms, 1)) /* extra parameters, see above */ < 0) { + + fprintf(stderr, "Can't connect: %s\n", spa_strerror(res)); + } else { + /* Use a 2s timeout to avoid locking on the pw_loop_iterate() loop! */ + struct timespec timeout = { .tv_sec = 2 }; + pw->timer = pw_loop_add_timer(pw->loop, on_timeout, pw); + pw_loop_update_timer(pw->loop, pw->timer, &timeout, NULL, false); + pw_loop_enter(pw->loop); + while (pw->cap_set.capture_idx < num_captures && !pw->cap_set.with_err) { + if (pw_loop_iterate(pw->loop, -1) < 0) { + break; + } + } + pw_loop_leave(pw->loop); +// restore_camera_settings(pw); + } + unsetenv("XDG_RUNTIME_DIR"); + return pw->cap_set.capture_idx; +} + +// Stolen from https://github.com/PipeWire/pipewire/blob/master/spa/plugins/v4l2/v4l2-utils.c#L1049 +static inline enum spa_prop control_to_prop_id(uint32_t control_id) { + switch (control_id) { + case V4L2_CID_BRIGHTNESS: + return SPA_PROP_brightness; + case V4L2_CID_CONTRAST: + return SPA_PROP_contrast; + case V4L2_CID_SATURATION: + return SPA_PROP_saturation; + case V4L2_CID_HUE: + return SPA_PROP_hue; + case V4L2_CID_GAMMA: + return SPA_PROP_gamma; + case V4L2_CID_EXPOSURE: + return SPA_PROP_exposure; + case V4L2_CID_GAIN: + return SPA_PROP_gain; + case V4L2_CID_SHARPNESS: + return SPA_PROP_sharpness; + default: + return SPA_PROP_START_CUSTOM + control_id; + } +} + +// TODO: unsupported in pipewire right now +static struct v4l2_control *set_camera_setting(void *priv, uint32_t op, float val, const char *op_name, bool store) { + pw_data_t *pw = (pw_data_t *)priv; + enum spa_prop pw_op = control_to_prop_id(op); +// if (val > 0) { +// uint8_t params_buffer[1024]; +// struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); +// const struct spa_pod *params = spa_pod_builder_add_object(&b, +// SPA_TYPE_OBJECT_Props, 0, +// SPA_PROP_brightness, SPA_POD_Float(140)); +// pw_node_set_param(pw->node.proxy, SPA_PARAM_Props, 0, params); +// } + + const struct pw_stream_control *ctrl = pw_stream_get_control(pw->stream, pw_op); + if (ctrl) { + INFO("%s (%u) default val: %.2lf\n", op_name, pw_op, ctrl->def); + if (val < 0) { + /* Set default value */ + val = ctrl->def; + } + if (ctrl->values[0] != val) { + float old_val = ctrl->values[0]; + int ret = pw_stream_set_control(pw->stream, pw_op, 1, &val); + if (ret == 0) { + INFO("Set '%s' val: %.2lf\n", op_name, val); + if (store) { + INFO("Storing initial setting for '%s': %.2lf\n", op_name, old_val); + struct v4l2_control *v = malloc(sizeof(struct v4l2_control)); + v->value = old_val; + v->id = op; + return v; + } + } else { + INFO("Failed to set '%s'.\n", op_name); + } + } else { + INFO("Value %2.lf for '%s' already set.\n", val, ctrl->name); + } + } else { + INFO("'%s' unsupported\n", op_name); + } + return NULL; +} + +#endif diff --git a/src/modules/signal.c b/src/modules/signal.c index 5033be3..bac01fb 100644 --- a/src/modules/signal.c +++ b/src/modules/signal.c @@ -5,10 +5,29 @@ MODULE("SIGNAL"); -static void module_pre_start(void) { - +static sigset_t mask; + + +static void block_signals(void) { + sigemptyset(&mask); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGTERM); + sigprocmask(SIG_BLOCK, &mask, NULL); } +/* + * Block signals for any thread spawned, + * so that only main thread will indeed receive SIGTERM/SIGINT signals + * + * It is needed as some libraries (libusb, libddcutil, libpipewire) spawn threads + * that would mess with the signal receiving mechanism of Clightd + * + * NOTE: we cannot use a normal constructor because libraries ctor are + * always run before program ctor, as expected; + * see: https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/elf/dl-init.c#L109 + */ +__attribute__((section(".preinit_array"), used)) static typeof(block_signals) *preinit_p = block_signals; + static bool check(void) { return true; } @@ -18,11 +37,6 @@ static bool evaluate(void) { } static void init(void) { - sigset_t mask; - sigemptyset(&mask); - sigaddset(&mask, SIGINT); - sigaddset(&mask, SIGTERM); - sigprocmask(SIG_BLOCK, &mask, NULL); m_register_fd(signalfd(-1, &mask, 0), true, NULL); } diff --git a/src/utils/bus_utils.c b/src/utils/bus_utils.c new file mode 100644 index 0000000..1575ad6 --- /dev/null +++ b/src/utils/bus_utils.c @@ -0,0 +1,45 @@ +#include "bus_utils.h" +#include + +static char xdg_runtime_dir[PATH_MAX + 1]; +static char xauth_path[PATH_MAX + 1]; + +int bus_sender_fill_creds(sd_bus_message *m) { + xdg_runtime_dir[0] = 0; + xauth_path[0] = 0; + + int ret = -1; + sd_bus_creds *c = NULL; + sd_bus_query_sender_creds(m, SD_BUS_CREDS_EUID, &c); + if (c) { + uid_t uid = 0; + if (sd_bus_creds_get_euid(c, &uid) >= 0) { + snprintf(xdg_runtime_dir, PATH_MAX, "/run/user/%d", uid); + setpwent(); + for (struct passwd *p = getpwent(); p; p = getpwent()) { + if (p->pw_uid == uid && p->pw_dir) { + snprintf(xauth_path, PATH_MAX, "%s/.Xauthority", p->pw_dir); + ret = 0;; + break; + } + } + endpwent(); + } + free(c); + } + return ret; +} + +const char *bus_sender_runtime_dir(void) { + if (xdg_runtime_dir[0] != 0) { + return xdg_runtime_dir; + } + return NULL; +} + +const char *bus_sender_xauth(void) { + if (xauth_path[0] != 0) { + return xauth_path; + } + return NULL; +} diff --git a/src/utils/bus_utils.h b/src/utils/bus_utils.h new file mode 100644 index 0000000..ecd7268 --- /dev/null +++ b/src/utils/bus_utils.h @@ -0,0 +1,5 @@ +#include "commons.h" + +int bus_sender_fill_creds(sd_bus_message *m); +const char *bus_sender_runtime_dir(void); +const char *bus_sender_xauth(void); diff --git a/src/utils/drm_utils.c b/src/utils/drm_utils.c index 4f50c20..676cd98 100644 --- a/src/utils/drm_utils.c +++ b/src/utils/drm_utils.c @@ -7,7 +7,7 @@ #define DRM_SUBSYSTEM "drm" int drm_open_card(const char **card) { - if (!*card || !strlen(*card)) { + if (*card == NULL || *card[0] == '\0') { /* Fetch first matching device from udev */ struct udev_device *dev = NULL; get_udev_device(NULL, DRM_SUBSYSTEM, NULL, NULL, &dev); diff --git a/src/utils/udev.c b/src/utils/udev.c index ca08f81..f9f1f31 100644 --- a/src/utils/udev.c +++ b/src/utils/udev.c @@ -33,7 +33,7 @@ void get_udev_device(const char *interface, const char *subsystem, const udev_ma sd_bus_error **ret_error, struct udev_device **dev) { *dev = NULL; /* if no interface is specified, try to get first matching device */ - if (!interface || !strlen(interface)) { + if (interface == NULL || interface[0] == '\0') { get_first_matching_device(dev, subsystem, match); } else { char *name = strrchr(interface, '/'); diff --git a/src/utils/wl_utils.c b/src/utils/wl_utils.c index 5daa594..e39c013 100644 --- a/src/utils/wl_utils.c +++ b/src/utils/wl_utils.c @@ -2,9 +2,12 @@ #include "wl_utils.h" #include "commons.h" +#include "bus_utils.h" #include #include +#define WL_DISPLAY_DEF "wayland-0" + typedef struct { struct wl_display *dpy; char *env; @@ -30,6 +33,12 @@ static void wl_info_dtor(void *data) { } struct wl_display *fetch_wl_display(const char *display, const char *env) { + if (!env || env[0] == 0) { + env = bus_sender_runtime_dir(); + } + if (!display || display[0] == 0) { + display = WL_DISPLAY_DEF; + } if (env) { wl_info *info = map_get(wl_map, display); if (!info) { diff --git a/src/utils/wl_utils.h b/src/utils/wl_utils.h index 37a260a..1c2cd4d 100644 --- a/src/utils/wl_utils.h +++ b/src/utils/wl_utils.h @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/src/utils/xorg_utils.c b/src/utils/xorg_utils.c new file mode 100644 index 0000000..d21670c --- /dev/null +++ b/src/utils/xorg_utils.c @@ -0,0 +1,25 @@ +#if defined GAMMA_PRESENT || defined DPMS_PRESENT || defined SCREEN_PRESENT + +#include "bus_utils.h" +#include "xorg_utils.h" + +#define XORG_DISPLAY_DEF ":0" + +Display *fetch_xorg_display(const char **display, const char *xauthority) { + if (!*display || display[0] == 0) { + *display = XORG_DISPLAY_DEF; + } + if (!xauthority || xauthority[0] == 0) { + xauthority = bus_sender_xauth(); + } + + if (xauthority) { + setenv("XAUTHORITY", xauthority, 1); + Display *dpy = XOpenDisplay(*display); + unsetenv("XAUTHORITY"); + return dpy; + } + return NULL; +} + +#endif diff --git a/src/utils/xorg_utils.h b/src/utils/xorg_utils.h new file mode 100644 index 0000000..06fb402 --- /dev/null +++ b/src/utils/xorg_utils.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +Display *fetch_xorg_display(const char **display, const char *xauthority);