/**
* @file battery-statefs.c
* Battery module -- this implements battery and charger logic for MCE
*
* Copyright (C) 2013-2015 Jolla Ltd.
*
* @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 .
*
*
* Rough diagram of data/control flow within this module
* @verbatim
*
* .------. .-------.
* |SFSCTL| |statefs|
* `------' `-------'
* | |
* .-------. .--------.
* |TRACKER|.---|INPUTSET|
* `-------'|. `--------'
* `-------'|
* `-------'
* |
* .------.
* |SFSBAT|
* `------'
* |
* .------.
* |MCEBAT|
* `------'
* |
* .---------.
* |datapipes|
* `---------'
* @endverbatim
*/
#include "../mce.h"
#include "../mce-log.h"
#include
#include
#include
#include
#include
#include
#include
/* ========================================================================= *
* CONSTANTS
* ========================================================================= */
/** Delay between re-open attempts while statefs entries are missing; [ms] */
#define START_DELAY (5 * 1000)
/** Delay from 1st property change to forced property re-read; [ms]
*
* HACK: Depending on kernel & fuse versions there are varying problems
* with epoll wakeups. It is possible that we get woken up, but
* do not receive events identifying the input file with changed
* content. To overcome this we schedule forced re-read of all
* battery properties if we get any kind of wakeup from epoll fd.
*/
#define REREAD_DELAY 250
/** Delay from 1st property change to state machine update; [ms] */
#define UPDATE_DELAY (REREAD_DELAY + 50)
/** Whether to support legacy pattery low led pattern; nonzero for yes */
#define SUPPORT_BATTERY_LOW_LED_PATTERN 0
/** Enumeration of possible statefs Battery.State property values */
typedef enum
{
STATEFS_BATTERY_STATE_UNKNOWN = -1,
STATEFS_BATTERY_STATE_EMPTY = 0,
STATEFS_BATTERY_STATE_LOW = 1,
STATEFS_BATTERY_STATE_DISCHARGING = 2,
STATEFS_BATTERY_STATE_CHARGING = 3,
STATEFS_BATTERY_STATE_FULL = 4,
} sfsbat_state_t;
/* ========================================================================= *
* PROTOTYPES
* ========================================================================= */
/* ------------------------------------------------------------------------- *
* MISC_UTILS
* ------------------------------------------------------------------------- */
static bool parse_state (const char *data, sfsbat_state_t *res);
static bool parse_int (const char *data, int *res);
static bool parse_bool (const char *data, bool *res);
static const char *repr_state (sfsbat_state_t state);
static const char *repr_bool (bool val);
/* ------------------------------------------------------------------------- *
* DATAPIPE_HANDLERS
* ------------------------------------------------------------------------- */
static void bsf_datapipe_shutting_down_cb(gconstpointer data);
static void bsf_datapipe_heartbeat_event_cb(gconstpointer data);
static void bsf_datapipe_usb_cable_state_cb(gconstpointer data);
static void bsf_datapipe_resume_detected_event_cb(gconstpointer data);
static void bsf_datapipe_init(void);
static void bsf_datapipe_quit(void);
/* ------------------------------------------------------------------------- *
* INPUTSET -- generic epoll set as glib io watch input listener
* ------------------------------------------------------------------------- */
static bool inputset_init (bool (*input_cb)(struct epoll_event *, int));
static void inputset_quit (void);
static bool inputset_insert (int fd, void *data);
static void inputset_remove (int fd);
static gboolean inputset_watch_cb(GIOChannel *srce, GIOCondition cond,
gpointer data);
/* ------------------------------------------------------------------------- *
* SFSBAT -- battery data as available from statefs
* ------------------------------------------------------------------------- */
/** Battery properties available via statefs */
typedef struct
{
/** Battery is: charging, discharging, empty or full */
sfsbat_state_t State;
/** Device is drawing power from battery */
bool OnBattery;
/** Low battery condition */
bool LowBattery;
/** Charge level percentage */
int ChargePercentage;
} sfsbat_t;
static void sfsbat_init(void);
/* ------------------------------------------------------------------------- *
* MCEBAT -- battery data in form expected by mce statemachines
* ------------------------------------------------------------------------- */
/** Battery properties in mce statemachine compatible form */
typedef struct
{
/** Battery charge percentage; for use with battery_level_pipe */
int level;
/** Battery FULL/OK/LOW/EMPTY; for use with battery_status_pipe */
int status;
/** Charger connected; for use with charger_state_pipe */
charger_state_t charger;
} mcebat_t;
static void mcebat_init (void);
static void mcebat_update_from_sfsbat (void);
static gboolean mcebat_update_cb (gpointer user_data);
static void mcebat_update_cancel (void);
static void mcebat_update_schedule (void);
/* ------------------------------------------------------------------------- *
* TRACKER -- binds statefs file to sfsbat_t member
* ------------------------------------------------------------------------- */
typedef struct tracker_t tracker_t;
/** Bind statefs file to member of sfsbat_t structure */
struct tracker_t
{
/** Basename of the input file */
const char *name;
/** Path to input file, set at tracker_init() / sfsctl_init() */
char *path;
/** Pointer to a sfsbat_t member */
void *value;
/** Value type specific input parser hook */
bool (*update_cb)(tracker_t *, const char *);
/** File descriptor for the input file */
int fd;
/** For use with debugging with pipes instead of real statefs */
bool seekable;
};
static const char *tracker_propdir(void);
static void tracker_init (tracker_t *self);
static void tracker_quit (tracker_t *self);
static bool tracker_open (tracker_t *self, bool *warned);
static void tracker_close (tracker_t *self);
static bool tracker_start (tracker_t *self, bool *warned);
static bool tracker_read_data (tracker_t *self, char *data, size_t size);
static bool tracker_parse_int (tracker_t *self, const char *data);
static bool tracker_parse_bool (tracker_t *self, const char *data);
static bool tracker_update (tracker_t *self);
/* ------------------------------------------------------------------------- *
* SFSCTL -- controls for statefs tracking
* ------------------------------------------------------------------------- */
static void sfsctl_init (void);
static void sfsctl_quit (void);
static void sfsctl_start (void);
static bool sfsctl_start_try (void);
static gboolean sfsctl_start_cb (gpointer aptr);
static bool sfsctl_watch_cb (struct epoll_event *eve, int cnt);
static void sfsctl_schedule_reread (void);
static void sfsctl_cancel_reread (void);
static gboolean sfsctl_reread_cb (gpointer aptr);
/* ------------------------------------------------------------------------- *
* MODULE_INIT_EXIT
* ------------------------------------------------------------------------- */
G_MODULE_EXPORT const gchar *g_module_check_init (GModule *module);
G_MODULE_EXPORT void g_module_unload (GModule *module);
/* ========================================================================= *
* MISC_UTILS
* ========================================================================= */
/** Lookup table for sfsbat_state_t parsing */
static const struct {
/** State name visible in statefs file */
const char *name;
/** Enumeration ID used in code */
sfsbat_state_t state;
} state_lut[] =
{
{ "charging", STATEFS_BATTERY_STATE_CHARGING },
{ "discharging", STATEFS_BATTERY_STATE_DISCHARGING },
{ "empty", STATEFS_BATTERY_STATE_EMPTY },
{ "low", STATEFS_BATTERY_STATE_LOW },
{ "full", STATEFS_BATTERY_STATE_FULL },
{ "unknown", STATEFS_BATTERY_STATE_UNKNOWN },
{ "", STATEFS_BATTERY_STATE_UNKNOWN },
};
/** String to sfsbat_state_t helper
*
* @param data text to parse
* @param res where to store parsed value
*
* @return true if data could be parse and stored to res, false otherwise
*/
static bool
parse_state(const char *data, sfsbat_state_t *res)
{
static bool lut_miss_reported = false;
for( size_t i = 0; ; ++i ) {
if( i == G_N_ELEMENTS(state_lut) ) {
/* Value was not found in the lookup table - handle
* as if "unknown" had been reported */
*res = STATEFS_BATTERY_STATE_UNKNOWN;
/* Emit warning, but only once to avoid repetitive reporting
* due to forced property updates */
if( !lut_miss_reported ) {
lut_miss_reported = true;
mce_log(LL_WARN, "unrecognized Battery.State value '%s';"
" assuming battery state is not known", data);
}
break;
}
if( !strcmp(state_lut[i].name, data) ) {
/* Use the state from the lookup table */
*res = state_lut[i].state;
/* Enable reporting of lookup table misses again */
lut_miss_reported = false;
break;
}
}
return true;
}
/** String to int helper
*
* @param data text to parse
* @param res where to store parsed value
*
* @return true if data could be parse and stored to res, false otherwise
*/
static bool
parse_int(const char *data, int *res)
{
bool ack = true;
char *pos = (char *)data;
int val = strtol(pos, &pos, 0);
if( pos > data && *pos == 0 )
*res = val;
else
ack = false;
return ack;
}
/** String to bool helper
*
* @param data text to parse
* @param res where to store parsed value
*
* @return true if data could be parse and stored to res, false otherwise
*/
static bool
parse_bool(const char *data, bool *res)
{
bool ack = true;
int val = 0;
if( parse_int(data, &val) )
*res = (val != 0);
else if( !strcmp(data, "true") )
*res = true;
else if( !strcmp(data, "false") )
*res = false;
else
ack = false;
return ack;
}
/** sfsbat_state_t to string helper
*
* @param state sfsbat_state_t enum value
*
* @return human readable state name
*/
static const char *
repr_state(sfsbat_state_t state)
{
const char *res = "unknown";
for( size_t i = 0; i < G_N_ELEMENTS(state_lut); ++i ) {
if( state_lut[i].state != state )
continue;
res = state_lut[i].name;
break;
}
return res;
}
/** Boolean to string helper
*
* @param val boolean value
*
* @return "true" or "false" depending on val
*/
static const char *
repr_bool(bool val)
{
return val ? "true" : "false";
}
/* ========================================================================= *
* DATAPIPE_HANDLERS
* ========================================================================= */
/** Device is shutting down; assume false */
static bool shutting_down = false;
/** Change notifications for shutting_down
*/
static void bsf_datapipe_shutting_down_cb(gconstpointer data)
{
bool prev = shutting_down;
shutting_down = GPOINTER_TO_INT(data);
if( shutting_down == prev )
goto EXIT;
mce_log(LL_DEBUG, "shutting_down = %d -> %d",
prev, shutting_down);
/* Loss of statefs files is expected during shutdown */
EXIT:
return;
}
/** Change notifications for heartbeat_event_pipe
*/
static void bsf_datapipe_heartbeat_event_cb(gconstpointer data)
{
(void)data;
mce_log(LL_DEBUG, "heartbeat");
/* HACK: Force all props to be reread on system heartbeat
* i.e. periodically, when the device is awake anyway,
*/
sfsctl_schedule_reread();
}
/** USB cable status; assume disconnected */
static usb_cable_state_t usb_cable_state = USB_CABLE_UNDEF;
/** Change notifications for usb_cable_state
*/
static void bsf_datapipe_usb_cable_state_cb(gconstpointer data)
{
usb_cable_state_t prev = usb_cable_state;
usb_cable_state = GPOINTER_TO_INT(data);
if( prev == usb_cable_state )
goto EXIT;
mce_log(LL_DEBUG, "usb_cable_state = %s -> %s",
usb_cable_state_repr(prev),
usb_cable_state_repr(usb_cable_state));
/* HACK: Force all props to be reread when usb cable
* state change is reported by usb-moded.
*/
sfsctl_schedule_reread();
EXIT:
return;
}
/** Resumed from suspend notification */
static void bsf_datapipe_resume_detected_event_cb(gconstpointer data)
{
(void) data;
mce_log(LL_DEBUG, "resume detected");
/* HACK: Force all props to be reread on resume */
sfsctl_schedule_reread();
}
/** Array of datapipe handlers */
static datapipe_handler_t bsf_datapipe_handlers[] =
{
// output triggers
{
.datapipe = &shutting_down_pipe,
.output_cb = bsf_datapipe_shutting_down_cb,
},
{
.datapipe = &heartbeat_event_pipe,
.output_cb = bsf_datapipe_heartbeat_event_cb,
},
{
.datapipe = &usb_cable_state_pipe,
.output_cb = bsf_datapipe_usb_cable_state_cb,
},
{
.datapipe = &resume_detected_event_pipe,
.output_cb = bsf_datapipe_resume_detected_event_cb,
},
// sentinel
{
.datapipe = 0,
}
};
static datapipe_bindings_t bsf_datapipe_bindings =
{
.module = "battery_statefs",
.handlers = bsf_datapipe_handlers,
};
/** Append triggers/filters to datapipes
*/
static void bsf_datapipe_init(void)
{
mce_datapipe_init_bindings(&bsf_datapipe_bindings);
}
/** Remove triggers/filters from datapipes
*/
static void bsf_datapipe_quit(void)
{
mce_datapipe_quit_bindings(&bsf_datapipe_bindings);
}
/* ========================================================================= *
* INPUTSET
* ========================================================================= */
/** epoll fd for tracking a set of input files */
static int inputset_epoll_fd = -1;
/** glib io watch for inputset_epoll_fd */
static guint inputset_watch_id = 0;
/** Handle statefs change notifications received via epoll set
*
* @param srce (not used)
* @param cond wakeup reason
* @param data event handler function as void pointer
*
* @return FALSE if the io watch must be disabled, TRUE otherwise
*/
static gboolean
inputset_watch_cb(GIOChannel *srce, GIOCondition cond, gpointer data)
{
(void)srce;
gboolean keep_going = TRUE;
struct epoll_event eve[16];
if( cond & ~G_IO_IN ) {
mce_log(LL_ERR, "unexpected io cond: 0x%x", (unsigned)cond);
keep_going = FALSE;
}
int rc = epoll_wait(inputset_epoll_fd, eve, G_N_ELEMENTS(eve), 0);
if( rc == -1 ) {
switch( errno ) {
case EINTR:
case EAGAIN:
break;
default:
mce_log(LL_ERR, "statfs io wait: %m");
keep_going = FALSE;
}
goto cleanup;
}
bool (*input_cb)(struct epoll_event *, int) = data;
if( !input_cb(eve, rc) )
keep_going = FALSE;
cleanup:
if( !keep_going ) {
mce_log(LL_CRIT, "disabling statfs io watch");
inputset_watch_id = 0;
}
return keep_going;
}
/** Initialize epoll set and io watch for it
*
* @param input_cb event handler function
*
* @return true on success, false on failure
*/
static bool
inputset_init(bool (*input_cb)(struct epoll_event *, int))
{
bool success = false;
GIOChannel *chn = 0;
if( (inputset_epoll_fd = epoll_create1(EPOLL_CLOEXEC)) == -1 ) {
mce_log(LL_WARN, "epoll_create: %m");
goto cleanup;
}
if( !(chn = g_io_channel_unix_new(inputset_epoll_fd)) )
goto cleanup;
g_io_channel_set_close_on_unref(chn, FALSE);
inputset_watch_id = g_io_add_watch(chn, G_IO_IN,
inputset_watch_cb, input_cb);
if( !inputset_watch_id )
goto cleanup;
success = true;
cleanup:
if( chn )
g_io_channel_unref(chn);
if( !success )
inputset_quit();
return success;
}
/** Remove epoll set and io watch for it
*/
static void
inputset_quit(void)
{
if( inputset_watch_id )
g_source_remove(inputset_watch_id), inputset_watch_id = 0;
if( inputset_epoll_fd != -1 )
close(inputset_epoll_fd), inputset_epoll_fd = -1;
}
/** Add tracking object to epoll set
*
* @param fd input file descriptor
* @param data data to associate with the fd
*
* @return true on success, false on failure
*/
static bool
inputset_insert(int fd, void *data)
{
bool success = false;
struct epoll_event eve;
if( fd == -1 )
goto cleanup;
memset(&eve, 0, sizeof eve);
eve.events = EPOLLIN;
eve.data.ptr = data;
int rc = epoll_ctl(inputset_epoll_fd, EPOLL_CTL_ADD, fd, &eve);
if( rc == -1 ) {
mce_log(LL_WARN, "EPOLL_CTL_ADD(%d): %m", fd);
goto cleanup;
}
success = true;
cleanup:
return success;
}
/** Remove tracking object from epoll set
*
* @param fd input file descriptor
*/
static void
inputset_remove(int fd)
{
if( fd == -1 )
goto cleanup;
if( epoll_ctl(inputset_epoll_fd, EPOLL_CTL_DEL, fd, 0) == -1 )
mce_log(LL_WARN, "EPOLL_CTL_DEL(%d): %m", fd);
cleanup:
return;
}
/* ========================================================================= *
* SFSBAT
* ========================================================================= */
/** Battery status, as available via statefs */
static sfsbat_t sfsbat;
/** Provide intial guess of statefs battery status
*/
static void
sfsbat_init(void)
{
memset(&sfsbat, 0, sizeof sfsbat);
sfsbat.State = STATEFS_BATTERY_STATE_UNKNOWN;
sfsbat.OnBattery = true;
sfsbat.LowBattery = false;
sfsbat.ChargePercentage = 50;
}
/* ========================================================================= *
* MCEBAT
* ========================================================================= */
/** Timer for processing battery status changes */
static guint mcebat_update_id = 0;
/** Current battery status in mce legacy compatible form */
static mcebat_t mcebat;
/** Provide intial guess of mce battery status
*/
static void
mcebat_init(void)
{
memset(&mcebat, 0, sizeof mcebat);
mcebat.level = 50;
mcebat.status = BATTERY_STATUS_UNDEF;
mcebat.charger = CHARGER_STATE_UNDEF;
}
/** Update mce battery status from statefs battery data
*/
static void
mcebat_update_from_sfsbat(void)
{
mcebat.level = sfsbat.ChargePercentage;
switch( sfsbat.State ) {
case STATEFS_BATTERY_STATE_EMPTY:
mcebat.status = BATTERY_STATUS_EMPTY;
break;
case STATEFS_BATTERY_STATE_LOW:
mcebat.status = BATTERY_STATUS_LOW;
break;
case STATEFS_BATTERY_STATE_DISCHARGING:
if( sfsbat.LowBattery )
mcebat.status = BATTERY_STATUS_LOW;
else
mcebat.status = BATTERY_STATUS_OK;
break;
case STATEFS_BATTERY_STATE_CHARGING:
mcebat.status = BATTERY_STATUS_OK;
break;
case STATEFS_BATTERY_STATE_FULL:
mcebat.status = BATTERY_STATUS_FULL;
break;
default:
case STATEFS_BATTERY_STATE_UNKNOWN:
mcebat.status = BATTERY_STATUS_UNDEF;
break;
}
if( sfsbat.OnBattery )
mcebat.charger = CHARGER_STATE_OFF;
else
mcebat.charger = CHARGER_STATE_ON;
}
/** Process accumulated statefs battery status changes
*
* @param user_data (not used)
*
* @return FALSE (to stop timer from repeating)
*/
static gboolean
mcebat_update_cb(gpointer user_data)
{
(void)user_data;
if( !mcebat_update_id )
goto cleanup;
mcebat_update_id = 0;
mce_log(LL_DEBUG, "update datapipes");
/* Get a copy of current status */
mcebat_t prev = mcebat;
/* Update from statefs based information */
mcebat_update_from_sfsbat();
/* Process changes */
if( mcebat.charger != prev.charger ) {
mce_log(LL_NOTICE, "charger: %s -> %s",
charger_state_repr(prev.charger),
charger_state_repr(mcebat.charger));
/* Charger connected state */
datapipe_exec_full(&charger_state_pipe, GINT_TO_POINTER(mcebat.charger));
/* Charging led pattern */
if( mcebat.charger == 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( mcebat.status != prev.status ) {
mce_log(LL_NOTICE, "status: %s -> %s",
battery_status_repr(prev.status),
battery_status_repr(mcebat.status));
/* Battery full led pattern */
if( mcebat.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( mcebat.status == BATTERY_STATUS_LOW ||
mcebat.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(mcebat.status));
}
if( mcebat.level != prev.level ) {
mce_log(LL_NOTICE, "level: %d -> %d", prev.level, mcebat.level);
/* Battery charge percentage */
datapipe_exec_full(&battery_level_pipe,
GINT_TO_POINTER(mcebat.level));
}
cleanup:
return FALSE;
}
/** Cancel processing of statefs battery status changes
*/
static void
mcebat_update_cancel(void)
{
if( mcebat_update_id )
g_source_remove(mcebat_update_id), mcebat_update_id = 0;
}
/** Initiate delayed processing of statefs battery status changes
*/
static void
mcebat_update_schedule(void)
{
if( !mcebat_update_id )
mcebat_update_id = g_timeout_add(UPDATE_DELAY, mcebat_update_cb, 0);
}
/* ========================================================================= *
* TRACKER
* ========================================================================= */
/** Locate directory where battery properties are
*/
static const char *
tracker_propdir(void)
{
// TODO: use statefs helper function to get the path
static const char def[] = "/run/state/namespaces/Battery";
static char *res = 0;
if( !res ) {
// TODO: debug stuff, remove later
const char *env = getenv("BATTERY_BASEDIR");
res = strdup(env ?: def);
}
return res;
}
/** Read string from statefs input file
*
* @param self statefs input file tracking object
* @param data buffer where to read to
* @param size length of the buffer
*
* @return true on success, or false in case of errors
*/
static bool
tracker_read_data(tracker_t *self, char *data, size_t size)
{
bool res = false;
int rc;
if( self->fd == -1 )
goto cleanup;
/* Read the state data */
if( (rc = read(self->fd, data, size-1)) == -1 ) {
mce_log(LL_WARN, "%s: read: %m", self->path);
goto cleanup;
}
/* Rewind to the start of the file */
if( self->seekable && lseek(self->fd, 0, SEEK_SET) == -1 ) {
mce_log(LL_WARN, "%s: rewind: %m", self->path);
goto cleanup;
}
data[rc] = 0, res = true;
data[strcspn(data, "\r\n")] = 0;
cleanup:
return res;
}
/** Parse statefs file content to int value
*
* @param self statefs input file tracking object
* @param data string to parse
*
* @return true if data could be read and the value changed, false otherwise
*/
static bool
tracker_parse_int(tracker_t *self, const char *data)
{
bool ack = false;
int *now = self->value;
int zen = *now;
if( !parse_int(data, &zen) ) {
mce_log(LL_WARN, "%s: can't convert '%s' to int", self->name, data);
goto cleanup;
}
if( *now == zen )
goto cleanup;
mce_log(LL_INFO, "%s: %d -> %d", self->name, *now, zen);
*now = zen, ack = true;
cleanup:
return ack;
}
/** Parse statefs file content to bool value
*
* @param self statefs input file tracking object
* @param data string to parse
*
* @return true if data could be read and the value changed, false otherwise
*/
static bool
tracker_parse_bool(tracker_t *self, const char *data)
{
bool ack = false;
bool *now = self->value;
bool zen = *now;
if( !parse_bool(data, &zen) ) {
mce_log(LL_WARN, "%s: can't convert '%s' to bool", self->name, data);
goto cleanup;
}
if( *now == zen )
goto cleanup;
mce_log(LL_INFO, "%s: %s -> %s", self->name,
repr_bool(*now), repr_bool(zen));
*now = zen, ack = true;
cleanup:
return ack;
}
/** Parse statefs file content to battery state enum
*
* @param self statefs input file tracking object
* @param data string to parse
*
* @return true if data could be read and the value changed, false otherwise
*/
static bool
tracker_parse_state(tracker_t *self, const char *data)
{
bool ack = false;
sfsbat_state_t *now = self->value;
sfsbat_state_t zen = *now;
if( !parse_state(data, &zen) ) {
mce_log(LL_WARN, "%s: can't convert '%s' to battery state",
self->name, data);
goto cleanup;
}
if( *now == zen )
goto cleanup;
mce_log(LL_INFO, "%s: %s -> %s", self->name,
repr_state(*now), repr_state(zen));
*now = zen, ack = true;
cleanup:
return ack;
}
/** Update value from statefs content and schedule state machine update
*
* @param self statefs input file tracking object
*
* @return true if io was successfull, but the value did not change;
* false otherwise
*/
static bool
tracker_update(tracker_t *self)
{
bool dummy = false; // assume: io failed or value changed
char data[64];
if( !tracker_read_data(self, data, sizeof data) ) {
tracker_close(self);
goto cleanup;
}
if( self->update_cb(self, data) )
mcebat_update_schedule();
else
dummy = true; // io succeesfull, but value did not change
cleanup:
return dummy;
}
/** Open statefs file
*
* @param self statefs input file tracking object
* @param warned pointer to flag holding already-warned status
*
* @return true if file is open, false otherwise
*/
static bool
tracker_open(tracker_t *self, bool *warned)
{
bool success = false;
if( self->fd != -1 )
goto cleanup_success;
self->seekable = false;
self->fd = open(self->path, O_RDONLY | O_DIRECT);
if( self->fd == -1 ) {
/* On shutdown it is expected that statefs files
* become unaccessible. And to reduce journal
* spamming on statefs restart, log only the
* first file in the set that we fail to open.
*/
int level = LL_WARN;
if( shutting_down || *warned )
level = LL_DEBUG;
else
*warned = true;
mce_log(level, "%s: open: %m", self->path);
goto cleanup_failure;
}
if( lseek(self->fd, 0, SEEK_CUR) == -1 )
mce_log(LL_WARN, "%s: is not seekable", self->path);
else
self->seekable = true;
cleanup_success:
mce_log(LL_DEBUG, "%s: opened", self->name);
success = true;
cleanup_failure:
return success;
}
/** Close statefs file
*
* @param self statefs input file tracking object
*/
static void
tracker_close(tracker_t *self)
{
if( self->fd != -1 ) {
mce_log(LL_DEBUG, "%s: closing", self->name);
inputset_remove(self->fd);
close(self->fd), self->fd = -1;
}
}
/** Initialize tracker_t dynamic data
*
* @param self statefs input file tracking object
*/
static void
tracker_init(tracker_t *self)
{
self->path = g_strdup_printf("%s/%s", tracker_propdir(), self->name);
}
/** Release dynamic resources associated with tracker_t
*
* @param self statefs input file tracking object
*/
static void
tracker_quit(tracker_t *self)
{
tracker_close(self);
free(self->path), self->path = 0;
}
/** Start tracking statefs property file
*
* @param self statefs input file tracking object
* @param warned pointer to flag holding already-warned status
*
* @return true if statefs file is open and tracked, false otherwise
*/
static bool
tracker_start(tracker_t *self, bool *warned)
{
bool success = false;
if( self->fd != -1 )
goto cleanup_success;
if( !tracker_open(self, warned) )
goto cleanup_failure;
tracker_update(self);
if( !inputset_insert(self->fd, self) ) {
tracker_close(self);
goto cleanup_failure;
}
cleanup_success:
success = true;
cleanup_failure:
return success;
}
/* ========================================================================= *
* SFSCTL
* ========================================================================= */
/** Initializer macro for int properties */
#define INIT_PROP_INT(NAME)\
{\
.name = #NAME,\
.path = 0,\
.value = &sfsbat.NAME,\
.update_cb = tracker_parse_int,\
.fd = -1,\
}
/** Initializer macro for bool properties */
#define INIT_PROP_BOOL(NAME)\
{\
.name = #NAME,\
.path = 0,\
.value = &sfsbat.NAME,\
.update_cb = tracker_parse_bool,\
.fd = -1,\
}
/** Lookup table for statefs based properties */
static tracker_t sfsctl_props[] =
{
{
.name = "State",
.path = 0,
.value = &sfsbat.State,
.update_cb = tracker_parse_state,
.fd = -1,
},
INIT_PROP_BOOL(OnBattery),
INIT_PROP_BOOL(LowBattery),
INIT_PROP_INT(ChargePercentage),
// sentinel
{ .name = 0, }
};
/** timeout for waiting statefs to come available */
static guint sfsctl_start_id = 0;
/** timeout for handling missed epoll io notifications */
static guint sfsctl_reread_id = 0;
/** Initialize dynamic data for statefs tracking objects
*/
static void
sfsctl_init(void)
{
for( tracker_t *prop = sfsctl_props; prop->name; ++prop )
tracker_init(prop);
}
/** Stop statefs change tracking
*/
static void
sfsctl_quit(void)
{
if( sfsctl_start_id )
g_source_remove(sfsctl_start_id), sfsctl_start_id = 0;
for( tracker_t *prop = sfsctl_props; prop->name; ++prop )
tracker_quit(prop);
}
/** Helper for starting/restarting statefs change tracking
*
* @return true if all properties could be bound to statefs files
*/
static bool
sfsctl_start_try(void)
{
bool success = true;
bool warned = false;
mce_log(LL_NOTICE, "probe statefs files");
for( tracker_t *prop = sfsctl_props; prop->name; ++prop ) {
if( !tracker_start(prop, &warned) )
success = false;
}
return success;
}
/** Timeout for retrying start of statefs change tracking
*
* @return FALSE on success (to stop the timer), or
* TRUE on failure (to keep timer repeating)
*/
static gboolean
sfsctl_start_cb(gpointer aptr)
{
(void)aptr;
gboolean keep_going = FALSE;
if( !sfsctl_start_id )
goto cleanup;
if( !sfsctl_start_try() )
keep_going = TRUE;
else
sfsctl_start_id = 0;
cleanup:
return keep_going;
}
/** Start statefs change tracking
*
* If all properties are not available immediately, retry
* timer will be started
*/
static void
sfsctl_start(void)
{
/* Retry timer already active ? */
if( sfsctl_start_id )
goto cleanup;
/* Attempt to start file tracking */
if( sfsctl_start_try() )
goto cleanup;
/* Re-try again later */
sfsctl_start_id = g_timeout_add(START_DELAY, sfsctl_start_cb, 0);
cleanup:
return;
}
/** Handle statefs change notifications received via epoll set
*
* @param eve array of epoll events
* @param cnt number of epoll events
*
* @return false if io watch should be disabled, otherwise true
*/
static bool
sfsctl_watch_cb(struct epoll_event *eve, int cnt)
{
bool keep_going = true;
bool statefs_lost = false;
mce_log(LL_DEBUG, "process %d statefs changes", cnt);
for( int i = 0; i < cnt; ++i ) {
tracker_t *prop = eve[i].data.ptr;
if( eve[i].events & ~EPOLLIN )
tracker_close(prop), statefs_lost = true;
else
tracker_update(prop);
}
/* HACK: Force all props to be reread before datapipe updates */
sfsctl_schedule_reread();
if( statefs_lost ) {
/* ASSUME: Loss of inputs == statefs restart */
/* Start timer based re-open attempts */
sfsctl_start();
/* Forced re-read makes no sense, cancel it */
sfsctl_cancel_reread();
}
return keep_going;
}
/** Timeout for forced re-read of statefs properties
*
* @param aptr (not used)
*
* @return FALSE (to stop the timer from repeating)
*/
static gboolean
sfsctl_reread_cb(gpointer aptr)
{
(void)aptr;
if( !sfsctl_reread_id )
goto cleanup;
sfsctl_reread_id = 0;
mce_log(LL_DEBUG, "forced update of all states files");
for( tracker_t *prop = sfsctl_props; prop->name; ++prop )
tracker_update(prop);
cleanup:
return FALSE;
}
/** Cancel forced re-read of statefs properties
*/
static void
sfsctl_cancel_reread(void)
{
if( sfsctl_reread_id )
g_source_remove(sfsctl_reread_id), sfsctl_reread_id = 0;
}
/** Schedule forced re-read of statefs properties
*/
static void
sfsctl_schedule_reread(void)
{
if( !sfsctl_reread_id )
sfsctl_reread_id = g_timeout_add(REREAD_DELAY, sfsctl_reread_cb, 0);
}
/** Stop battery/charging tracking
*/
static void
battery_quit(void)
{
/* stop statefs change tracking */
sfsctl_quit();
/* cancel pending state machine updates */
mcebat_update_cancel();
/* cancel pending property re-reads */
sfsctl_cancel_reread();
/* remove epoll input listener */
inputset_quit();
}
/** Start battery/charging tracking
*
* @return true on success, false otherwise
*/
static bool
battery_init(void)
{
bool success = false;
/* initialize epoll input listener */
if( !inputset_init(sfsctl_watch_cb) )
goto cleanup;
/* reset data used by the state machine */
mcebat_init();
/* reset data that should come from statefs */
sfsbat_init();
/* initialize statefs paths etc */
sfsctl_init();
/* start statefs change tracking */
sfsctl_start();
success = true;
cleanup:
return success;
}
/* ========================================================================= *
* MODULE_INIT_EXIT
* ========================================================================= */
/** Module name */
#define MODULE_NAME "battery_statefs"
/** 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
};
/** Init function for the battery and charger module
*
* @param module (not used)
*
* @return NULL on success, a string with an error message on failure
*/
const gchar *g_module_check_init(GModule *module)
{
(void)module;
bsf_datapipe_init();
if( !battery_init() )
mce_log(LL_WARN, MODULE_NAME" module initialization failed");
else
mce_log(LL_INFO, MODULE_NAME" module initialized ");
return NULL;
}
/** Exit function for the battery and charger module
*
* @param module (not used)
*/
void g_module_unload(GModule *module)
{
(void)module;
bsf_datapipe_quit();
battery_quit();
}