Skip to content

Commit

Permalink
Use UPower daemon to provide battery levels
Browse files Browse the repository at this point in the history
Closes #1.
  • Loading branch information
v1993 committed Aug 3, 2023
1 parent 9983466 commit d7f6d48
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 0 deletions.
4 changes: 4 additions & 0 deletions ExampleConfig.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/Config.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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<DeviceTypeIdentifier, DeviceTypeConfig>();
Expand Down Expand Up @@ -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;
Expand Down
107 changes: 107 additions & 0 deletions src/EvdevCemuhookDevice.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<UPower.Core>(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<UPower.Device>(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; }

Expand Down
33 changes: 33 additions & 0 deletions src/dbus/upower_core.vala
Original file line number Diff line number Diff line change
@@ -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; }
}
}
116 changes: 116 additions & 0 deletions src/dbus/upower_device.vala
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 4 additions & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
evdevhook2_sources = [
'dbus/upower_core.vala',
'dbus/upower_device.vala',

'main.vala',

'Config.vala',
'EvdevCemuhookDevice.vala',
'Server.vala',
Expand Down

0 comments on commit d7f6d48

Please sign in to comment.