diff --git a/ExampleConfig.ini b/ExampleConfig.ini index 0ea4833..ef0a75e 100644 --- a/ExampleConfig.ini +++ b/ExampleConfig.ini @@ -10,6 +10,10 @@ Port=26760 # This is useful if you have over four controllers (which is the limit of DSU protocol) - run two servers with different config files, each serving specific controllers. # Default false. AllowlistMode=true +# Use UPower to try and provide battery level to clients. +# You may want to disable this to avoid warnings if you don't have UPower daemon up and running or as part of troubleshooting. +# Default true. +UseUPower=true # Per-device sections - optional unless AllowlistMode is enabled. # Section name can be found as "unique identifier" for device in server's output. diff --git a/src/Config.vala b/src/Config.vala index dd81daf..d63b7ec 100644 --- a/src/Config.vala +++ b/src/Config.vala @@ -111,6 +111,7 @@ namespace Evdevhook { public uint16 port { get; private set; default = 26760; } public bool allowlist_mode { get; private set; default = false; } + public bool use_upower { get; private set; default = true; } construct { device_type_configs = new HashMap(); @@ -169,6 +170,9 @@ namespace Evdevhook { case "AllowlistMode": allowlist_mode = kfile.get_boolean(MAIN_GROUP, key); break; + case "UseUPower": + use_upower = kfile.get_boolean(MAIN_GROUP, key); + break; default: warning("Unknown configuration key %s", key); break; diff --git a/src/EvdevCemuhookDevice.vala b/src/EvdevCemuhookDevice.vala index 6f852a4..afc9d77 100644 --- a/src/EvdevCemuhookDevice.vala +++ b/src/EvdevCemuhookDevice.vala @@ -49,14 +49,69 @@ namespace Evdevhook { } } + private Cemuhook.BatteryStatus battery_state_to_cemuhook(uint state) { + switch(state) { + case 1: + return CHARGING; + case 4: + return CHARGED; + default: + return NA; + } + } + + private Cemuhook.BatteryStatus battery_level_to_cemuhook(uint level) { + switch(level) { + case 3: + return LOW; + case 4: + return DYING; + case 6: + return MEDIUM; + case 7: + return HIGH; + case 8: + return FULL; + default: + return NA; + } + } + + private Cemuhook.BatteryStatus battery_percentage_to_cemuhook(double percentage) { + if (percentage == 0.0) { + return NA; + } + + if (percentage > 90.0) { + return FULL; + } + + if (percentage > 60.0) { + return HIGH; + } + + if (percentage > 30.0) { + return MEDIUM; + } + + if (percentage > 10.0) { + return LOW; + } + + return DYING; + } + sealed class EvdevCemuhookDevice: Object, Cemuhook.AbstractPhysicalDevice { private Evdev.Device dev; private IOChannel dev_iochan; private DeviceTypeConfig devtypeconf; private DeviceConfig devconf; + private Cancellable cancellable = new Cancellable(); + private Cemuhook.DeviceType devtype = NO_MOTION; private Cemuhook.ConnectionType connection_type = OTHER; + private Cemuhook.BatteryStatus battery_status = NA; private bool has_timestamp_event = false; private uint64 motion_timestamp = 0; private uint64 mac = 0; @@ -95,6 +150,9 @@ namespace Evdevhook { IOFunc cb = process_incoming; dev_iochan.add_watch(IN | HUP, cb); + if (new Config().use_upower) { + battery_reader.begin(); + } } private bool process_incoming(IOChannel source, IOCondition condition) { @@ -181,11 +239,60 @@ namespace Evdevhook { private void destroy() { print("Device %s disconnected\n", dev.uniq); + cancellable.cancel(); disconnected(); } + /* + * It's worth mentioning that access to members of proxy objects results in synchronous dbus calls. + * While this does not seem to cause issues in practice, it theoretically might. If it does, + * use org.freedesktop.DBus.Properties interface directly instead of relying on wrappers. + */ + private async void battery_reader() { + try { + UPower.Device battery = null; + var core = yield Bus.get_proxy(SYSTEM, "org.freedesktop.UPower", "/org/freedesktop/UPower", NONE, cancellable); + // Retry a few times with a pause to ensure that UPower has time to initialize the device + for (int i = 0; battery == null && i < 4; ++i) { + cancellable.set_error_if_cancelled(); + foreach (var devpath in yield core.enumerate_devices()) { + var upower_dev = yield Bus.get_proxy(SYSTEM, "org.freedesktop.UPower", devpath, NONE, cancellable); + if (upower_dev.serial == dev.uniq) { + battery = (owned)upower_dev; + break; + } + } + GLib.Timeout.add_once(500, () => { battery_reader.callback(); }); + yield; + } + + if (battery == null) { + return; + } + + while(!cancellable.is_cancelled()) { + battery_status = battery_state_to_cemuhook(battery.state); + if (battery_status == NA) { + battery_status = battery_level_to_cemuhook(battery.battery_level); + } + if (battery_status == NA) { + battery_status = battery_percentage_to_cemuhook(battery.percentage); + } + GLib.Timeout.add_once(5000, () => { battery_reader.callback(); }); + yield; + } + } catch(IOError.CANCELLED e) { + // Expected + } catch(Error e) { + warning("Error in battery reader: %s\n", e.message); + } finally { + battery_status = NA; + } + } + public Cemuhook.DeviceType get_device_type() { return devtype; } public Cemuhook.ConnectionType get_connection_type() { return connection_type; } + public Cemuhook.BatteryStatus get_battery() { return battery_status; } public uint64 get_mac() { return mac; } diff --git a/src/dbus/upower_core.vala b/src/dbus/upower_core.vala new file mode 100644 index 0000000..b123721 --- /dev/null +++ b/src/dbus/upower_core.vala @@ -0,0 +1,33 @@ +namespace UPower { + + [DBus (name = "org.freedesktop.UPower", timeout = 5000)] + public interface Core : GLib.Object { + + [DBus (name = "EnumerateDevices")] + public abstract async GLib.ObjectPath[] enumerate_devices() throws DBusError, IOError; + + [DBus (name = "GetDisplayDevice")] + public abstract async GLib.ObjectPath get_display_device() throws DBusError, IOError; + + [DBus (name = "GetCriticalAction")] + public abstract async string get_critical_action() throws DBusError, IOError; + + [DBus (name = "DeviceAdded")] + public signal void device_added(GLib.ObjectPath device); + + [DBus (name = "DeviceRemoved")] + public signal void device_removed(GLib.ObjectPath device); + + [DBus (name = "DaemonVersion")] + public abstract string daemon_version { owned get; } + + [DBus (name = "OnBattery")] + public abstract bool on_battery { get; } + + [DBus (name = "LidIsClosed")] + public abstract bool lid_is_closed { get; } + + [DBus (name = "LidIsPresent")] + public abstract bool lid_is_present { get; } + } +} diff --git a/src/dbus/upower_device.vala b/src/dbus/upower_device.vala new file mode 100644 index 0000000..0b00b01 --- /dev/null +++ b/src/dbus/upower_device.vala @@ -0,0 +1,116 @@ +namespace UPower { + + [DBus (name = "org.freedesktop.UPower.Device", timeout = 5000)] + public interface Device : GLib.Object { + + [DBus (name = "Refresh")] + public abstract async void refresh() throws DBusError, IOError; + + [DBus (name = "GetHistory")] + public abstract async DeviceDataStruct[] get_history(string type, uint timespan, uint resolution) throws DBusError, IOError; + + [DBus (name = "GetStatistics")] + public abstract async DeviceDataStruct2[] get_statistics(string type) throws DBusError, IOError; + + [DBus (name = "NativePath")] + public abstract string native_path { owned get; } + + [DBus (name = "Vendor")] + public abstract string vendor { owned get; } + + [DBus (name = "Model")] + public abstract string model { owned get; } + + [DBus (name = "Serial")] + public abstract string serial { owned get; } + + [DBus (name = "UpdateTime")] + public abstract uint64 update_time { get; } + + //[DBus (name = "Type")] + //public abstract uint type { get; } + + [DBus (name = "PowerSupply")] + public abstract bool power_supply { get; } + + [DBus (name = "HasHistory")] + public abstract bool has_history { get; } + + [DBus (name = "HasStatistics")] + public abstract bool has_statistics { get; } + + [DBus (name = "Online")] + public abstract bool online { get; } + + [DBus (name = "Energy")] + public abstract double energy { get; } + + [DBus (name = "EnergyEmpty")] + public abstract double energy_empty { get; } + + [DBus (name = "EnergyFull")] + public abstract double energy_full { get; } + + [DBus (name = "EnergyFullDesign")] + public abstract double energy_full_design { get; } + + [DBus (name = "EnergyRate")] + public abstract double energy_rate { get; } + + [DBus (name = "Voltage")] + public abstract double voltage { get; } + + [DBus (name = "ChargeCycles")] + public abstract int charge_cycles { get; } + + [DBus (name = "Luminosity")] + public abstract double luminosity { get; } + + [DBus (name = "TimeToEmpty")] + public abstract int64 time_to_empty { get; } + + [DBus (name = "TimeToFull")] + public abstract int64 time_to_full { get; } + + [DBus (name = "Percentage")] + public abstract double percentage { get; } + + [DBus (name = "Temperature")] + public abstract double temperature { get; } + + [DBus (name = "IsPresent")] + public abstract bool is_present { get; } + + [DBus (name = "State")] + public abstract uint state { get; } + + [DBus (name = "IsRechargeable")] + public abstract bool is_rechargeable { get; } + + [DBus (name = "Capacity")] + public abstract double capacity { get; } + + [DBus (name = "Technology")] + public abstract uint technology { get; } + + [DBus (name = "WarningLevel")] + public abstract uint warning_level { get; } + + [DBus (name = "BatteryLevel")] + public abstract uint battery_level { get; } + + [DBus (name = "IconName")] + public abstract string icon_name { owned get; } + } + + public struct DeviceDataStruct2 { + public double attr1; + public double attr2; + } + + public struct DeviceDataStruct { + public uint attr1; + public double attr2; + public uint attr3; + } +} diff --git a/src/meson.build b/src/meson.build index 13cd40c..3242388 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,5 +1,9 @@ evdevhook2_sources = [ + 'dbus/upower_core.vala', + 'dbus/upower_device.vala', + 'main.vala', + 'Config.vala', 'EvdevCemuhookDevice.vala', 'Server.vala',