From a2d950044dbe5bc31c92ef89514d85e712f188f7 Mon Sep 17 00:00:00 2001 From: Benjamin Berg Date: Wed, 21 Apr 2021 17:16:03 +0200 Subject: [PATCH] device: Add simple temperature model for devices This temperature model has three states: * COLD * WARM * HOT Device drivers can define the time it requires for the device to get HOT and COLD. The underlying model assumes an exponential warming and cooling process and enforces a cool-off time after the device has reached the HOT state. This cool down period is however shorter than the specified time in the driver. --- libfprint/drivers/elanmoc/elanmoc.c | 1 + libfprint/drivers/goodixmoc/goodix.c | 1 + libfprint/drivers/synaptics/synaptics.c | 1 + libfprint/fp-device-private.h | 29 ++++++ libfprint/fp-device.c | 71 +++++++++++++++ libfprint/fp-device.h | 18 ++++ libfprint/fpi-device.c | 114 ++++++++++++++++++++++++ libfprint/fpi-device.h | 8 ++ 8 files changed, 243 insertions(+) diff --git a/libfprint/drivers/elanmoc/elanmoc.c b/libfprint/drivers/elanmoc/elanmoc.c index 712af90..49ea267 100644 --- a/libfprint/drivers/elanmoc/elanmoc.c +++ b/libfprint/drivers/elanmoc/elanmoc.c @@ -1121,6 +1121,7 @@ fpi_device_elanmoc_class_init (FpiDeviceElanmocClass *klass) dev_class->scan_type = FP_SCAN_TYPE_PRESS; dev_class->id_table = id_table; dev_class->nr_enroll_stages = ELAN_MOC_ENROLL_TIMES; + dev_class->temp_hot_seconds = -1; dev_class->open = elanmoc_open; dev_class->close = elanmoc_close; diff --git a/libfprint/drivers/goodixmoc/goodix.c b/libfprint/drivers/goodixmoc/goodix.c index d98e733..d012d46 100644 --- a/libfprint/drivers/goodixmoc/goodix.c +++ b/libfprint/drivers/goodixmoc/goodix.c @@ -1565,6 +1565,7 @@ fpi_device_goodixmoc_class_init (FpiDeviceGoodixMocClass *klass) dev_class->scan_type = FP_SCAN_TYPE_PRESS; dev_class->id_table = id_table; dev_class->nr_enroll_stages = DEFAULT_ENROLL_SAMPLES; + dev_class->temp_hot_seconds = -1; dev_class->open = gx_fp_init; dev_class->close = gx_fp_exit; diff --git a/libfprint/drivers/synaptics/synaptics.c b/libfprint/drivers/synaptics/synaptics.c index d92135f..e50a30d 100644 --- a/libfprint/drivers/synaptics/synaptics.c +++ b/libfprint/drivers/synaptics/synaptics.c @@ -1385,6 +1385,7 @@ fpi_device_synaptics_class_init (FpiDeviceSynapticsClass *klass) dev_class->scan_type = FP_SCAN_TYPE_PRESS; dev_class->id_table = id_table; dev_class->nr_enroll_stages = ENROLL_SAMPLES; + dev_class->temp_hot_seconds = -1; dev_class->open = dev_init; dev_class->close = dev_exit; diff --git a/libfprint/fp-device-private.h b/libfprint/fp-device-private.h index 3f8bf0f..f17541e 100644 --- a/libfprint/fp-device-private.h +++ b/libfprint/fp-device-private.h @@ -22,6 +22,23 @@ #include "fpi-device.h" +/* Chosen so that if we turn on after WARM -> COLD, it takes exactly one time + * constant to go from COLD -> HOT. + * TEMP_COLD_THRESH = 1 / (e + 1) + */ +#define TEMP_COLD_THRESH (0.26894142136999512075) +#define TEMP_WARM_HOT_THRESH (1.0 - TEMP_COLD_THRESH) +#define TEMP_HOT_WARM_THRESH (0.5) + +/* Delay updates by 100ms to avoid hitting the border exactly */ +#define TEMP_DELAY_SECONDS 0.1 + +/* Hopefully 3min is long enough to not get in the way, while also not + * properly overheating any devices. + */ +#define DEFAULT_TEMP_HOT_SECONDS (3 * 60) +#define DEFAULT_TEMP_COLD_SECONDS (9 * 60) + typedef struct { FpDeviceType type; @@ -58,6 +75,15 @@ typedef struct /* State for tasks */ gboolean wait_for_finger; FpFingerStatusFlags finger_status; + + /* Device temperature model information and state */ + GSource *temp_timeout; + FpTemperature temp_current; + gint32 temp_hot_seconds; + gint32 temp_cold_seconds; + gint64 temp_last_update; + gboolean temp_last_active; + gdouble temp_current_ratio; } FpDevicePrivate; @@ -88,3 +114,6 @@ typedef struct } FpMatchData; void match_data_free (FpMatchData *match_data); + +void fpi_device_update_temp (FpDevice *device, + gboolean is_active); diff --git a/libfprint/fp-device.c b/libfprint/fp-device.c index eeaa875..a99596e 100644 --- a/libfprint/fp-device.c +++ b/libfprint/fp-device.c @@ -48,6 +48,7 @@ enum { PROP_NR_ENROLL_STAGES, PROP_SCAN_TYPE, PROP_FINGER_STATUS, + PROP_TEMPERATURE, PROP_FPI_ENVIRON, PROP_FPI_USB_DEVICE, PROP_FPI_UDEV_DATA_SPIDEV, @@ -151,6 +152,36 @@ fp_device_constructed (GObject *object) priv->device_name = g_strdup (cls->full_name); priv->device_id = g_strdup ("0"); + if (cls->temp_hot_seconds > 0) + { + priv->temp_hot_seconds = cls->temp_hot_seconds; + priv->temp_cold_seconds = cls->temp_cold_seconds; + g_assert (priv->temp_cold_seconds > 0); + } + else if (cls->temp_hot_seconds == 0) + { + priv->temp_hot_seconds = DEFAULT_TEMP_HOT_SECONDS; + priv->temp_cold_seconds = DEFAULT_TEMP_COLD_SECONDS; + } + else + { + /* Temperature management disabled */ + priv->temp_hot_seconds = -1; + priv->temp_cold_seconds = -1; + } + + /* Start out at not completely cold (i.e. assume we are only at the upper + * bound of COLD). + * To be fair, the warm-up from 0 to WARM should be really short either way. + * + * Note that a call to fpi_device_update_temp() is not needed here as no + * timeout must be registered. + */ + priv->temp_current = FP_TEMPERATURE_COLD; + priv->temp_current_ratio = TEMP_COLD_THRESH; + priv->temp_last_update = g_get_monotonic_time (); + priv->temp_last_active = FALSE; + G_OBJECT_CLASS (fp_device_parent_class)->constructed (object); } @@ -165,6 +196,8 @@ fp_device_finalize (GObject *object) if (priv->is_open) g_warning ("User destroyed open device! Not cleaning up properly!"); + g_clear_pointer (&priv->temp_timeout, g_source_destroy); + g_slist_free_full (priv->sources, (GDestroyNotify) g_source_destroy); g_clear_pointer (&priv->current_idle_cancel_source, g_source_destroy); @@ -204,6 +237,10 @@ fp_device_get_property (GObject *object, g_value_set_flags (value, priv->finger_status); break; + case PROP_TEMPERATURE: + g_value_set_enum (value, priv->temp_current); + break; + case PROP_DRIVER: g_value_set_static_string (value, FP_DEVICE_GET_CLASS (self)->id); break; @@ -358,6 +395,13 @@ fp_device_class_init (FpDeviceClass *klass) FP_TYPE_FINGER_STATUS_FLAGS, FP_FINGER_STATUS_NONE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE); + properties[PROP_TEMPERATURE] = + g_param_spec_enum ("temperature", + "Temperature", + "The temperature estimation for device to prevent overheating.", + FP_TYPE_TEMPERATURE, FP_TEMPERATURE_COLD, + G_PARAM_STATIC_STRINGS | G_PARAM_READABLE); + properties[PROP_DRIVER] = g_param_spec_string ("driver", "Driver", @@ -616,6 +660,25 @@ fp_device_get_nr_enroll_stages (FpDevice *device) return priv->nr_enroll_stages; } +/** + * fp_device_get_temperature: + * @device: A #FpDevice + * + * Retrieves simple temperature information for device. It is not possible + * to use a device when this is #FP_TEMPERATURE_HOT. + * + * Returns: The current temperature estimation. + */ +FpTemperature +fp_device_get_temperature (FpDevice *device) +{ + FpDevicePrivate *priv = fp_device_get_instance_private (device); + + g_return_val_if_fail (FP_IS_DEVICE (device), -1); + + return priv->temp_current; +} + /** * fp_device_supports_identify: * @device: A #FpDevice @@ -902,6 +965,8 @@ fp_device_enroll (FpDevice *device, priv->current_task = g_steal_pointer (&task); maybe_cancel_on_cancelled (device, cancellable); + fpi_device_update_temp (device, TRUE); + data = g_new0 (FpEnrollData, 1); data->print = g_object_ref_sink (template_print); data->enroll_progress_cb = progress_cb; @@ -995,6 +1060,8 @@ fp_device_verify (FpDevice *device, priv->current_task = g_steal_pointer (&task); maybe_cancel_on_cancelled (device, cancellable); + fpi_device_update_temp (device, TRUE); + data = g_new0 (FpMatchData, 1); data->enrolled_print = g_object_ref (enrolled_print); data->match_cb = match_cb; @@ -1114,6 +1181,8 @@ fp_device_identify (FpDevice *device, priv->current_task = g_steal_pointer (&task); maybe_cancel_on_cancelled (device, cancellable); + fpi_device_update_temp (device, TRUE); + data = g_new0 (FpMatchData, 1); /* We cannot store the gallery directly, because the ptr array may not own * a reference to each print. Also, the caller could in principle modify the @@ -1231,6 +1300,8 @@ fp_device_capture (FpDevice *device, priv->current_task = g_steal_pointer (&task); maybe_cancel_on_cancelled (device, cancellable); + fpi_device_update_temp (device, TRUE); + priv->wait_for_finger = wait_for_finger; cls->capture (device); diff --git a/libfprint/fp-device.h b/libfprint/fp-device.h index 5d224f8..d2d50ca 100644 --- a/libfprint/fp-device.h +++ b/libfprint/fp-device.h @@ -81,6 +81,23 @@ typedef enum { FP_SCAN_TYPE_PRESS, } FpScanType; +/** + * FpTemperature: + * @FP_TEMPERATURE_COLD: Sensor is considered cold. + * @FP_TEMPERATURE_WARM: Sensor is warm, usage time may be limited. + * @FP_TEMPERATURE_HOT: Sensor is hot and cannot be used. + * + * When a device is created, it is assumed to be cold. Applications such as + * fprintd may want to ensure all devices on the system are cold before + * shutting down in order to ensure that the cool-off period is not violated + * because the internal libfprint state about the device is lost. + */ +typedef enum { + FP_TEMPERATURE_COLD, + FP_TEMPERATURE_WARM, + FP_TEMPERATURE_HOT, +} FpTemperature; + /** * FpDeviceRetry: * @FP_DEVICE_RETRY_GENERAL: The scan did not succeed due to poor scan quality @@ -201,6 +218,7 @@ gboolean fp_device_is_open (FpDevice *device); FpScanType fp_device_get_scan_type (FpDevice *device); FpFingerStatusFlags fp_device_get_finger_status (FpDevice *device); gint fp_device_get_nr_enroll_stages (FpDevice *device); +FpTemperature fp_device_get_temperature (FpDevice *device); FpDeviceFeature fp_device_get_features (FpDevice *device); gboolean fp_device_has_feature (FpDevice *device, diff --git a/libfprint/fpi-device.c b/libfprint/fpi-device.c index d91fc94..c6e21da 100644 --- a/libfprint/fpi-device.c +++ b/libfprint/fpi-device.c @@ -19,6 +19,8 @@ */ #define FP_COMPONENT "device" +#include + #include "fpi-log.h" #include "fp-device-private.h" @@ -851,6 +853,8 @@ fp_device_task_return_in_idle_cb (gpointer user_data) priv->current_action = FPI_DEVICE_ACTION_NONE; priv->current_task_idle_return_source = NULL; + fpi_device_update_temp (data->device, FALSE); + if (action == FPI_DEVICE_ACTION_OPEN && data->type != FP_DEVICE_TASK_RETURN_ERROR) { @@ -1685,3 +1689,113 @@ fpi_device_report_finger_status_changes (FpDevice *device, return fpi_device_report_finger_status (device, finger_status); } + +static void +update_temp_timeout (FpDevice *device, gpointer user_data) +{ + FpDevicePrivate *priv = fp_device_get_instance_private (device); + + fpi_device_update_temp (device, priv->temp_last_active); +} + +/** + * fpi_device_update_temp: + * @device: The #FpDevice + * @is_active: Whether the device is now active + * + * Purely internal function to update the temperature. Also ensure that the + * state is updated once a threshold is reached. + */ +void +fpi_device_update_temp (FpDevice *device, gboolean is_active) +{ + FpDevicePrivate *priv = fp_device_get_instance_private (device); + gint64 now = g_get_monotonic_time (); + gdouble passed_seconds; + gdouble alpha; + gdouble next_threshold; + gdouble old_ratio; + FpTemperature old_temp; + g_autofree char *old_temp_str = NULL; + g_autofree char *new_temp_str = NULL; + + if (priv->temp_hot_seconds < 0) + { + g_debug ("Not updating temperature model, device can run continuously!"); + return; + } + + passed_seconds = (now - priv->temp_last_update) / 1e6; + old_ratio = priv->temp_current_ratio; + + if (priv->temp_last_active) + { + alpha = exp (-passed_seconds / priv->temp_hot_seconds); + priv->temp_current_ratio = alpha * priv->temp_current_ratio + 1 - alpha; + } + else + { + alpha = exp (-passed_seconds / priv->temp_cold_seconds); + priv->temp_current_ratio = alpha * priv->temp_current_ratio; + } + + priv->temp_last_active = is_active; + priv->temp_last_update = now; + + old_temp = priv->temp_current; + if (priv->temp_current_ratio < TEMP_COLD_THRESH) + { + priv->temp_current = FP_TEMPERATURE_COLD; + next_threshold = is_active ? TEMP_COLD_THRESH : -1.0; + } + else if (priv->temp_current_ratio < TEMP_HOT_WARM_THRESH) + { + priv->temp_current = FP_TEMPERATURE_WARM; + next_threshold = is_active ? TEMP_WARM_HOT_THRESH : TEMP_COLD_THRESH; + } + else if (priv->temp_current_ratio < TEMP_WARM_HOT_THRESH) + { + /* Keep HOT until we reach TEMP_HOT_WARM_THRESH */ + if (priv->temp_current != FP_TEMPERATURE_HOT) + priv->temp_current = FP_TEMPERATURE_WARM; + + next_threshold = is_active ? TEMP_WARM_HOT_THRESH : TEMP_HOT_WARM_THRESH; + } + else + { + priv->temp_current = FP_TEMPERATURE_HOT; + next_threshold = is_active ? -1.0 : TEMP_HOT_WARM_THRESH; + } + + old_temp_str = g_enum_to_string (FP_TYPE_TEMPERATURE, old_temp); + new_temp_str = g_enum_to_string (FP_TYPE_TEMPERATURE, priv->temp_current); + g_debug ("Updated temperature model after %0.2f seconds, ratio %0.2f -> %0.2f, active %d -> %d, %s -> %s", + passed_seconds, + old_ratio, + priv->temp_current_ratio, + priv->temp_last_active, + is_active, + old_temp_str, + new_temp_str); + + if (priv->temp_current != old_temp) + g_object_notify (G_OBJECT (device), "temperature"); + + g_clear_pointer (&priv->temp_timeout, g_source_destroy); + + if (next_threshold < 0) + return; + + /* Set passed_seconds to the time until the next update is needed */ + if (is_active) + passed_seconds = -priv->temp_hot_seconds * log ((next_threshold - 1.0) / (priv->temp_current_ratio - 1.0)); + else + passed_seconds = -priv->temp_cold_seconds * log (next_threshold / priv->temp_current_ratio); + + passed_seconds += TEMP_DELAY_SECONDS; + + priv->temp_timeout = fpi_device_add_timeout (device, + passed_seconds * 1000, + update_temp_timeout, + NULL, NULL); +} diff --git a/libfprint/fpi-device.h b/libfprint/fpi-device.h index eb8b8fe..f52e435 100644 --- a/libfprint/fpi-device.h +++ b/libfprint/fpi-device.h @@ -82,6 +82,10 @@ struct _FpIdEntry * fpi_device_set_nr_enroll_stages() from @probe if this is dynamic. * @scan_type: The scan type of supported devices; use * fpi_device_set_scan_type() from @probe if this is dynamic. + * @temp_hot_seconds: Assumed time in seconds for the device to become too hot + * after being mostly cold. Set to -1 if the device can be always-on. + * @temp_cold_seconds: Assumed time in seconds for the device to be mostly cold + * after having been too hot to operate. * @usb_discover: Class method to check whether a USB device is supported by * the driver. Should return 0 if the device is unsupported and a positive * score otherwise. The default score is 50 and the driver with the highest @@ -142,6 +146,10 @@ struct _FpDeviceClass gint nr_enroll_stages; FpScanType scan_type; + /* Simple device temperature model constants */ + gint32 temp_hot_seconds; + gint32 temp_cold_seconds; + /* Callbacks */ gint (*usb_discover) (GUsbDevice *usb_device); void (*probe) (FpDevice *device);