/**
* @file battery-udev.c
* Battery module -- this implements battery and charger logic for MCE
*
* Copyright (c) 2018 - 2022 Jolla Ltd.
* Copyright (c) 2019 - 2020 Open Mobile Platform LLC.
*
* @author Simo Piiroinen
*
* mce is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* mce is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with mce. If not, see .
*/
#include "../mce.h"
#include "../mce-io.h"
#include "../mce-lib.h"
#include "../mce-log.h"
#include "../mce-conf.h"
#include "../mce-dbus.h"
#include "../mce-wakelock.h"
#include
#include
#include
#include
/* ========================================================================= *
* Constants
* ========================================================================= */
/** Module name */
#define MODULE_NAME "battery_udev"
/** Whether to support legacy pattery low led pattern; nonzero for yes */
#define SUPPORT_BATTERY_LOW_LED_PATTERN 0
/* Limits for udev capacity percent -> battery_status_t mapping
*
* FIXME: These should be configurable / device type, and they should be
* defined in one place only. Currently we have:
* - this mce plugin: hardcoded values
* - dsme: hardcoded / from config file values
* - statefs: hardcoded / from environment values
*/
#define BATTERY_CAPACITY_UNDEF -1
#define BATTERY_CAPACITY_EMPTY 2 // statefs uses 3, dsme defaults to 2
#define BATTERY_CAPACITY_LOW 10 // statefs uses 10
#define BATTERY_CAPACITY_FULL 90 // statefs uses 96
/* Power supply device properties we are interested in */
#define PROP_PRESENT "POWER_SUPPLY_PRESENT"
#define PROP_ONLINE "POWER_SUPPLY_ONLINE"
#define PROP_CAPACITY "POWER_SUPPLY_CAPACITY"
#define PROP_STATUS "POWER_SUPPLY_STATUS"
#define PROP_REAL_TYPE "POWER_SUPPLY_REAL_TYPE"
#define PROP_TYPE "POWER_SUPPLY_TYPE"
/** INI-file group for blacklisting device properties */
#define MCE_CONF_BATTERY_UDEV_PROPERTY_BLACKLIST_GROUP "BatteryUDevPropertyBlacklist"
/** INI-file group for blacklisting devices */
#define MCE_CONF_BATTERY_UDEV_DEVICE_BLACKLIST_GROUP "BatteryUDevDeviceBlacklist"
/** INI-file group for configuring charger types */
#define MCE_CONF_BATTERY_UDEV_DEVICE_CHARGERTYPE_GROUP "BatteryUDevChargerTypes"
/** INI-file group for miscellaneous settings */
#define MCE_CONF_BATTERY_UDEV_SETTINGS_GROUP "BatteryUDevSettings"
/** Setting for forced refresh on udev notify event */
#define MCE_CONF_BATTERY_UDEV_REFRESH_ON_NOTIFY "RefreshOnNotify"
#define DEFAULT_BATTERY_UDEV_REFRESH_ON_NOTIFY false
/** Setting for forced refresh on USB state udev notify from extcon devices */
#define MCE_CONF_BATTERY_UDEV_REFRESH_ON_EXTCON "RefreshOnExtcon"
#define DEFAULT_BATTERY_UDEV_REFRESH_ON_EXTCON false
/** Setting for forced refresh on system heartbeat */
#define MCE_CONF_BATTERY_UDEV_REFRESH_ON_HEARTBEAT "RefreshOnHeartbeat"
#define DEFAULT_BATTERY_UDEV_REFRESH_ON_HEARTBEAT true
/** Delay between udev notifications and battery state evaluation
*
* The purpose is to increase chances of getting battery and
* charger notifications handled in one go and thus decrease
* changes of getting false positive battery full blips.
*/
#define BATTERY_REEVALUATE_DELAY 50 // [ms]
/** Delay between udev notifications and refreshing all devices
*
* Some kernels do better job with udev notifications than
* others... if we get notification about any device node
* that is used for battery / charger tracking, all properties
* of all tracked devices are checked after brief delay
*
* As this is relatively costly operation -> the wait should
* be long enought to cover all related notifications that
* kernel will send.
*
* As suspend is blocked during the wait -> the wait should
* be as short as possible.
*
* As delay affects ui responce to physical actions taken by
* user (e.g. detaching charger cable) -> the wait should be
* in "perceived immediate" time span.
*
* As a compromize, relatively short delay is used and the timer
* is restarted whenever we get udev notifications -> a burst of
* udev activity leads to only one evaluation round and suspend
* blocking ends soon after udev goes idle.
*/
#define DEVICES_REFRESH_DELAY 250
/* ========================================================================= *
* Types
* ========================================================================= */
/** Classification of power supply device properties
*/
typedef enum
{
/** Placeholder value, property type not defined in lookup table
*
* Interpreted as PROPERTY_TYPE_DEBUG or PROPERTY_TYPE_IGNORE
* depending on whether property blacklist configuration block
* exists or not.
*/
PROPERTY_TYPE_UNDEF,
/** Property has been configured to be completely ignored */
PROPERTY_TYPE_IGNORE,
/** Property has been configured to be shown for debugging purposes */
PROPERTY_TYPE_DEBUG,
/** Property has been configured to be relevant for state evaluation */
PROPERTY_TYPE_USED,
} property_type_t;
/** Battery properties in mce statemachine compatible form
*/
typedef struct
{
/** Battery charge percentage; for use with battery_level_pipe */
int battery_level;
/** Battery FULL/OK/LOW/EMPTY; for use with battery_status_pipe */
battery_status_t battery_status;
/** Battery UNKNOWN|CHARGING|DISCHARGING|NOT_CHARGING|FULL"*/
battery_state_t battery_state;
/** Charger connected; for use with charger_state_pipe */
charger_state_t charger_state;
/** Charger type; for tweaking UI behavior */
charger_type_t charger_type;
} mcebat_t;
typedef struct udevtracker_t udevtracker_t;
typedef struct udevdevice_t udevdevice_t;
typedef struct udevproperty_t udevproperty_t;
/** Bookkeeping data for udev power supply device tracking
*/
struct udevtracker_t
{
/** udev handle */
struct udev *udt_udev_handle;
/** Monitor for power supply devices */
struct udev_monitor *udt_udev_monitor;
/** I/O watch id for monitor input */
guint udt_udev_event_id;
/** Timer id for delayed state re-evaluation */
guint udt_rethink_id;
/** Cached charger/battery device data */
GHashTable *udt_devices; // [dev_name] -> udevdevice_t *
};
/** Bookkeeping data for a single udev power supply device
*/
struct udevdevice_t
{
/** Device sysname */
gchar *udd_name;
/** Properties associated with the device */
GHashTable *udd_props; // [key_name] -> udevproperty_t *
/** Flag for: Device has reached battery full state */
bool udd_full;
/** Flag for: The latest evaluated status was "Charging" */
bool udd_charging;
};
/** Bookkeeping data for a single udev device property
*/
struct udevproperty_t
{
/** Containing device */
udevdevice_t *udp_dev;
/** Property name */
gchar *udp_key;
/** Property value */
gchar *udp_val;
/** Flag for: Property is used in state evaluation */
bool udp_used;
};
/* ========================================================================= *
* Prototypes
* ========================================================================= */
/* ------------------------------------------------------------------------- *
* DBUS_HANDLERS
* ------------------------------------------------------------------------- */
#ifdef ENABLE_BATTERY_SIMULATION
static void mcebat_dbus_remove_client (const char *dbus_name);
static gboolean mcebat_dbus_client_removed_cb (DBusMessage *const msg);
static bool mcebat_dbus_add_client (const char *dbus_name);
static void mcebat_dbus_evaluate_battery_status(void);
static gboolean mcebat_dbus_charger_type_req_cb (DBusMessage *const msg);
static gboolean mcebat_dbus_charger_state_req_cb (DBusMessage *const msg);
static gboolean mcebat_dbus_battery_level_req_cb (DBusMessage *const msg);
#endif // ENABLE_BATTERY_SIMULATION
static void mcebat_dbus_init (void);
static void mcebat_dbus_quit (void);
/* ------------------------------------------------------------------------- *
* MCEBAT
* ------------------------------------------------------------------------- */
static void mcebat_update (void);
static gboolean mcebat_init_tracker_cb(gpointer aptr);
static void mcebat_init_settings (void);
/* ------------------------------------------------------------------------- *
* UDEVPROPERTY
* ------------------------------------------------------------------------- */
static void udevproperty_init_types (void);
static void udevproperty_quit_types (void);
static property_type_t udevproperty_lookup_type(const char *key);
static bool udevproperty_is_used (const char *key);
static bool udevproperty_is_ignored (const char *key);
static udevproperty_t *udevproperty_create (udevdevice_t *dev, const char *key);
static void udevproperty_delete (udevproperty_t *self);
static void udevproperty_delete_cb (void *self);
static const char *udevproperty_key (const udevproperty_t *self);
static const char *udevproperty_get (const udevproperty_t *self);
static bool udevproperty_set (udevproperty_t *self, const char *val);
/* ------------------------------------------------------------------------- *
* UDEVDEVICE
* ------------------------------------------------------------------------- */
static battery_state_t udevdevice_lookup_battery_state(const char *status);
static charger_type_t udevdevice_lookup_charger_type (const char *name);
static void udevdevice_init_chargertype (void);
static void udevdevice_quit_chargertype (void);
static void udevdevice_init_blacklist (void);
static void udevdevice_quit_blacklist (void);
static bool udevdevice_is_blacklisted (const char *name);
static udevdevice_t *udevdevice_create (const char *name);
static void udevdevice_delete (udevdevice_t *self);
static void udevdevice_delete_cb (void *self);
static const char *udevdevice_name (const udevdevice_t *self);
static udevproperty_t *udevdevice_get_prop (udevdevice_t *self, const char *key);
static udevproperty_t *udevdevice_add_prop (udevdevice_t *self, const char *key);
static bool udevdevice_set_prop (udevdevice_t *self, const char *key, const char *val);
static const char *udevdevice_get_str_prop (udevdevice_t *self, const char *key, const char *def);
static int udevdevice_get_int_prop (udevdevice_t *self, const char *key, int def);
static bool udevdevice_refresh (udevdevice_t *self, struct udev_device *dev);
static bool udevdevice_is_battery (udevdevice_t *self);
static bool udevdevice_is_charger (udevdevice_t *self);
static void udevdevice_evaluate_charger (udevdevice_t *self, mcebat_t *mcebat);
static void udevdevice_evaluate_charger_cb (gpointer key, gpointer value, gpointer aptr);
static void udevdevice_evaluate_battery (udevdevice_t *self, mcebat_t *mcebat);
static void udevdevice_evaluate_battery_cb (gpointer key, gpointer value, gpointer aptr);
/* ------------------------------------------------------------------------- *
* UDEVEXTCON
* ------------------------------------------------------------------------- */
static gchar *udevextcon_parse_usb_state(const char *state);
static bool udevextcon_update_state (const char *syspath, const char *state);
static void udevextcon_initialize_from(struct udev_device *dev);
static bool udevextcon_update_from (struct udev_device *dev);
static void udevextcon_init (void);
static void udevextcon_quit (void);
/* ------------------------------------------------------------------------- *
* UDEVTRACKER
* ------------------------------------------------------------------------- */
static udevtracker_t *udevtracker_create (void);
static void udevtracker_delete (udevtracker_t *self);
static void udevtracker_rethink (udevtracker_t *self);
static gboolean udevtracker_rethink_cb (gpointer aptr);
static void udevtracker_cancel_rethink (udevtracker_t *self);
static void udevtracker_schedule_rethink(udevtracker_t *self);
static udevdevice_t *udevtracker_add_dev (udevtracker_t *self, const char *path, const char *name);
static bool udevtracker_update_device (udevtracker_t *self, struct udev_device *dev);
static bool udevtracker_start (udevtracker_t *self);
static void udevtracker_stop (udevtracker_t *self);
static gboolean udevtracker_event_cb (GIOChannel *chn, GIOCondition cnd, gpointer aptr);
static void udevtracker_refresh_all (udevtracker_t *self);
static gboolean udevtracker_refresh_cb (gpointer aptr);
static void udevtracker_schedule_refresh(void);
static void udevtracker_cancel_refresh (void);
/* ------------------------------------------------------------------------- *
* DATAPIPE_HANDLERS
* ------------------------------------------------------------------------- */
static void mcebat_datapipe_heartbeat_event_cb(gconstpointer data);
static void mcebat_datapipe_init (void);
static void mcebat_datapipe_quit (void);
/* ------------------------------------------------------------------------- *
* G_MODULE
* ------------------------------------------------------------------------- */
const gchar *g_module_check_init(GModule *module);
void g_module_unload (GModule *module);
/* ========================================================================= *
* Data
* ========================================================================= */
/** Functionality provided by this module */
static const gchar *const provides[] = { MODULE_NAME, NULL };
/** Module information */
G_MODULE_EXPORT module_info_struct module_info =
{
/** Name of the module */
.name = MODULE_NAME,
/** Module provides */
.provides = provides,
/** Module priority */
.priority = 100
};
/** Cached battery state as exposed in datapipes
*
* Note: To avoid mce startup time glitches, these must be kept in
* sync with default values held in the relevant datapipes.
*/
static mcebat_t mcebat_datapipe = {
.battery_level = MCE_BATTERY_LEVEL_UNKNOWN,
.battery_status = BATTERY_STATUS_UNDEF,
.battery_state = BATTERY_STATE_UNKNOWN,
.charger_state = CHARGER_STATE_UNDEF,
.charger_type = CHARGER_TYPE_NONE,
};
/** Cached battery state as derived from udev
*/
static mcebat_t mcebat_actual = {
.battery_level = MCE_BATTERY_LEVEL_UNKNOWN,
.battery_status = BATTERY_STATUS_UNDEF,
.battery_state = BATTERY_STATE_UNKNOWN,
.charger_state = CHARGER_STATE_UNDEF,
.charger_type = CHARGER_TYPE_NONE,
};
#ifdef ENABLE_BATTERY_SIMULATION
/** Maximum number of concurrent call state requesters */
# define CLIENTS_MONITOR_COUNT 1
/** List of monitored battery state requesters */
static GSList *clients_monitor_list = NULL;
/** Cached battery state as requested over D-Bus
*
* Synchronized with mcebat_datapipe when simulation
* is activated, so no initialization is needed.
*/
static mcebat_t mcebat_simulated;
#endif // ENABLE_BATTERY_SIMULATION
/** Wakelock used for suspend proofing edev event processing */
static const char udevtracker_wakelock[] = "udevtracker_wakeup";
/** The device subsystem we are monitoring */
static const char udevtracker_subsystem[] = "power_supply";
/** Tracking state */
static udevtracker_t *udevtracker_object = 0;
/** Lookup table for device property classification */
static GHashTable *udevproperty_type_lut = 0;
/** Lookup table for device blacklisting */
static GHashTable *udevdevice_blacklist_lut = 0;
/** Lookup table for determining charger types */
static GHashTable *udevdevice_chargertype_lut = 0;
/** How to treat unknown properties; default to ignoring them */
static property_type_t udevproperty_type_def = PROPERTY_TYPE_IGNORE;
/** Properties that affect battery/charger evaluation
*
* If values for these properties change, battery state
* re-evaluation is triggered.
*
* @see #udevproperty_is_used()
*/
static const char * const udevproperty_used_keys[] = {
// common
PROP_PRESENT,
// charger
PROP_ONLINE,
PROP_REAL_TYPE,
PROP_TYPE,
// battery
PROP_CAPACITY,
PROP_STATUS,
NULL
};
/** Cached MCE_CONF_BATTERY_UDEV_REFRESH_ON_NOTIFY value */
static bool mcebat_refresh_on_notify = DEFAULT_BATTERY_UDEV_REFRESH_ON_NOTIFY;
/** Cached MCE_CONF_BATTERY_UDEV_REFRESH_ON_EXTCON value */
static bool mcebat_refresh_on_extcon = DEFAULT_BATTERY_UDEV_REFRESH_ON_EXTCON;
/** Cached MCE_CONF_BATTERY_UDEV_REFRESH_ON_HEARTBEAT value */
static bool mcebat_refresh_on_heartbeat = DEFAULT_BATTERY_UDEV_REFRESH_ON_HEARTBEAT;
/* ========================================================================= *
* CLIENT
* ========================================================================= */
#ifdef ENABLE_BATTERY_SIMULATION
/** Unregister battery simulation client
*
* When the last client is removed, actual battery/charger state is
* taken back to use.
*
* @param dbus_name Private D-Bus name of the client
*/
static void
mcebat_dbus_remove_client(const char *dbus_name)
{
gssize rc = mce_dbus_owner_monitor_remove(dbus_name,
&clients_monitor_list);
if( rc < 0 )
goto EXIT;
if( rc == 0 ) {
mce_log(LL_WARN, "client %s removed - stop simulation", dbus_name);
mcebat_update();
}
EXIT:
return;
}
/** D-Bus callback: A tracked client dropped from bus
*
* @param msg The D-Bus message
*
* @return TRUE
*/
static gboolean
mcebat_dbus_client_removed_cb(DBusMessage *const msg)
{
DBusError error = DBUS_ERROR_INIT;
const char *dbus_name = 0;
const char *old_owner = 0;
const char *new_owner = 0;
if( !dbus_message_get_args(msg, &error,
DBUS_TYPE_STRING, &dbus_name,
DBUS_TYPE_STRING, &old_owner,
DBUS_TYPE_STRING, &new_owner,
DBUS_TYPE_INVALID) ) {
mce_log(LL_ERR, "Failed to parse NameOwnerChanged: %s: %s",
error.name, error.message);
goto EXIT;
}
mcebat_dbus_remove_client(dbus_name);
EXIT:
dbus_error_free(&error);
return TRUE;
}
/** Register battery simulation client
*
* When the first client is registered, simulated battery state
* is synchronized with actual state.
*
* @param dbus_name Private D-Bus name of the client
*
* @return true if client was registered, false otherwise
*/
static bool
mcebat_dbus_add_client(const char *dbus_name)
{
bool ack = false;
gssize rc = mce_dbus_owner_monitor_add(dbus_name,
mcebat_dbus_client_removed_cb,
&clients_monitor_list,
CLIENTS_MONITOR_COUNT);
if( rc < 0 ) {
mce_log(LL_WARN, "client %s not added", dbus_name);
goto EXIT;
}
if( rc == 1 ) {
mce_log(LL_WARN, "client %s added - start simulation", dbus_name);
/* Note: Simulation starts from current state, so there
* is no need to re-evaluate immediately. */
mcebat_simulated = mcebat_datapipe;
}
ack = true;
EXIT:
return ack;
}
/** Evaluate simulated battery status
*
* Should be called whenever the simulation values controlled
* by clients change so that also derived values are re-evaluated.
*/
static void
mcebat_dbus_evaluate_battery_status(void)
{
/* Handle charger-connected special cases */
if( mcebat_simulated.charger_state == CHARGER_STATE_ON ) {
mcebat_simulated.battery_state = BATTERY_STATE_CHARGING;
if( mcebat_simulated.battery_level > 100 ) {
/* Battery full reached */
mcebat_simulated.battery_level = 100;
mcebat_simulated.battery_status = BATTERY_STATUS_FULL;
mcebat_simulated.battery_state = BATTERY_STATE_FULL;
goto EXIT;
}
if( mcebat_simulated.battery_status == BATTERY_STATUS_FULL &&
mcebat_simulated.battery_level >= BATTERY_CAPACITY_FULL ) {
/* Maintenance charging retains full status*/
goto EXIT;
}
if( mcebat_simulated.battery_level > BATTERY_CAPACITY_UNDEF ) {
/* Low/empty does not apply while charging */
mcebat_simulated.battery_status = BATTERY_STATUS_OK;
goto EXIT;
}
}
else {
mcebat_simulated.battery_state = BATTERY_STATE_DISCHARGING;
}
/* Evaluate based on battery level */
if( mcebat_simulated.battery_level <= BATTERY_CAPACITY_UNDEF )
mcebat_simulated.battery_status = BATTERY_STATUS_UNDEF;
else if( mcebat_simulated.battery_level <= BATTERY_CAPACITY_EMPTY )
mcebat_simulated.battery_status = BATTERY_STATUS_EMPTY;
else if( mcebat_simulated.battery_level <= BATTERY_CAPACITY_LOW )
mcebat_simulated.battery_status = BATTERY_STATUS_LOW;
else
mcebat_simulated.battery_status = BATTERY_STATUS_OK;
EXIT:
return;
}
/** D-Bus callback: Simulated charger type requested
*
* @param msg The D-Bus message
*
* @return TRUE
*/
static gboolean
mcebat_dbus_charger_type_req_cb(DBusMessage *const msg)
{
dbus_bool_t accepted = false;
const char *sender = dbus_message_get_sender(msg);
DBusError error = DBUS_ERROR_INIT;
const char *type = 0;
DBusMessage *reply = 0;
mce_log(LL_DEVEL, "charger type request from %s",
mce_dbus_get_name_owner_ident(sender));
if( !mcebat_dbus_add_client(sender) )
goto EXIT;
if( !dbus_message_get_args(msg, &error,
DBUS_TYPE_STRING, &type,
DBUS_TYPE_INVALID) ) {
goto EXIT;
}
if( !strcmp(type, MCE_CHARGER_TYPE_NONE) )
mcebat_simulated.charger_type = CHARGER_TYPE_NONE;
else if( !strcmp(type, MCE_CHARGER_TYPE_USB) )
mcebat_simulated.charger_type = CHARGER_TYPE_USB;
else if( !strcmp(type, MCE_CHARGER_TYPE_DCP) )
mcebat_simulated.charger_type = CHARGER_TYPE_DCP;
else if( !strcmp(type, MCE_CHARGER_TYPE_HVDCP) )
mcebat_simulated.charger_type = CHARGER_TYPE_HVDCP;
else if( !strcmp(type, MCE_CHARGER_TYPE_CDP) )
mcebat_simulated.charger_type = CHARGER_TYPE_CDP;
else if( !strcmp(type, MCE_CHARGER_TYPE_WIRELESS) )
mcebat_simulated.charger_type = CHARGER_TYPE_WIRELESS;
else
mcebat_simulated.charger_type = CHARGER_TYPE_OTHER;
mcebat_dbus_evaluate_battery_status();
mcebat_update();
accepted = true;
EXIT:
/* Setup the reply */
reply = dbus_new_method_reply(msg);
/* Append the result */
if( !dbus_message_append_args(reply,
DBUS_TYPE_BOOLEAN, &accepted,
DBUS_TYPE_INVALID)) {
mce_log(LL_ERR,"Failed to append reply arguments to D-Bus "
"message for %s.%s",
MCE_REQUEST_IF, dbus_message_get_member(msg));
}
else if( !dbus_message_get_no_reply(msg) ) {
dbus_send_message(reply), reply = 0;
}
if( reply )
dbus_message_unref(reply);
dbus_error_free(&error);
return TRUE;
}
/** D-Bus callback: Simulated charger state requested
*
* @param msg The D-Bus message
*
* @return TRUE
*/
static gboolean
mcebat_dbus_charger_state_req_cb(DBusMessage *const msg)
{
dbus_bool_t accepted = false;
const char *sender = dbus_message_get_sender(msg);
DBusError error = DBUS_ERROR_INIT;
const char *state = 0;
DBusMessage *reply = 0;
mce_log(LL_DEVEL, "charger state request from %s",
mce_dbus_get_name_owner_ident(sender));
if( !mcebat_dbus_add_client(sender) )
goto EXIT;
if( !dbus_message_get_args(msg, &error,
DBUS_TYPE_STRING, &state,
DBUS_TYPE_INVALID) ) {
goto EXIT;
}
if( !strcmp(state, MCE_CHARGER_STATE_ON) )
mcebat_simulated.charger_state = CHARGER_STATE_ON;
else if( !strcmp(state, MCE_CHARGER_STATE_OFF) )
mcebat_simulated.charger_state = CHARGER_STATE_OFF;
else
mcebat_simulated.charger_state = CHARGER_STATE_UNDEF;
mcebat_dbus_evaluate_battery_status();
mcebat_update();
accepted = true;
EXIT:
/* Setup the reply */
reply = dbus_new_method_reply(msg);
/* Append the result */
if( !dbus_message_append_args(reply,
DBUS_TYPE_BOOLEAN, &accepted,
DBUS_TYPE_INVALID)) {
mce_log(LL_ERR,"Failed to append reply arguments to D-Bus "
"message for %s.%s",
MCE_REQUEST_IF, dbus_message_get_member(msg));
}
else if( !dbus_message_get_no_reply(msg) ) {
dbus_send_message(reply), reply = 0;
}
if( reply )
dbus_message_unref(reply);
dbus_error_free(&error);
return TRUE;
}
/** D-Bus callback: Simulated charger state requested
*
* @param msg The D-Bus message
*
* @return TRUE
*/
static gboolean
mcebat_dbus_battery_level_req_cb(DBusMessage *const msg)
{
dbus_bool_t accepted = false;
const char *sender = dbus_message_get_sender(msg);
DBusError error = DBUS_ERROR_INIT;
dbus_int32_t level = 0;
DBusMessage *reply = 0;
mce_log(LL_DEVEL, "battery level request from %s",
mce_dbus_get_name_owner_ident(sender));
if( !mcebat_dbus_add_client(sender) )
goto EXIT;
if( !dbus_message_get_args(msg, &error,
DBUS_TYPE_INT32, &level,
DBUS_TYPE_INVALID) ) {
goto EXIT;
}
mcebat_simulated.battery_level = level;
mcebat_dbus_evaluate_battery_status();
mcebat_update();
accepted = true;
EXIT:
/* Setup the reply */
reply = dbus_new_method_reply(msg);
/* Append the result */
if( !dbus_message_append_args(reply,
DBUS_TYPE_BOOLEAN, &accepted,
DBUS_TYPE_INVALID)) {
mce_log(LL_ERR,"Failed to append reply arguments to D-Bus "
"message for %s.%s",
MCE_REQUEST_IF, dbus_message_get_member(msg));
}
else if( !dbus_message_get_no_reply(msg) ) {
dbus_send_message(reply), reply = 0;
}
if( reply )
dbus_message_unref(reply);
dbus_error_free(&error);
return TRUE;
}
#endif // ENABLE_BATTERY_SIMULATION
/** Array of dbus message handlers */
static mce_dbus_handler_t callstate_dbus_handlers[] =
{
/* method calls */
#ifdef ENABLE_BATTERY_SIMULATION
{
.interface = MCE_REQUEST_IF,
.name = MCE_CHARGER_TYPE_REQ,
.type = DBUS_MESSAGE_TYPE_METHOD_CALL,
.callback = mcebat_dbus_charger_type_req_cb,
.privileged = true,
.args =
" \n"
" \n"
},
{
.interface = MCE_REQUEST_IF,
.name = MCE_CHARGER_STATE_REQ,
.type = DBUS_MESSAGE_TYPE_METHOD_CALL,
.callback = mcebat_dbus_charger_state_req_cb,
.privileged = true,
.args =
" \n"
" \n"
},
{
.interface = MCE_REQUEST_IF,
.name = MCE_BATTERY_LEVEL_REQ,
.type = DBUS_MESSAGE_TYPE_METHOD_CALL,
.callback = mcebat_dbus_battery_level_req_cb,
.privileged = true,
.args =
" \n"
" \n"
},
#endif // ENABLE_BATTERY_SIMULATION
/* sentinel */
{
.interface = 0
}
};
/** Add dbus handlers
*/
static void mcebat_dbus_init(void)
{
mce_dbus_handler_register_array(callstate_dbus_handlers);
}
/** Remove dbus handlers
*/
static void mcebat_dbus_quit(void)
{
mce_dbus_handler_unregister_array(callstate_dbus_handlers);
#ifdef ENABLE_BATTERY_SIMULATION
/* Just release resources, do not re-evaluate state */
mce_dbus_owner_monitor_remove_all(&clients_monitor_list);
#endif
}
/* ========================================================================= *
* MCEBAT
* ========================================================================= */
/** Update battery state visible in datapipes
*
* @param curr Battery state data to expose.
*/
static void
mcebat_update(void)
{
const mcebat_t *curr = &mcebat_actual;
#ifdef ENABLE_BATTERY_SIMULATION
if( clients_monitor_list )
curr = &mcebat_simulated;
#endif
mcebat_t prev = mcebat_datapipe;
mcebat_datapipe = *curr;
if( prev.charger_type != curr->charger_type ) {
mce_log(LL_CRUCIAL, "charger_type: %s -> %s",
charger_type_repr(prev.charger_type),
charger_type_repr(curr->charger_type));
datapipe_exec_full(&charger_type_pipe,
GINT_TO_POINTER(curr->charger_type));
}
if( prev.charger_state != curr->charger_state ) {
mce_log(LL_CRUCIAL, "charger_state: %s -> %s",
charger_state_repr(prev.charger_state),
charger_state_repr(curr->charger_state));
/* Charger connected state */
datapipe_exec_full(&charger_state_pipe,
GINT_TO_POINTER(curr->charger_state));
/* Charging led pattern */
if( curr->charger_state == CHARGER_STATE_ON ) {
datapipe_exec_full(&led_pattern_activate_pipe,
MCE_LED_PATTERN_BATTERY_CHARGING);
}
else {
datapipe_exec_full(&led_pattern_deactivate_pipe,
MCE_LED_PATTERN_BATTERY_CHARGING);
}
/* Generate activity */
mce_datapipe_generate_activity();
}
if( prev.battery_state != curr->battery_state ) {
mce_log(LL_CRUCIAL, "battery_state: %s -> %s",
battery_state_repr(prev.battery_state),
battery_state_repr(curr->battery_state));
/* Battery charging state */
datapipe_exec_full(&battery_state_pipe,
GINT_TO_POINTER(curr->battery_state));
}
if( prev.battery_status != curr->battery_status ) {
mce_log(LL_CRUCIAL, "battery_status: %s -> %s",
battery_status_repr(prev.battery_status),
battery_status_repr(curr->battery_status));
/* Battery full led pattern */
if( curr->battery_status == BATTERY_STATUS_FULL ) {
datapipe_exec_full(&led_pattern_activate_pipe,
MCE_LED_PATTERN_BATTERY_FULL);
}
else {
datapipe_exec_full(&led_pattern_deactivate_pipe,
MCE_LED_PATTERN_BATTERY_FULL);
}
#if SUPPORT_BATTERY_LOW_LED_PATTERN
/* Battery low led pattern */
if( curr->battery_status == BATTERY_STATUS_LOW ||
curr->battery_status == BATTERY_STATUS_EMPTY ) {
datapipe_exec_full(&led_pattern_activate_pipe,
MCE_LED_PATTERN_BATTERY_LOW);
}
else {
datapipe_exec_full(&led_pattern_deactivate_pipe,
MCE_LED_PATTERN_BATTERY_LOW);
}
#endif /* SUPPORT_BATTERY_LOW_LED_PATTERN */
/* Battery charge state */
datapipe_exec_full(&battery_status_pipe,
GINT_TO_POINTER(curr->battery_status));
}
if( prev.battery_level != curr->battery_level ) {
mce_log(LL_CRUCIAL, "battery_level : %d -> %d",
prev.battery_level, curr->battery_level);
/* Battery charge percentage */
datapipe_exec_full(&battery_level_pipe,
GINT_TO_POINTER(curr->battery_level));
}
}
/* ========================================================================= *
* UDEVPROPERTY
* ========================================================================= */
/** Initialize device property classification lookup table
*/
static void
udevproperty_init_types(void)
{
static const char grp[] = MCE_CONF_BATTERY_UDEV_PROPERTY_BLACKLIST_GROUP;
if( udevproperty_type_lut )
goto EXIT;
udevproperty_type_lut = g_hash_table_new_full(g_str_hash, g_str_equal,
g_free, 0);
/* Deal with property blacklist configuration */
if( mce_conf_has_group(grp) ) {
/* Properties that are not listed in config group
* are treated as show-for-debugging-purposes.
*/
udevproperty_type_def = PROPERTY_TYPE_DEBUG;
gsize count = 0;
gchar **keys = mce_conf_get_keys(grp, &count);
for( gsize i = 0; i < count; ++i ) {
bool blacklisted = mce_conf_get_bool(grp, keys[i], true);
g_hash_table_replace(udevproperty_type_lut,
g_strdup(keys[i]),
GINT_TO_POINTER(blacklisted
? PROPERTY_TYPE_IGNORE
: PROPERTY_TYPE_DEBUG));
}
g_strfreev(keys);
}
/* Make sure that required properties are not blacklisted */
for( size_t i = 0; udevproperty_used_keys[i]; ++i ) {
g_hash_table_replace(udevproperty_type_lut,
g_strdup(udevproperty_used_keys[i]),
GINT_TO_POINTER(PROPERTY_TYPE_USED));
}
EXIT:
return;
}
/** Release device property classification lookup table
*/
static void
udevproperty_quit_types(void)
{
if( udevproperty_type_lut ) {
g_hash_table_unref(udevproperty_type_lut),
udevproperty_type_lut = 0;
}
}
/** Lookup device property classification
*
* @param key property name
*
* @return property classification
*/
static property_type_t
udevproperty_lookup_type(const char *key)
{
property_type_t type = PROPERTY_TYPE_IGNORE;
if( udevproperty_type_lut ) {
gpointer val = g_hash_table_lookup(udevproperty_type_lut, key);
type = GPOINTER_TO_INT(val);
}
return (type == PROPERTY_TYPE_UNDEF) ? udevproperty_type_def : type;
}
/* Predicate for: Property is needed for battery/charging evaluation
*
* @param key Property name
*
* @return true if property is used, false otherwise
*/
static bool
udevproperty_is_used(const char *key)
{
return udevproperty_lookup_type(key) == PROPERTY_TYPE_USED;
}
/* Predicate for: Property should not be cached
*
* @param key Property name
*
* @return true if property should be excluded, false otherwise
*/
static bool
udevproperty_is_ignored(const char *key)
{
return udevproperty_lookup_type(key) == PROPERTY_TYPE_IGNORE;
}
/** Create property value object
*
* @param dev Containing device
* @param key Property name
*
* @return property object
*/
static udevproperty_t *
udevproperty_create(udevdevice_t *dev, const char *key)
{
udevproperty_t *self = g_malloc0(sizeof *self);
self->udp_dev = dev;
self->udp_key = g_strdup(key);
self->udp_val = 0;
self->udp_used = udevproperty_is_used(key);
return self;
}
/** Delete property value object
*
* @param self property object, or NULL
*/
static void
udevproperty_delete(udevproperty_t *self)
{
if( self != 0 ) {
g_free(self->udp_key);
g_free(self->udp_val);
g_free(self);
}
}
/** Type agnostic callback for deleting value objects
*
* @param self property object, or NULL
*/
static void
udevproperty_delete_cb(void *self)
{
udevproperty_delete(self);
}
/** Get property name
*
* @param self property object
*
* @return property name
*/
static const char *
udevproperty_key(const udevproperty_t *self)
{
return self->udp_key;
}
/** Get property value
*
* @param self property object
*
* @return property value
*/
static const char *
udevproperty_get(const udevproperty_t *self)
{
return self->udp_val;
}
/** Set property value
*
* @param self property object
* @param val property value
*
* @return true if value was changed and is used for state evalueation,
* false otherwise
*/
static bool
udevproperty_set(udevproperty_t *self, const char *val)
{
bool rethink = false;
gchar *prev = self->udp_val;
if( g_strcmp0(prev, val) ) {
rethink = self->udp_used;
mce_log(LL_DEBUG, "%s.%s : %s -> %s%s",
udevdevice_name(self->udp_dev),
udevproperty_key(self), prev, val,
rethink ? "" : " (ignored)");
self->udp_val = val ? g_strdup(val) : 0;
g_free(prev);
}
return rethink;
}
/* ========================================================================= *
* UDEVDEVICE
* ========================================================================= */
/** Lookup mce battery state based on udev battery status property value
*
* @param status udev battery status
*
* @return battery_state_t enumeration value
*/
static battery_state_t
udevdevice_lookup_battery_state(const char *status)
{
battery_state_t state = BATTERY_STATE_UNKNOWN;
if( !g_strcmp0(status, "Charging") )
state = BATTERY_STATE_CHARGING;
else if( !g_strcmp0(status, "Discharging") )
state = BATTERY_STATE_DISCHARGING;
else if( !g_strcmp0(status, "Not charging") )
state = BATTERY_STATE_NOT_CHARGING;
else if( !g_strcmp0(status, "Full") )
state = BATTERY_STATE_FULL;
else if( g_strcmp0(status, "Unknown") )
mce_log(LL_WARN, "unrecognized power supply state '%s'", status);
return state;
}
/** Lookup charger type based on device name / value of type property
*
* @param name string to match
*
* @return charger_type_t enumeration value
*/
static charger_type_t
udevdevice_lookup_charger_type(const char *name)
{
charger_type_t type = CHARGER_TYPE_INVALID;
gchar *key = 0;
gpointer val;
if( !name || !udevdevice_chargertype_lut )
goto EXIT;
key = g_ascii_strdown(name, -1);
/* Try exact match 1st, then relaxed one which equates
* "chipname-ac" with plain "ac".
*/
val = g_hash_table_lookup(udevdevice_chargertype_lut, key);
if( !val ) {
const char *end = strrchr(key, '-');
if( end )
val = g_hash_table_lookup(udevdevice_chargertype_lut, end + 1);
}
type = GPOINTER_TO_INT(val);
EXIT:
if( type == CHARGER_TYPE_INVALID ) {
mce_log(LL_WARN, "unknown charger type: %s", name ?: "null");
type = CHARGER_TYPE_OTHER;
}
g_free(key);
mce_log(LL_DEBUG, "charger type: %s -> %s", name ?: "null", charger_type_repr(type));
return type;
}
static void
udevdevice_init_chargertype(void)
{
static const struct {
const char *name;
charger_type_t type;
} lut[] = {
/* Type map - adapted from statefs sources
*/
{ "CDP", CHARGER_TYPE_CDP },
{ "USB_CDP", CHARGER_TYPE_CDP },
{ "USB_DCP", CHARGER_TYPE_DCP },
{ "USB_HVDCP", CHARGER_TYPE_HVDCP },
{ "USB_HVDCP_3", CHARGER_TYPE_HVDCP },
{ "Mains", CHARGER_TYPE_DCP },
{ "USB", CHARGER_TYPE_USB },
{ "USB_ACA", CHARGER_TYPE_USB },
/* Additions since leaving statefs behind
*/
{ "WIRELESS", CHARGER_TYPE_WIRELESS },
{ "AC", CHARGER_TYPE_DCP },
/* To make connect/disconnect transitions
* cleaner, ignore "Unknown" reporting
*/
{ "Unknown", CHARGER_TYPE_NONE },
/* Sentinel */
{ 0, 0 }
};
static const char grp[] = MCE_CONF_BATTERY_UDEV_DEVICE_CHARGERTYPE_GROUP;
if( udevdevice_chargertype_lut )
goto EXIT;
udevdevice_chargertype_lut =
g_hash_table_new_full(g_str_hash, g_str_equal, g_free, 0);
/* Seed with built-in values */
for( size_t i = 0; lut[i].name; ++i ) {
g_hash_table_insert(udevdevice_chargertype_lut,
g_ascii_strdown(lut[i].name, -1),
GINT_TO_POINTER(lut[i].type));
}
#if 0
/* Dump as ini-file for use as an example */
{
GHashTableIter iter;
gpointer key, val;
g_hash_table_iter_init(&iter, udevdevice_chargertype_lut);
printf("[%s]\n", grp);
while ( g_hash_table_iter_next (&iter, &key, &val) )
printf("#%s = %s\n", key, charger_type_repr(GPOINTER_TO_INT(val)));
}
#endif
/* Override with configuration */
if( mce_conf_has_group(grp) ) {
mce_log(LL_DEBUG, "using configured chargertypes");
gsize count = 0;
gchar **keys = mce_conf_get_keys(grp, &count);
for( gsize i = 0; i < count; ++i ) {
const gchar *name = keys[i];
gchar *value = mce_conf_get_string(grp, name, 0);
charger_type_t type = charger_type_parse(value);
if( type != CHARGER_TYPE_INVALID ) {
g_hash_table_insert(udevdevice_chargertype_lut,
g_ascii_strdown(name, -1),
GINT_TO_POINTER(type));
}
g_free(value);
}
g_strfreev(keys);
}
EXIT:
return;
}
/** Release device chargertype lookup table
*/
static void
udevdevice_quit_chargertype(void)
{
if( udevdevice_chargertype_lut ) {
g_hash_table_unref(udevdevice_chargertype_lut),
udevdevice_chargertype_lut = 0;
}
}
/** Initialize device blacklist lookup table
*/
static void
udevdevice_init_blacklist(void)
{
static const char grp[] = MCE_CONF_BATTERY_UDEV_DEVICE_BLACKLIST_GROUP;
static const char * const builtin_blacklist[] = {
"bcl",
"bms",
"dc",
"fg_adc",
"main",
"parallel",
"pc_port",
"pm8921-dc",
0
};
if( udevdevice_blacklist_lut )
goto EXIT;
udevdevice_blacklist_lut =
g_hash_table_new_full(g_str_hash, g_str_equal, g_free, 0);
if( mce_conf_has_group(grp) ) {
mce_log(LL_DEBUG, "using configured device blacklist");
gsize count = 0;
gchar **keys = mce_conf_get_keys(grp, &count);
for( gsize i = 0; i < count; ++i ) {
bool blacklisted = mce_conf_get_bool(grp, keys[i], true);
if( blacklisted ) {
g_hash_table_replace(udevdevice_blacklist_lut,
g_strdup(keys[i]),
GINT_TO_POINTER(true));
}
}
g_strfreev(keys);
}
else {
mce_log(LL_DEBUG, "using built-in device blacklist");
for( size_t i = 0; builtin_blacklist[i]; ++i ) {
g_hash_table_replace(udevdevice_blacklist_lut,
g_strdup(builtin_blacklist[i]),
GINT_TO_POINTER(true));
}
}
EXIT:
return;
}
/** Release device blacklist lookup table
*/
static void
udevdevice_quit_blacklist(void)
{
if( udevdevice_blacklist_lut ) {
g_hash_table_unref(udevdevice_blacklist_lut),
udevdevice_blacklist_lut = 0;
}
}
/** Check if device is blacklisted
*
* @param name device sysname
*
* @return true if device is blacklisted, false otherwise
*/
static bool
udevdevice_is_blacklisted(const char *name)
{
bool blacklisted = false;
if( udevdevice_blacklist_lut ) {
gpointer val = g_hash_table_lookup(udevdevice_blacklist_lut, name);
blacklisted = GPOINTER_TO_INT(val);
}
return blacklisted;
}
/** Create device object
*
* @param name device sysname
*
* @return device object
*/
static udevdevice_t *
udevdevice_create(const char *name)
{
udevdevice_t *self = g_malloc0(sizeof *self);
self->udd_name = g_strdup(name);
self->udd_props = g_hash_table_new_full(g_str_hash,
g_str_equal,
g_free,
udevproperty_delete_cb);
self->udd_full = false;
self->udd_charging = false;
return self;
}
/** Delete device object
*
* @param self device object, or NULL
*/
static void
udevdevice_delete(udevdevice_t *self)
{
if( self != 0 ) {
g_hash_table_unref(self->udd_props);
g_free(self->udd_name);
g_free(self);
}
}
/** Type agnostic callback for deleting device objects
*
* @param self device object, or NULL
*/
static void
udevdevice_delete_cb(void *self)
{
udevdevice_delete(self);
}
/** Get device object name
*
* @param self device object
*
* @return device sysname
*/
static const char *
udevdevice_name(const udevdevice_t *self)
{
return self->udd_name;
}
/** Get device object property
*
* @param self device object
* @param key property name
*
* @return property object, or NULL if not found
*/
static udevproperty_t *
udevdevice_get_prop(udevdevice_t *self, const char *key)
{
udevproperty_t *prop = g_hash_table_lookup(self->udd_props, key);
return prop;
}
/** Add device object property
*
* @param self device object
* @param key property name
*
* @return property object
*/
static udevproperty_t *
udevdevice_add_prop(udevdevice_t *self, const char *key)
{
udevproperty_t *prop = udevdevice_get_prop(self, key);
if( !prop ) {
prop = udevproperty_create(self, key);
g_hash_table_replace(self->udd_props,
g_strdup(key),
prop);
}
return prop;
}
/** Set device object property value
*
* @param self device object
* @param key property name
* @param val property value
*
* @return true if battery state should be re-evaluated, false otherwise
*/
static bool
udevdevice_set_prop(udevdevice_t *self, const char *key, const char *val)
{
udevproperty_t *prop = udevdevice_add_prop(self, key);
bool rethink = udevproperty_set(prop, val);
return rethink;
}
/** Get device object property value as string
*
* @param self device object
* @param key property name
* @param def fallback value to return if property does not exist
*
* @return property value, or the given fallback values
*/
static const char *
udevdevice_get_str_prop(udevdevice_t *self, const char *key, const char *def)
{
const char *val = 0;
udevproperty_t *prop = udevdevice_get_prop(self, key);
if( prop )
val = udevproperty_get(prop);
return val ?: def;
}
/** Get device object property value as integer
*
* @param self device object
* @param key property name
* @param def fallback value to return if property does not exist
*
* @return property value, or the given fallback values
*/
static int
udevdevice_get_int_prop(udevdevice_t *self, const char *key, int def)
{
const char *val = udevdevice_get_str_prop(self, key, 0);
return val ? strtol(val, 0, 0) : def;
}
/** Update device object properties
*
* @param self device object
* @param dev udev device object
*
* @return true if battery state should be re-evaluated, false otherwise
*/
static bool
udevdevice_refresh(udevdevice_t *self, struct udev_device *dev)
{
bool rethink = false;
for( struct udev_list_entry *iter =
udev_device_get_properties_list_entry(dev);
iter; iter = udev_list_entry_get_next(iter) ) {
const char *key = udev_list_entry_get_name(iter);
if( udevproperty_is_ignored(key) )
continue;
const char *val = udev_list_entry_get_value(iter);
if( udevdevice_set_prop(self, key, val) )
rethink = true;
}
return rethink;
}
/** Predicate for: power_supply device is a battery
*
* @param self device object
*
* @return true device is a battery, false otherwise
*/
static bool
udevdevice_is_battery(udevdevice_t *self)
{
return (udevdevice_get_prop(self, PROP_STATUS) &&
udevdevice_get_prop(self, PROP_CAPACITY));
}
/** Predicate for: power_supply device is a charger
*
* @param self device object
*
* @return true device is a charger, false otherwise
*/
static bool
udevdevice_is_charger(udevdevice_t *self)
{
if( udevdevice_is_battery(self) )
return false;
return (udevdevice_get_prop(self, PROP_PRESENT) ||
udevdevice_get_prop(self, PROP_ONLINE));
}
/** Update mce style battery data based on device properties
*
* @param self device object
* @param mcebat mce style battery data to update
*/
static void
udevdevice_evaluate_charger(udevdevice_t *self, mcebat_t *mcebat)
{
if( !udevdevice_is_charger(self) )
goto EXIT;
int present = udevdevice_get_int_prop(self, PROP_PRESENT, -1);
int online = udevdevice_get_int_prop(self, PROP_ONLINE, -1);
/* Device is a charger.
*
* Whatever the meaning of present / online properties
* is supposed to be, the best guess we can make is that
* we ought to be able to charge when either one gets
* non-zero value ... */
bool active = (present == 1 || online == 1);
if( active ) {
mcebat->charger_state = CHARGER_STATE_ON;
/* Charger is online, evaluate charger type
*
* Legacy QC devices have TYPE property that has
* content sfos sw stack knows how to interpret.
*
* More recent QC devices might expose "USB_PD" in
* TYPE prop and have additional REAL_TYPE property
* containing old style data.
*
* MTK devices have multiple power supply device
* nodes visible in udev and charger type must be
* determined from device node name.
*/
const char *name;
if( !(name = udevdevice_get_str_prop(self, PROP_REAL_TYPE, 0)) ) {
if( !(name = udevdevice_get_str_prop(self, PROP_TYPE, 0)) )
name = udevdevice_name(self);
}
charger_type_t type = udevdevice_lookup_charger_type(name);
/* Update effective charger type exposed on D-Bus
*/
if( mcebat->charger_type < type )
mcebat->charger_type = type;
}
mce_log(LL_DEBUG, "%s: charger @ "
"present=%d online=%d -> active=%d",
udevdevice_name(self),
present, online, active);
EXIT:
return;
}
/** g_hash_table_foreach() compatible udevdevice_evaluate_charger() wrapper callback
*
* @param key (unused) device sysname as void pointer
* @param value device object as void pointer
* @param aptr mce battery data object as void pointer
*/
static void
udevdevice_evaluate_charger_cb(gpointer key, gpointer value, gpointer aptr)
{
(void)key;
mcebat_t *mcebat = aptr;
udevdevice_t *self = value;
udevdevice_evaluate_charger(self, mcebat);
}
/** Update mce style battery data based on device properties
*
* @param self device object
* @param mcebat mce style battery data to update
*/
static void
udevdevice_evaluate_battery(udevdevice_t *self, mcebat_t *mcebat)
{
if( !udevdevice_is_battery(self) )
goto EXIT;
/* Device is a battery.
*
* FIXME: There is a built-in assumption that there will be only
* one battery device - if there should be more than one,
* then the one that happens to be the last to be seen
* during g_hash_table_foreach() ends up being used.
*/
int capacity = udevdevice_get_int_prop(self, PROP_CAPACITY, -1);
const char *status = udevdevice_get_str_prop(self, PROP_STATUS, 0);
/* mce level is udev capacity as-is
*/
mcebat->battery_level = capacity;
/* mce status is by default derived from udev capacity
*/
if( capacity <= BATTERY_CAPACITY_UNDEF )
mcebat->battery_status = BATTERY_STATUS_UNDEF;
else if( capacity <= BATTERY_CAPACITY_EMPTY )
mcebat->battery_status = BATTERY_STATUS_EMPTY;
else if( capacity <= BATTERY_CAPACITY_LOW )
mcebat->battery_status = BATTERY_STATUS_LOW;
else
mcebat->battery_status = BATTERY_STATUS_OK;
/* udev status is "Unknown|Charging|Discharging|Not charging|Full" */
mcebat->battery_state = udevdevice_lookup_battery_state(status);
/* "Charging" and "Full" override capacity based mce battery status
* evaluation above.
*
* How maintenance charging is reported after hitting battery
* full varies from one device to another. To normalize behavior
* and avoid repeated charging started notifications sequences
* like "Full"->"Charging"->"Full"->... are compressed into
* a single "Full" (untill charger is disconnected / battery level
* makes significant enough drop).
*
* Also if battery device indicates that it is getting charged,
* assume that a charger is connected.
*/
if( mcebat->battery_state == BATTERY_STATE_FULL ) {
mcebat->charger_state = CHARGER_STATE_ON;
mcebat->battery_status = BATTERY_STATUS_FULL;
self->udd_full = true;
}
else if( mcebat->battery_state == BATTERY_STATE_CHARGING ) {
mcebat->charger_state = CHARGER_STATE_ON;
mcebat->battery_status = BATTERY_STATUS_OK;
if( self->udd_full && capacity >= BATTERY_CAPACITY_FULL )
mcebat->battery_status = BATTERY_STATUS_FULL;
else
self->udd_full = false;
}
/* Some devices go:
* Charging -> Full -> Discharging -> Charging -> Full
*
* Others might go:
* Charging -> Not charging -> Charging -> Not charging
*
* Use heuristics to normalize such things to battery full too.
*/
else if( mcebat->charger_state == CHARGER_STATE_ON &&
capacity >= BATTERY_CAPACITY_FULL &&
(self->udd_full || self->udd_charging) ) {
mcebat->battery_status = BATTERY_STATUS_FULL;
if( !self->udd_full ) {
mce_log(LL_WARN, "assuming end of charging due to battery full");
self->udd_full = true;
}
}
else {
self->udd_full = false;
}
/* Override udev status on heuristically determined battery full */
if( mcebat->battery_status == BATTERY_STATUS_FULL )
mcebat->battery_state = BATTERY_STATE_FULL;
mce_log(LL_DEBUG, "%s: battery @ cap=%d status=%s full=%d",
udevdevice_name(self), capacity, status, self->udd_full);
self->udd_charging = !g_strcmp0(status, "Charging");
EXIT:
return;
}
/** g_hash_table_foreach() compatible udevdevice_evaluate_battery() wrapper callback
*
* @param key (unused) device sysname as void pointer
* @param value device object as void pointer
* @param aptr mce battery data object as void pointer
*/
static void
udevdevice_evaluate_battery_cb(gpointer key, gpointer value, gpointer aptr)
{
(void)key;
mcebat_t *mcebat = aptr;
udevdevice_t *self = value;
udevdevice_evaluate_battery(self, mcebat);
}
/* ========================================================================= *
* UDEVEXTCON
* ========================================================================= */
/** Subsystem name for extcon devices */
static const char udevextcon_subsystem[] = "extcon";
/** USB state cache for extcon devices */
static GHashTable *udevextcon_usb_state_lut = NULL;
/** Parse USB entry from extcon state information
*
* When non-null value is returned, caller must release it with g_free().
*
* @param state Full state data for extcon device
*
* @return value for USB entry, or NULL
*/
static gchar *
udevextcon_parse_usb_state(const char *state)
{
gchar *res = NULL;
gchar *tmp = NULL;
if( (tmp = g_strdup(state)) ) {
for( char *pos = tmp; *pos; ) {
char *val = mce_slice_token(pos, &pos, NULL);
char *key = mce_slice_token(val, &val, "=");
if( !strcmp(key, "USB") ) {
res = g_strdup(val);
break;
}
}
g_free(tmp);
}
return res;
}
/** Update cached USB state for an extcon device
*
* @param syspath syspath for device to update
* @param state state reported for the device
*
* @return true if cached value changed, false otherwise
*/
static bool
udevextcon_update_state(const char *syspath, const char *state)
{
bool changed = false;
gchar *usb_state = NULL;
const gchar *old_state = NULL;
if( !syspath || !udevextcon_usb_state_lut )
goto EXIT;
usb_state = udevextcon_parse_usb_state(state);
old_state = g_hash_table_lookup(udevextcon_usb_state_lut, syspath);
if( g_strcmp0(old_state, usb_state) ) {
mce_log(LL_DEBUG, "%s.STATE / USB: %s -> %s",
basename(syspath), old_state ?: "null", usb_state ?: "null");
if( usb_state )
g_hash_table_replace(udevextcon_usb_state_lut, g_strdup(syspath),
usb_state), usb_state = NULL;
else
g_hash_table_remove(udevextcon_usb_state_lut, syspath);
changed = true;
}
EXIT:
g_free(usb_state);
return changed;
}
/** Initialize cached USB state for an extcon device
*
* @param device device to initialize from
*/
static void
udevextcon_initialize_from(struct udev_device *dev)
{
const char *syspath = udev_device_get_syspath(dev);
const char *state = udev_device_get_sysattr_value(dev, "state");
udevextcon_update_state(syspath, state);
}
/** Update cached USB state for an extcon device
*
* @param device device to update from
*
* @return true if cached value changed, false otherwise
*/
static bool
udevextcon_update_from(struct udev_device *dev)
{
const char *syspath = udev_device_get_syspath(dev);
const char *state = udev_device_get_property_value(dev, "STATE");
return udevextcon_update_state(syspath, state);
}
/** Initialize USB state cache for extcon devices
*/
static void
udevextcon_init(void)
{
if( mcebat_refresh_on_extcon && !udevextcon_usb_state_lut ) {
udevextcon_usb_state_lut = g_hash_table_new_full(g_str_hash,
g_str_equal,
g_free,
g_free);
}
}
/** Release USB state cache for extcon devices
*/
static void
udevextcon_quit(void)
{
if( udevextcon_usb_state_lut ) {
g_hash_table_unref(udevextcon_usb_state_lut),
udevextcon_usb_state_lut = NULL;
}
}
/* ========================================================================= *
* UDEVTRACKER
* ========================================================================= */
/** Create udev power supply device tracking object
*
* @return tracker object
*/
static udevtracker_t *
udevtracker_create(void)
{
udevtracker_t *self = g_malloc0(sizeof *self);
self->udt_udev_handle = 0;
self->udt_udev_monitor = 0;
self->udt_udev_event_id = 0;
self->udt_rethink_id = 0;
self->udt_devices = g_hash_table_new_full(g_str_hash,
g_str_equal,
g_free,
udevdevice_delete_cb);
return self;
}
/** Delete tracking object
*
* @param self tracker object, or NULL
*/
static void
udevtracker_delete(udevtracker_t *self)
{
if( self != 0 ) {
/* Detach from udev notifications */
udevtracker_stop(self);
/* Flush cached device data */
g_hash_table_unref(self->udt_devices),
self->udt_devices = 0;
/* Cancel pending state re-evaluation */
udevtracker_cancel_rethink(self);
g_free(self);
}
}
/** Update mce battery state according to tracked udev state
*
* @param self tracker object
*/
static void
udevtracker_rethink(udevtracker_t *self)
{
udevtracker_cancel_rethink(self);
/* Give charger_state special treatment: Assume charger is
* disconnect & rectify if any of the battery/charger devices
* indicate charging activity. */
mcebat_actual.charger_state = CHARGER_STATE_OFF;
/* Reset charger type, iterator chooses maximum of
* none < other < wall chargers < pc connection. */
mcebat_actual.charger_type = CHARGER_TYPE_NONE;
g_hash_table_foreach(self->udt_devices, udevdevice_evaluate_charger_cb, &mcebat_actual);
g_hash_table_foreach(self->udt_devices, udevdevice_evaluate_battery_cb, &mcebat_actual);
/* Sync to datapipes */
mcebat_update();
}
/** Timer callback for delayed battery state evaluation
*
* @param aptr tracker object as void pointer
* @return G_SOURCE_REMOVE to stop timer from repeating
*/
static gboolean
udevtracker_rethink_cb(gpointer aptr)
{
udevtracker_t *self = aptr;
mce_log(LL_DEBUG, "battery state re-evaluation triggered");
self->udt_rethink_id = 0;
udevtracker_rethink(self);
return G_SOURCE_REMOVE;
}
/** Shedule delayed battery state evaluation
*
* @param self tracker object
*/
static void
udevtracker_cancel_rethink(udevtracker_t *self)
{
if( self->udt_rethink_id ) {
mce_log(LL_DEBUG, "battery state re-evaluation canceled");
g_source_remove(self->udt_rethink_id), self->udt_rethink_id = 0;
}
}
/** Cancle delayed battery state evaluation
*
* @param self tracker object
*/
static void
udevtracker_schedule_rethink(udevtracker_t *self)
{
if( !self->udt_rethink_id ) {
self->udt_rethink_id =
mce_wakelocked_timeout_add(BATTERY_REEVALUATE_DELAY,
udevtracker_rethink_cb,
self);
mce_log(LL_DEBUG, "battery state re-evaluation sheduled");
}
}
/** Add device object to track
*
* @param self tracker object
* @param path device syspath
* @param name device sysname
*
* @return device object
*/
static udevdevice_t *
udevtracker_add_dev(udevtracker_t *self, const char *path, const char *name)
{
udevdevice_t *dev = g_hash_table_lookup(self->udt_devices, path);
if( !dev ) {
dev = udevdevice_create(name);
g_hash_table_replace(self->udt_devices, g_strdup(path), dev);
}
return dev;
}
/** Update properties of tracked device
*
* @param self tracker object
* @param dev udev device object
*
* @return true if device is used, or false if ignored
*/
static bool
udevtracker_update_device(udevtracker_t *self, struct udev_device *dev)
{
bool rethink = false;
/* TODO: Currently it is assumed that we receive only
* "add" or "change" notifications for power
* supply devices after the initial enumeration.
*/
const char *sysname = udev_device_get_sysname(dev);
const char *syspath = udev_device_get_syspath(dev);
const char *action = udev_device_get_action(dev);
if( udevdevice_is_blacklisted(sysname) ) {
/* Report blacklisted devices during initial enumeration */
if( !action )
mce_log(LL_DEBUG, "%s: is blacklisted", sysname);
goto EXIT;
}
udevdevice_t *powerdev = udevtracker_add_dev(self, syspath, sysname);
if( (rethink = udevdevice_refresh(powerdev, dev)) )
udevtracker_schedule_rethink(self);
EXIT:
return rethink;
}
/** Start udev device tracking
*
* @param self tracker object
*
* @return true if tracking was successfully started, false otherwise
*/
static bool
udevtracker_start(udevtracker_t *self)
{
struct udev_enumerate *udev_enum = 0;
/* Already started? */
if( self->udt_udev_event_id )
goto EXIT;
/* Make sure we start from clean state */
udevtracker_stop(self);
if( !(self->udt_udev_handle = udev_new()) )
goto EXIT;
/* Scan initial state */
mce_log(LL_DEBUG, "ENTER - get initial state");
udev_enum = udev_enumerate_new(self->udt_udev_handle);
udev_enumerate_add_match_subsystem(udev_enum, udevtracker_subsystem);
if( mcebat_refresh_on_extcon )
udev_enumerate_add_match_subsystem(udev_enum, udevextcon_subsystem);
udev_enumerate_scan_devices(udev_enum);
for( struct udev_list_entry *iter =
udev_enumerate_get_list_entry(udev_enum);
iter ; iter = udev_list_entry_get_next(iter) ) {
const char *path = udev_list_entry_get_name(iter);
struct udev_device *dev =
udev_device_new_from_syspath(self->udt_udev_handle, path);
if( dev ) {
const char *subsystem = udev_device_get_subsystem(dev);
if( !g_strcmp0(subsystem, udevtracker_subsystem) )
udevtracker_update_device(self, dev);
else if( !g_strcmp0(subsystem, udevextcon_subsystem) )
udevextcon_initialize_from(dev);
udev_device_unref(dev);
}
}
mce_log(LL_DEBUG, "LEAVE - get initial state");
/* Monitor changes */
self->udt_udev_monitor =
udev_monitor_new_from_netlink(self->udt_udev_handle, "udev");
udev_monitor_filter_add_match_subsystem_devtype(self->udt_udev_monitor,
udevtracker_subsystem, 0);
if( mcebat_refresh_on_extcon )
udev_monitor_filter_add_match_subsystem_devtype(self->udt_udev_monitor,
udevextcon_subsystem,
0);
udev_monitor_enable_receiving(self->udt_udev_monitor);
int fd = udev_monitor_get_fd(self->udt_udev_monitor);
if( fd == -1 )
goto EXIT;
self->udt_udev_event_id = mce_io_add_watch(fd, false, G_IO_IN,
udevtracker_event_cb, self);
EXIT:
if( udev_enum )
udev_enumerate_unref(udev_enum);
if( !self->udt_udev_event_id )
udevtracker_stop(self);
return self->udt_udev_event_id != 0;
}
/** Stop udev device tracking
*
* @param self tracker object
*/
static void
udevtracker_stop(udevtracker_t *self)
{
if( self->udt_udev_event_id )
g_source_remove(self->udt_udev_event_id), self->udt_udev_event_id = 0;
if( self->udt_udev_monitor )
udev_monitor_unref(self->udt_udev_monitor), self->udt_udev_monitor = 0;
if( self->udt_udev_handle )
udev_unref(self->udt_udev_handle), self->udt_udev_handle = 0;
}
/** I/O callback for receiving udev device changed notifications
*
* @param chn (unused) glib io channel
* @param cnd reason for invoking the callback
* @param aptr tracker object as void pointer
*
* return G_SOURCE_CONTINUE to keep I/O watch alive, or
* G_SOURCE_REMOVE to disable it
*/
static gboolean
udevtracker_event_cb(GIOChannel *chn, GIOCondition cnd, gpointer aptr)
{
(void)chn;
/* Deny suspending while handling timer wakeup */
mce_wakelock_obtain(udevtracker_wakelock, -1);
mce_log(LL_DEBUG, "ENTER - udev notification");
gboolean result = G_SOURCE_REMOVE;
udevtracker_t *self = aptr;
if( self->udt_udev_event_id == 0 ) {
mce_log(LL_WARN, "stray udev wakeup");
goto EXIT;
}
if( cnd & ~G_IO_IN ) {
mce_log(LL_CRIT, "unexpected udev wakeup: %s",
mce_io_condition_repr(cnd));
goto EXIT;
}
struct udev_device *dev =
udev_monitor_receive_device(self->udt_udev_monitor);
if( dev ) {
const char *subsystem = udev_device_get_subsystem(dev);
if( !g_strcmp0(subsystem, udevtracker_subsystem) ) {
bool changed = udevtracker_update_device(self, dev);
if( changed && mcebat_refresh_on_notify )
udevtracker_schedule_refresh();
}
else if( !g_strcmp0(subsystem, udevextcon_subsystem) ) {
bool changed = udevextcon_update_from(dev);
if( changed && mcebat_refresh_on_extcon )
udevtracker_schedule_refresh();
}
udev_device_unref(dev);
}
result = G_SOURCE_CONTINUE;
EXIT:
if( result != G_SOURCE_CONTINUE && self->udt_udev_event_id != 0 ) {
mce_log(LL_CRIT, "disabling udev io watch");
self->udt_udev_event_id = 0;
udevtracker_stop(self);
}
mce_log(LL_DEBUG, "LEAVE - udev notification");
mce_wakelock_release(udevtracker_wakelock);
return result;
}
static void
udevtracker_refresh_all(udevtracker_t *self)
{
/* Doing it now, cancel delayed refresh */
udevtracker_cancel_refresh();
/* Operate on copy of keys just in case the hash
* table should change due to changes made from here.
*/
GList *syspaths = g_hash_table_get_keys(self->udt_devices);
for( GList *iter = syspaths; iter; iter = iter->next ) {
const gchar *syspath = iter->data;
iter->data = g_strdup(syspath);
}
/* Assumption based on taking a peek at libudev code:
* properties for freshly created udev_device are
* populated by reading appropriate uevent file and
* thus are not something that would be cached at
* libudev level -> we get current data from kernel.
*/
for( GList *iter = syspaths; iter; iter = iter->next ) {
const gchar *syspath = iter->data;
struct udev_device *dev =
udev_device_new_from_syspath(self->udt_udev_handle, syspath);
if( dev ) {
udevtracker_update_device(self, dev);
udev_device_unref(dev);
}
}
g_list_free_full(syspaths, g_free);
}
static guint udevtracker_refresh_id = 0;
static gboolean udevtracker_refresh_cb(gpointer aptr)
{
(void)aptr;
if( udevtracker_refresh_id ) {
udevtracker_refresh_id = 0;
mce_log(LL_DEBUG, "ENTER - refresh on notify");
if( udevtracker_object )
udevtracker_refresh_all(udevtracker_object);
mce_log(LL_DEBUG, "LEAVE - refresh on notify");
}
return G_SOURCE_REMOVE;
}
static void udevtracker_schedule_refresh(void)
{
if( !udevtracker_refresh_id )
mce_log(LL_DEBUG, "forced value refresh scheduled");
else
g_source_remove(udevtracker_refresh_id);
udevtracker_refresh_id =
mce_wakelocked_timeout_add(DEVICES_REFRESH_DELAY,
udevtracker_refresh_cb, 0);
}
static void udevtracker_cancel_refresh(void)
{
if( udevtracker_refresh_id ) {
mce_log(LL_DEBUG, "forced value refresh cancelled");
g_source_remove(udevtracker_refresh_id),
udevtracker_refresh_id = 0;
}
}
/* ========================================================================= *
* DATAPIPE_HANDLERS
* ========================================================================= */
/** Change notifications for heartbeat_event_pipe
*
* @param data (unused dummy parameter)
*/
static void mcebat_datapipe_heartbeat_event_cb(gconstpointer data)
{
(void)data;
mce_log(LL_DEBUG, "ENTER - refresh on heartbeat");
if( mcebat_refresh_on_heartbeat && udevtracker_object )
udevtracker_refresh_all(udevtracker_object);
mce_log(LL_DEBUG, "LEAVE - refresh on heartbeat");
}
/** Array of datapipe handlers */
static datapipe_handler_t mcebat_datapipe_handlers[] =
{
// output triggers
{
.datapipe = &heartbeat_event_pipe,
.output_cb = mcebat_datapipe_heartbeat_event_cb,
},
// sentinel
{
.datapipe = 0,
}
};
static datapipe_bindings_t mcebat_datapipe_bindings =
{
.module = "battery_udev",
.handlers = mcebat_datapipe_handlers,
};
/** Append triggers/filters to datapipes
*/
static void mcebat_datapipe_init(void)
{
mce_datapipe_init_bindings(&mcebat_datapipe_bindings);
}
/** Remove triggers/filters from datapipes
*/
static void mcebat_datapipe_quit(void)
{
mce_datapipe_quit_bindings(&mcebat_datapipe_bindings);
}
/* ========================================================================= *
* G_MODULE
* ========================================================================= */
static guint mcebat_init_tracker_id = 0;
static gboolean
mcebat_init_tracker_cb(gpointer aptr)
{
(void)aptr;
udevtracker_object = udevtracker_create();
if( !udevtracker_start(udevtracker_object) )
goto EXIT;
EXIT:
mcebat_init_tracker_id = 0;
return G_SOURCE_REMOVE;
}
static void
mcebat_init_settings(void)
{
mcebat_refresh_on_notify =
mce_conf_get_bool(MCE_CONF_BATTERY_UDEV_SETTINGS_GROUP,
MCE_CONF_BATTERY_UDEV_REFRESH_ON_NOTIFY,
DEFAULT_BATTERY_UDEV_REFRESH_ON_NOTIFY);
mcebat_refresh_on_extcon =
mce_conf_get_bool(MCE_CONF_BATTERY_UDEV_SETTINGS_GROUP,
MCE_CONF_BATTERY_UDEV_REFRESH_ON_EXTCON,
DEFAULT_BATTERY_UDEV_REFRESH_ON_EXTCON);
mcebat_refresh_on_heartbeat =
mce_conf_get_bool(MCE_CONF_BATTERY_UDEV_SETTINGS_GROUP,
MCE_CONF_BATTERY_UDEV_REFRESH_ON_HEARTBEAT,
DEFAULT_BATTERY_UDEV_REFRESH_ON_HEARTBEAT);
}
/** Init function for the battery and charger module
*
* @param module (not used)
*
* @return NULL on success, a string with an error message on failure
*/
G_MODULE_EXPORT const gchar *g_module_check_init(GModule *module)
{
(void)module;
mcebat_init_settings();
udevextcon_init();
udevdevice_init_blacklist();
udevdevice_init_chargertype();
udevproperty_init_types();
mcebat_dbus_init();
mcebat_datapipe_init();
/* Initial udev probing can take a long time.
* Do it from idle callback in order not to delay
* reaching systemd unit ready state.
*/
mcebat_init_tracker_id = g_idle_add(mcebat_init_tracker_cb, 0);
mce_log(LL_DEBUG, "%s: loaded", MODULE_NAME);
return NULL;
}
/** Exit function for the battery and charger module
*
* @param module (not used)
*/
G_MODULE_EXPORT void g_module_unload(GModule *module)
{
(void)module;
if( mcebat_init_tracker_id )
g_source_remove(mcebat_init_tracker_id), mcebat_init_tracker_id = 0;
mcebat_datapipe_quit();
mcebat_dbus_quit();
udevtracker_delete(udevtracker_object), udevtracker_object = 0;
udevproperty_quit_types();
udevdevice_quit_chargertype();
udevdevice_quit_blacklist();
udevextcon_quit();
udevtracker_cancel_refresh();
mce_log(LL_DEBUG, "%s: unloaded", MODULE_NAME);
}