| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * hwmon driver for HP (and some HP Compaq) business-class computers that |
| * report numeric sensor data via Windows Management Instrumentation (WMI). |
| * |
| * Copyright (C) 2023 James Seo <james@equiv.tech> |
| * |
| * References: |
| * [1] Hewlett-Packard Development Company, L.P., |
| * "HP Client Management Interface Technical White Paper", 2005. [Online]. |
| * Available: https://h20331.www2.hp.com/hpsub/downloads/cmi_whitepaper.pdf |
| * [2] Hewlett-Packard Development Company, L.P., |
| * "HP Retail Manageability", 2012. [Online]. |
| * Available: http://h10032.www1.hp.com/ctg/Manual/c03291135.pdf |
| * [3] Linux Hardware Project, A. Ponomarenko et al., |
| * "linuxhw/ACPI - Collect ACPI table dumps", 2018. [Online]. |
| * Available: https://github.com/linuxhw/ACPI |
| * [4] P. Rohár, "bmfdec - Decompile binary MOF file (BMF) from WMI buffer", |
| * 2017. [Online]. Available: https://github.com/pali/bmfdec |
| */ |
| |
| #include <linux/acpi.h> |
| #include <linux/debugfs.h> |
| #include <linux/hwmon.h> |
| #include <linux/jiffies.h> |
| #include <linux/mutex.h> |
| #include <linux/units.h> |
| #include <linux/wmi.h> |
| |
| #define HP_WMI_EVENT_NAMESPACE "root\\WMI" |
| #define HP_WMI_EVENT_CLASS "HPBIOS_BIOSEvent" |
| #define HP_WMI_EVENT_GUID "95F24279-4D7B-4334-9387-ACCDC67EF61C" |
| #define HP_WMI_NUMERIC_SENSOR_GUID "8F1F6435-9F42-42C8-BADC-0E9424F20C9A" |
| #define HP_WMI_PLATFORM_EVENTS_GUID "41227C2D-80E1-423F-8B8E-87E32755A0EB" |
| |
| /* Patterns for recognizing sensors and matching events to channels. */ |
| |
| #define HP_WMI_PATTERN_SYS_TEMP "Chassis Thermal Index" |
| #define HP_WMI_PATTERN_SYS_TEMP2 "System Ambient Temperature" |
| #define HP_WMI_PATTERN_CPU_TEMP "CPU Thermal Index" |
| #define HP_WMI_PATTERN_CPU_TEMP2 "CPU Temperature" |
| #define HP_WMI_PATTERN_TEMP_SENSOR "Thermal Index" |
| #define HP_WMI_PATTERN_TEMP_ALARM "Thermal Critical" |
| #define HP_WMI_PATTERN_INTRUSION_ALARM "Hood Intrusion" |
| #define HP_WMI_PATTERN_FAN_ALARM "Stall" |
| #define HP_WMI_PATTERN_TEMP "Temperature" |
| #define HP_WMI_PATTERN_CPU "CPU" |
| |
| /* These limits are arbitrary. The WMI implementation may vary by system. */ |
| |
| #define HP_WMI_MAX_STR_SIZE 128U |
| #define HP_WMI_MAX_PROPERTIES 32U |
| #define HP_WMI_MAX_INSTANCES 32U |
| |
| enum hp_wmi_type { |
| HP_WMI_TYPE_OTHER = 1, |
| HP_WMI_TYPE_TEMPERATURE = 2, |
| HP_WMI_TYPE_VOLTAGE = 3, |
| HP_WMI_TYPE_CURRENT = 4, |
| HP_WMI_TYPE_AIR_FLOW = 12, |
| HP_WMI_TYPE_INTRUSION = 0xabadb01, /* Custom. */ |
| }; |
| |
| enum hp_wmi_category { |
| HP_WMI_CATEGORY_SENSOR = 3, |
| }; |
| |
| enum hp_wmi_severity { |
| HP_WMI_SEVERITY_UNKNOWN = 0, |
| HP_WMI_SEVERITY_OK = 5, |
| HP_WMI_SEVERITY_DEGRADED_WARNING = 10, |
| HP_WMI_SEVERITY_MINOR_FAILURE = 15, |
| HP_WMI_SEVERITY_MAJOR_FAILURE = 20, |
| HP_WMI_SEVERITY_CRITICAL_FAILURE = 25, |
| HP_WMI_SEVERITY_NON_RECOVERABLE_ERROR = 30, |
| }; |
| |
| enum hp_wmi_status { |
| HP_WMI_STATUS_OK = 2, |
| HP_WMI_STATUS_DEGRADED = 3, |
| HP_WMI_STATUS_STRESSED = 4, |
| HP_WMI_STATUS_PREDICTIVE_FAILURE = 5, |
| HP_WMI_STATUS_ERROR = 6, |
| HP_WMI_STATUS_NON_RECOVERABLE_ERROR = 7, |
| HP_WMI_STATUS_NO_CONTACT = 12, |
| HP_WMI_STATUS_LOST_COMMUNICATION = 13, |
| HP_WMI_STATUS_ABORTED = 14, |
| HP_WMI_STATUS_SUPPORTING_ENTITY_IN_ERROR = 16, |
| |
| /* Occurs combined with one of "OK", "Degraded", and "Error" [1]. */ |
| HP_WMI_STATUS_COMPLETED = 17, |
| }; |
| |
| enum hp_wmi_units { |
| HP_WMI_UNITS_OTHER = 1, |
| HP_WMI_UNITS_DEGREES_C = 2, |
| HP_WMI_UNITS_DEGREES_F = 3, |
| HP_WMI_UNITS_DEGREES_K = 4, |
| HP_WMI_UNITS_VOLTS = 5, |
| HP_WMI_UNITS_AMPS = 6, |
| HP_WMI_UNITS_RPM = 19, |
| }; |
| |
| enum hp_wmi_property { |
| HP_WMI_PROPERTY_NAME = 0, |
| HP_WMI_PROPERTY_DESCRIPTION = 1, |
| HP_WMI_PROPERTY_SENSOR_TYPE = 2, |
| HP_WMI_PROPERTY_OTHER_SENSOR_TYPE = 3, |
| HP_WMI_PROPERTY_OPERATIONAL_STATUS = 4, |
| HP_WMI_PROPERTY_SIZE = 5, |
| HP_WMI_PROPERTY_POSSIBLE_STATES = 6, |
| HP_WMI_PROPERTY_CURRENT_STATE = 7, |
| HP_WMI_PROPERTY_BASE_UNITS = 8, |
| HP_WMI_PROPERTY_UNIT_MODIFIER = 9, |
| HP_WMI_PROPERTY_CURRENT_READING = 10, |
| HP_WMI_PROPERTY_RATE_UNITS = 11, |
| }; |
| |
| static const acpi_object_type hp_wmi_property_map[] = { |
| [HP_WMI_PROPERTY_NAME] = ACPI_TYPE_STRING, |
| [HP_WMI_PROPERTY_DESCRIPTION] = ACPI_TYPE_STRING, |
| [HP_WMI_PROPERTY_SENSOR_TYPE] = ACPI_TYPE_INTEGER, |
| [HP_WMI_PROPERTY_OTHER_SENSOR_TYPE] = ACPI_TYPE_STRING, |
| [HP_WMI_PROPERTY_OPERATIONAL_STATUS] = ACPI_TYPE_INTEGER, |
| [HP_WMI_PROPERTY_SIZE] = ACPI_TYPE_INTEGER, |
| [HP_WMI_PROPERTY_POSSIBLE_STATES] = ACPI_TYPE_STRING, |
| [HP_WMI_PROPERTY_CURRENT_STATE] = ACPI_TYPE_STRING, |
| [HP_WMI_PROPERTY_BASE_UNITS] = ACPI_TYPE_INTEGER, |
| [HP_WMI_PROPERTY_UNIT_MODIFIER] = ACPI_TYPE_INTEGER, |
| [HP_WMI_PROPERTY_CURRENT_READING] = ACPI_TYPE_INTEGER, |
| [HP_WMI_PROPERTY_RATE_UNITS] = ACPI_TYPE_INTEGER, |
| }; |
| |
| enum hp_wmi_platform_events_property { |
| HP_WMI_PLATFORM_EVENTS_PROPERTY_NAME = 0, |
| HP_WMI_PLATFORM_EVENTS_PROPERTY_DESCRIPTION = 1, |
| HP_WMI_PLATFORM_EVENTS_PROPERTY_SOURCE_NAMESPACE = 2, |
| HP_WMI_PLATFORM_EVENTS_PROPERTY_SOURCE_CLASS = 3, |
| HP_WMI_PLATFORM_EVENTS_PROPERTY_CATEGORY = 4, |
| HP_WMI_PLATFORM_EVENTS_PROPERTY_POSSIBLE_SEVERITY = 5, |
| HP_WMI_PLATFORM_EVENTS_PROPERTY_POSSIBLE_STATUS = 6, |
| }; |
| |
| static const acpi_object_type hp_wmi_platform_events_property_map[] = { |
| [HP_WMI_PLATFORM_EVENTS_PROPERTY_NAME] = ACPI_TYPE_STRING, |
| [HP_WMI_PLATFORM_EVENTS_PROPERTY_DESCRIPTION] = ACPI_TYPE_STRING, |
| [HP_WMI_PLATFORM_EVENTS_PROPERTY_SOURCE_NAMESPACE] = ACPI_TYPE_STRING, |
| [HP_WMI_PLATFORM_EVENTS_PROPERTY_SOURCE_CLASS] = ACPI_TYPE_STRING, |
| [HP_WMI_PLATFORM_EVENTS_PROPERTY_CATEGORY] = ACPI_TYPE_INTEGER, |
| [HP_WMI_PLATFORM_EVENTS_PROPERTY_POSSIBLE_SEVERITY] = ACPI_TYPE_INTEGER, |
| [HP_WMI_PLATFORM_EVENTS_PROPERTY_POSSIBLE_STATUS] = ACPI_TYPE_INTEGER, |
| }; |
| |
| enum hp_wmi_event_property { |
| HP_WMI_EVENT_PROPERTY_NAME = 0, |
| HP_WMI_EVENT_PROPERTY_DESCRIPTION = 1, |
| HP_WMI_EVENT_PROPERTY_CATEGORY = 2, |
| HP_WMI_EVENT_PROPERTY_SEVERITY = 3, |
| HP_WMI_EVENT_PROPERTY_STATUS = 4, |
| }; |
| |
| static const acpi_object_type hp_wmi_event_property_map[] = { |
| [HP_WMI_EVENT_PROPERTY_NAME] = ACPI_TYPE_STRING, |
| [HP_WMI_EVENT_PROPERTY_DESCRIPTION] = ACPI_TYPE_STRING, |
| [HP_WMI_EVENT_PROPERTY_CATEGORY] = ACPI_TYPE_INTEGER, |
| [HP_WMI_EVENT_PROPERTY_SEVERITY] = ACPI_TYPE_INTEGER, |
| [HP_WMI_EVENT_PROPERTY_STATUS] = ACPI_TYPE_INTEGER, |
| }; |
| |
| static const enum hwmon_sensor_types hp_wmi_hwmon_type_map[] = { |
| [HP_WMI_TYPE_TEMPERATURE] = hwmon_temp, |
| [HP_WMI_TYPE_VOLTAGE] = hwmon_in, |
| [HP_WMI_TYPE_CURRENT] = hwmon_curr, |
| [HP_WMI_TYPE_AIR_FLOW] = hwmon_fan, |
| }; |
| |
| static const u32 hp_wmi_hwmon_attributes[hwmon_max] = { |
| [hwmon_chip] = HWMON_C_REGISTER_TZ, |
| [hwmon_temp] = HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_FAULT, |
| [hwmon_in] = HWMON_I_INPUT | HWMON_I_LABEL, |
| [hwmon_curr] = HWMON_C_INPUT | HWMON_C_LABEL, |
| [hwmon_fan] = HWMON_F_INPUT | HWMON_F_LABEL | HWMON_F_FAULT, |
| [hwmon_intrusion] = HWMON_INTRUSION_ALARM, |
| }; |
| |
| /* |
| * struct hp_wmi_numeric_sensor - a HPBIOS_BIOSNumericSensor instance |
| * |
| * Two variants of HPBIOS_BIOSNumericSensor are known. The first is specified |
| * in [1] and appears to be much more widespread. The second was discovered by |
| * decoding BMOF blobs [4], seems to be found only in some newer ZBook systems |
| * [3], and has two new properties and a slightly different property order. |
| * |
| * These differences don't matter on Windows, where WMI object properties are |
| * accessed by name. For us, supporting both variants gets ugly and hacky at |
| * times. The fun begins now; this struct is defined as per the new variant. |
| * |
| * Effective MOF definition: |
| * |
| * #pragma namespace("\\\\.\\root\\HP\\InstrumentedBIOS"); |
| * class HPBIOS_BIOSNumericSensor { |
| * [read] string Name; |
| * [read] string Description; |
| * [read, ValueMap {"0","1","2","3","4","5","6","7","8","9", |
| * "10","11","12"}, Values {"Unknown","Other","Temperature", |
| * "Voltage","Current","Tachometer","Counter","Switch","Lock", |
| * "Humidity","Smoke Detection","Presence","Air Flow"}] |
| * uint32 SensorType; |
| * [read] string OtherSensorType; |
| * [read, ValueMap {"0","1","2","3","4","5","6","7","8","9", |
| * "10","11","12","13","14","15","16","17","18","..", |
| * "0x8000.."}, Values {"Unknown","Other","OK","Degraded", |
| * "Stressed","Predictive Failure","Error", |
| * "Non-Recoverable Error","Starting","Stopping","Stopped", |
| * "In Service","No Contact","Lost Communication","Aborted", |
| * "Dormant","Supporting Entity in Error","Completed", |
| * "Power Mode","DMTF Reserved","Vendor Reserved"}] |
| * uint32 OperationalStatus; |
| * [read] uint32 Size; |
| * [read] string PossibleStates[]; |
| * [read] string CurrentState; |
| * [read, ValueMap {"0","1","2","3","4","5","6","7","8","9", |
| * "10","11","12","13","14","15","16","17","18","19","20", |
| * "21","22","23","24","25","26","27","28","29","30","31", |
| * "32","33","34","35","36","37","38","39","40","41","42", |
| * "43","44","45","46","47","48","49","50","51","52","53", |
| * "54","55","56","57","58","59","60","61","62","63","64", |
| * "65"}, Values {"Unknown","Other","Degrees C","Degrees F", |
| * "Degrees K","Volts","Amps","Watts","Joules","Coulombs", |
| * "VA","Nits","Lumens","Lux","Candelas","kPa","PSI", |
| * "Newtons","CFM","RPM","Hertz","Seconds","Minutes", |
| * "Hours","Days","Weeks","Mils","Inches","Feet", |
| * "Cubic Inches","Cubic Feet","Meters","Cubic Centimeters", |
| * "Cubic Meters","Liters","Fluid Ounces","Radians", |
| * "Steradians","Revolutions","Cycles","Gravities","Ounces", |
| * "Pounds","Foot-Pounds","Ounce-Inches","Gauss","Gilberts", |
| * "Henries","Farads","Ohms","Siemens","Moles","Becquerels", |
| * "PPM (parts/million)","Decibels","DbA","DbC","Grays", |
| * "Sieverts","Color Temperature Degrees K","Bits","Bytes", |
| * "Words (data)","DoubleWords","QuadWords","Percentage"}] |
| * uint32 BaseUnits; |
| * [read] sint32 UnitModifier; |
| * [read] uint32 CurrentReading; |
| * [read] uint32 RateUnits; |
| * }; |
| * |
| * Effective MOF definition of old variant [1] (sans redundant info): |
| * |
| * class HPBIOS_BIOSNumericSensor { |
| * [read] string Name; |
| * [read] string Description; |
| * [read] uint32 SensorType; |
| * [read] string OtherSensorType; |
| * [read] uint32 OperationalStatus; |
| * [read] string CurrentState; |
| * [read] string PossibleStates[]; |
| * [read] uint32 BaseUnits; |
| * [read] sint32 UnitModifier; |
| * [read] uint32 CurrentReading; |
| * }; |
| */ |
| struct hp_wmi_numeric_sensor { |
| const char *name; |
| const char *description; |
| u32 sensor_type; |
| const char *other_sensor_type; /* Explains "Other" SensorType. */ |
| u32 operational_status; |
| u8 size; /* Count of PossibleStates[]. */ |
| const char **possible_states; |
| const char *current_state; |
| u32 base_units; |
| s32 unit_modifier; |
| u32 current_reading; |
| u32 rate_units; |
| }; |
| |
| /* |
| * struct hp_wmi_platform_events - a HPBIOS_PlatformEvents instance |
| * |
| * Instances of this object reveal the set of possible HPBIOS_BIOSEvent |
| * instances for the current system, but it may not always be present. |
| * |
| * Effective MOF definition: |
| * |
| * #pragma namespace("\\\\.\\root\\HP\\InstrumentedBIOS"); |
| * class HPBIOS_PlatformEvents { |
| * [read] string Name; |
| * [read] string Description; |
| * [read] string SourceNamespace; |
| * [read] string SourceClass; |
| * [read, ValueMap {"0","1","2","3","4",".."}, Values { |
| * "Unknown","Configuration Change","Button Pressed", |
| * "Sensor","BIOS Settings","Reserved"}] |
| * uint32 Category; |
| * [read, ValueMap{"0","5","10","15","20","25","30",".."}, |
| * Values{"Unknown","OK","Degraded/Warning","Minor Failure", |
| * "Major Failure","Critical Failure","Non-recoverable Error", |
| * "DMTF Reserved"}] |
| * uint32 PossibleSeverity; |
| * [read, ValueMap {"0","1","2","3","4","5","6","7","8","9", |
| * "10","11","12","13","14","15","16","17","18","..", |
| * "0x8000.."}, Values {"Unknown","Other","OK","Degraded", |
| * "Stressed","Predictive Failure","Error", |
| * "Non-Recoverable Error","Starting","Stopping","Stopped", |
| * "In Service","No Contact","Lost Communication","Aborted", |
| * "Dormant","Supporting Entity in Error","Completed", |
| * "Power Mode","DMTF Reserved","Vendor Reserved"}] |
| * uint32 PossibleStatus; |
| * }; |
| */ |
| struct hp_wmi_platform_events { |
| const char *name; |
| const char *description; |
| const char *source_namespace; |
| const char *source_class; |
| u32 category; |
| u32 possible_severity; |
| u32 possible_status; |
| }; |
| |
| /* |
| * struct hp_wmi_event - a HPBIOS_BIOSEvent instance |
| * |
| * Effective MOF definition [1] (corrected below from original): |
| * |
| * #pragma namespace("\\\\.\\root\\WMI"); |
| * class HPBIOS_BIOSEvent : WMIEvent { |
| * [read] string Name; |
| * [read] string Description; |
| * [read ValueMap {"0","1","2","3","4"}, Values {"Unknown", |
| * "Configuration Change","Button Pressed","Sensor", |
| * "BIOS Settings"}] |
| * uint32 Category; |
| * [read, ValueMap {"0","5","10","15","20","25","30"}, |
| * Values {"Unknown","OK","Degraded/Warning", |
| * "Minor Failure","Major Failure","Critical Failure", |
| * "Non-recoverable Error"}] |
| * uint32 Severity; |
| * [read, ValueMap {"0","1","2","3","4","5","6","7","8", |
| * "9","10","11","12","13","14","15","16","17","18","..", |
| * "0x8000.."}, Values {"Unknown","Other","OK","Degraded", |
| * "Stressed","Predictive Failure","Error", |
| * "Non-Recoverable Error","Starting","Stopping","Stopped", |
| * "In Service","No Contact","Lost Communication","Aborted", |
| * "Dormant","Supporting Entity in Error","Completed", |
| * "Power Mode","DMTF Reserved","Vendor Reserved"}] |
| * uint32 Status; |
| * }; |
| */ |
| struct hp_wmi_event { |
| const char *name; |
| const char *description; |
| u32 category; |
| }; |
| |
| /* |
| * struct hp_wmi_info - sensor info |
| * @nsensor: numeric sensor properties |
| * @instance: its WMI instance number |
| * @state: pointer to driver state |
| * @has_alarm: whether sensor has an alarm flag |
| * @alarm: alarm flag |
| * @type: its hwmon sensor type |
| * @cached_val: current sensor reading value, scaled for hwmon |
| * @last_updated: when these readings were last updated |
| */ |
| struct hp_wmi_info { |
| struct hp_wmi_numeric_sensor nsensor; |
| u8 instance; |
| void *state; /* void *: Avoid forward declaration. */ |
| bool has_alarm; |
| bool alarm; |
| enum hwmon_sensor_types type; |
| long cached_val; |
| unsigned long last_updated; /* In jiffies. */ |
| |
| }; |
| |
| /* |
| * struct hp_wmi_sensors - driver state |
| * @wdev: pointer to the parent WMI device |
| * @info_map: sensor info structs by hwmon type and channel number |
| * @channel_count: count of hwmon channels by hwmon type |
| * @has_intrusion: whether an intrusion sensor is present |
| * @intrusion: intrusion flag |
| * @lock: mutex to lock polling WMI and changes to driver state |
| */ |
| struct hp_wmi_sensors { |
| struct wmi_device *wdev; |
| struct hp_wmi_info **info_map[hwmon_max]; |
| u8 channel_count[hwmon_max]; |
| bool has_intrusion; |
| bool intrusion; |
| |
| struct mutex lock; /* Lock polling WMI and driver state changes. */ |
| }; |
| |
| /* hp_wmi_strdup - devm_kstrdup, but length-limited */ |
| static char *hp_wmi_strdup(struct device *dev, const char *src) |
| { |
| char *dst; |
| size_t len; |
| |
| len = strnlen(src, HP_WMI_MAX_STR_SIZE - 1); |
| |
| dst = devm_kmalloc(dev, (len + 1) * sizeof(*dst), GFP_KERNEL); |
| if (!dst) |
| return NULL; |
| |
| strscpy(dst, src, len + 1); |
| |
| return dst; |
| } |
| |
| /* |
| * hp_wmi_get_wobj - poll WMI for a WMI object instance |
| * @guid: WMI object GUID |
| * @instance: WMI object instance number |
| * |
| * Returns a new WMI object instance on success, or NULL on error. |
| * Caller must kfree() the result. |
| */ |
| static union acpi_object *hp_wmi_get_wobj(const char *guid, u8 instance) |
| { |
| struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; |
| acpi_status err; |
| |
| err = wmi_query_block(guid, instance, &out); |
| if (ACPI_FAILURE(err)) |
| return NULL; |
| |
| return out.pointer; |
| } |
| |
| /* hp_wmi_wobj_instance_count - find count of WMI object instances */ |
| static u8 hp_wmi_wobj_instance_count(const char *guid) |
| { |
| int count; |
| |
| count = wmi_instance_count(guid); |
| |
| return clamp(count, 0, (int)HP_WMI_MAX_INSTANCES); |
| } |
| |
| static int check_wobj(const union acpi_object *wobj, |
| const acpi_object_type property_map[], int last_prop) |
| { |
| acpi_object_type type = wobj->type; |
| acpi_object_type valid_type; |
| union acpi_object *elements; |
| u32 elem_count; |
| int prop; |
| |
| if (type != ACPI_TYPE_PACKAGE) |
| return -EINVAL; |
| |
| elem_count = wobj->package.count; |
| if (elem_count != last_prop + 1) |
| return -EINVAL; |
| |
| elements = wobj->package.elements; |
| for (prop = 0; prop <= last_prop; prop++) { |
| type = elements[prop].type; |
| valid_type = property_map[prop]; |
| if (type != valid_type) |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int extract_acpi_value(struct device *dev, |
| union acpi_object *element, |
| acpi_object_type type, |
| u32 *out_value, char **out_string) |
| { |
| switch (type) { |
| case ACPI_TYPE_INTEGER: |
| *out_value = element->integer.value; |
| break; |
| |
| case ACPI_TYPE_STRING: |
| *out_string = hp_wmi_strdup(dev, strim(element->string.pointer)); |
| if (!*out_string) |
| return -ENOMEM; |
| break; |
| |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * check_numeric_sensor_wobj - validate a HPBIOS_BIOSNumericSensor instance |
| * @wobj: pointer to WMI object instance to check |
| * @out_size: out pointer to count of possible states |
| * @out_is_new: out pointer to whether this is a "new" variant object |
| * |
| * Returns 0 on success, or a negative error code on error. |
| */ |
| static int check_numeric_sensor_wobj(const union acpi_object *wobj, |
| u8 *out_size, bool *out_is_new) |
| { |
| acpi_object_type type = wobj->type; |
| int prop = HP_WMI_PROPERTY_NAME; |
| acpi_object_type valid_type; |
| union acpi_object *elements; |
| u32 elem_count; |
| int last_prop; |
| bool is_new; |
| u8 count; |
| u32 j; |
| u32 i; |
| |
| if (type != ACPI_TYPE_PACKAGE) |
| return -EINVAL; |
| |
| /* |
| * elements is a variable-length array of ACPI objects, one for |
| * each property of the WMI object instance, except that the |
| * strings in PossibleStates[] are flattened into this array |
| * as if each individual string were a property by itself. |
| */ |
| elements = wobj->package.elements; |
| |
| elem_count = wobj->package.count; |
| if (elem_count <= HP_WMI_PROPERTY_SIZE || |
| elem_count > HP_WMI_MAX_PROPERTIES) |
| return -EINVAL; |
| |
| type = elements[HP_WMI_PROPERTY_SIZE].type; |
| switch (type) { |
| case ACPI_TYPE_INTEGER: |
| is_new = true; |
| last_prop = HP_WMI_PROPERTY_RATE_UNITS; |
| break; |
| |
| case ACPI_TYPE_STRING: |
| is_new = false; |
| last_prop = HP_WMI_PROPERTY_CURRENT_READING; |
| break; |
| |
| default: |
| return -EINVAL; |
| } |
| |
| /* |
| * In general, the count of PossibleStates[] must be > 0. |
| * Also, the old variant lacks the Size property, so we may need to |
| * reduce the value of last_prop by 1 when doing arithmetic with it. |
| */ |
| if (elem_count < last_prop - !is_new + 1) |
| return -EINVAL; |
| |
| count = elem_count - (last_prop - !is_new); |
| |
| for (i = 0; i < elem_count && prop <= last_prop; i++, prop++) { |
| type = elements[i].type; |
| valid_type = hp_wmi_property_map[prop]; |
| if (type != valid_type) |
| return -EINVAL; |
| |
| switch (prop) { |
| case HP_WMI_PROPERTY_OPERATIONAL_STATUS: |
| /* Old variant: CurrentState follows OperationalStatus. */ |
| if (!is_new) |
| prop = HP_WMI_PROPERTY_CURRENT_STATE - 1; |
| break; |
| |
| case HP_WMI_PROPERTY_SIZE: |
| /* New variant: Size == count of PossibleStates[]. */ |
| if (count != elements[i].integer.value) |
| return -EINVAL; |
| break; |
| |
| case HP_WMI_PROPERTY_POSSIBLE_STATES: |
| /* PossibleStates[0] has already been type-checked. */ |
| for (j = 0; i + 1 < elem_count && j + 1 < count; j++) { |
| type = elements[++i].type; |
| if (type != valid_type) |
| return -EINVAL; |
| } |
| |
| /* Old variant: BaseUnits follows PossibleStates[]. */ |
| if (!is_new) |
| prop = HP_WMI_PROPERTY_BASE_UNITS - 1; |
| break; |
| |
| case HP_WMI_PROPERTY_CURRENT_STATE: |
| /* Old variant: PossibleStates[] follows CurrentState. */ |
| if (!is_new) |
| prop = HP_WMI_PROPERTY_POSSIBLE_STATES - 1; |
| break; |
| } |
| } |
| |
| if (prop != last_prop + 1) |
| return -EINVAL; |
| |
| *out_size = count; |
| *out_is_new = is_new; |
| |
| return 0; |
| } |
| |
| static int |
| numeric_sensor_is_connected(const struct hp_wmi_numeric_sensor *nsensor) |
| { |
| u32 operational_status = nsensor->operational_status; |
| |
| return operational_status != HP_WMI_STATUS_NO_CONTACT; |
| } |
| |
| static int numeric_sensor_has_fault(const struct hp_wmi_numeric_sensor *nsensor) |
| { |
| u32 operational_status = nsensor->operational_status; |
| |
| switch (operational_status) { |
| case HP_WMI_STATUS_DEGRADED: |
| case HP_WMI_STATUS_STRESSED: /* e.g. Overload, overtemp. */ |
| case HP_WMI_STATUS_PREDICTIVE_FAILURE: /* e.g. Fan removed. */ |
| case HP_WMI_STATUS_ERROR: |
| case HP_WMI_STATUS_NON_RECOVERABLE_ERROR: |
| case HP_WMI_STATUS_NO_CONTACT: |
| case HP_WMI_STATUS_LOST_COMMUNICATION: |
| case HP_WMI_STATUS_ABORTED: |
| case HP_WMI_STATUS_SUPPORTING_ENTITY_IN_ERROR: |
| |
| /* Assume combination by addition; bitwise OR doesn't make sense. */ |
| case HP_WMI_STATUS_COMPLETED + HP_WMI_STATUS_DEGRADED: |
| case HP_WMI_STATUS_COMPLETED + HP_WMI_STATUS_ERROR: |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /* scale_numeric_sensor - scale sensor reading for hwmon */ |
| static long scale_numeric_sensor(const struct hp_wmi_numeric_sensor *nsensor) |
| { |
| u32 current_reading = nsensor->current_reading; |
| s32 unit_modifier = nsensor->unit_modifier; |
| u32 sensor_type = nsensor->sensor_type; |
| u32 base_units = nsensor->base_units; |
| s32 target_modifier; |
| long val; |
| |
| /* Fan readings are in RPM units; others are in milliunits. */ |
| target_modifier = sensor_type == HP_WMI_TYPE_AIR_FLOW ? 0 : -3; |
| |
| val = current_reading; |
| |
| for (; unit_modifier < target_modifier; unit_modifier++) |
| val = DIV_ROUND_CLOSEST(val, 10); |
| |
| for (; unit_modifier > target_modifier; unit_modifier--) { |
| if (val > LONG_MAX / 10) { |
| val = LONG_MAX; |
| break; |
| } |
| val *= 10; |
| } |
| |
| if (sensor_type == HP_WMI_TYPE_TEMPERATURE) { |
| switch (base_units) { |
| case HP_WMI_UNITS_DEGREES_F: |
| val -= MILLI * 32; |
| val = val <= LONG_MAX / 5 ? |
| DIV_ROUND_CLOSEST(val * 5, 9) : |
| DIV_ROUND_CLOSEST(val, 9) * 5; |
| break; |
| |
| case HP_WMI_UNITS_DEGREES_K: |
| val = milli_kelvin_to_millicelsius(val); |
| break; |
| } |
| } |
| |
| return val; |
| } |
| |
| /* |
| * classify_numeric_sensor - classify a numeric sensor |
| * @nsensor: pointer to numeric sensor struct |
| * |
| * Returns an enum hp_wmi_type value on success, |
| * or a negative value if the sensor type is unsupported. |
| */ |
| static int classify_numeric_sensor(const struct hp_wmi_numeric_sensor *nsensor) |
| { |
| u32 sensor_type = nsensor->sensor_type; |
| u32 base_units = nsensor->base_units; |
| const char *name = nsensor->name; |
| |
| switch (sensor_type) { |
| case HP_WMI_TYPE_TEMPERATURE: |
| /* |
| * Some systems have sensors named "X Thermal Index" in "Other" |
| * units. Tested CPU sensor examples were found to be in °C, |
| * albeit perhaps "differently" accurate; e.g. readings were |
| * reliably -6°C vs. coretemp on a HP Compaq Elite 8300, and |
| * +8°C on an EliteOne G1 800. But this is still within the |
| * realm of plausibility for cheaply implemented motherboard |
| * sensors, and chassis readings were about as expected. |
| */ |
| if ((base_units == HP_WMI_UNITS_OTHER && |
| strstr(name, HP_WMI_PATTERN_TEMP_SENSOR)) || |
| base_units == HP_WMI_UNITS_DEGREES_C || |
| base_units == HP_WMI_UNITS_DEGREES_F || |
| base_units == HP_WMI_UNITS_DEGREES_K) |
| return HP_WMI_TYPE_TEMPERATURE; |
| break; |
| |
| case HP_WMI_TYPE_VOLTAGE: |
| if (base_units == HP_WMI_UNITS_VOLTS) |
| return HP_WMI_TYPE_VOLTAGE; |
| break; |
| |
| case HP_WMI_TYPE_CURRENT: |
| if (base_units == HP_WMI_UNITS_AMPS) |
| return HP_WMI_TYPE_CURRENT; |
| break; |
| |
| case HP_WMI_TYPE_AIR_FLOW: |
| /* |
| * Strangely, HP considers fan RPM sensor type to be |
| * "Air Flow" instead of the more intuitive "Tachometer". |
| */ |
| if (base_units == HP_WMI_UNITS_RPM) |
| return HP_WMI_TYPE_AIR_FLOW; |
| break; |
| } |
| |
| return -EINVAL; |
| } |
| |
| static int |
| populate_numeric_sensor_from_wobj(struct device *dev, |
| struct hp_wmi_numeric_sensor *nsensor, |
| union acpi_object *wobj, bool *out_is_new) |
| { |
| int last_prop = HP_WMI_PROPERTY_RATE_UNITS; |
| int prop = HP_WMI_PROPERTY_NAME; |
| const char **possible_states; |
| union acpi_object *element; |
| acpi_object_type type; |
| char *string; |
| bool is_new; |
| u32 value; |
| u8 size; |
| int err; |
| |
| err = check_numeric_sensor_wobj(wobj, &size, &is_new); |
| if (err) |
| return err; |
| |
| possible_states = devm_kcalloc(dev, size, sizeof(*possible_states), |
| GFP_KERNEL); |
| if (!possible_states) |
| return -ENOMEM; |
| |
| element = wobj->package.elements; |
| nsensor->possible_states = possible_states; |
| nsensor->size = size; |
| |
| if (!is_new) |
| last_prop = HP_WMI_PROPERTY_CURRENT_READING; |
| |
| for (; prop <= last_prop; prop++) { |
| type = hp_wmi_property_map[prop]; |
| |
| err = extract_acpi_value(dev, element, type, &value, &string); |
| if (err) |
| return err; |
| |
| element++; |
| |
| switch (prop) { |
| case HP_WMI_PROPERTY_NAME: |
| nsensor->name = string; |
| break; |
| |
| case HP_WMI_PROPERTY_DESCRIPTION: |
| nsensor->description = string; |
| break; |
| |
| case HP_WMI_PROPERTY_SENSOR_TYPE: |
| if (value > HP_WMI_TYPE_AIR_FLOW) |
| return -EINVAL; |
| |
| nsensor->sensor_type = value; |
| break; |
| |
| case HP_WMI_PROPERTY_OTHER_SENSOR_TYPE: |
| nsensor->other_sensor_type = string; |
| break; |
| |
| case HP_WMI_PROPERTY_OPERATIONAL_STATUS: |
| nsensor->operational_status = value; |
| |
| /* Old variant: CurrentState follows OperationalStatus. */ |
| if (!is_new) |
| prop = HP_WMI_PROPERTY_CURRENT_STATE - 1; |
| break; |
| |
| case HP_WMI_PROPERTY_SIZE: |
| break; /* Already set. */ |
| |
| case HP_WMI_PROPERTY_POSSIBLE_STATES: |
| *possible_states++ = string; |
| if (--size) |
| prop--; |
| |
| /* Old variant: BaseUnits follows PossibleStates[]. */ |
| if (!is_new && !size) |
| prop = HP_WMI_PROPERTY_BASE_UNITS - 1; |
| break; |
| |
| case HP_WMI_PROPERTY_CURRENT_STATE: |
| nsensor->current_state = string; |
| |
| /* Old variant: PossibleStates[] follows CurrentState. */ |
| if (!is_new) |
| prop = HP_WMI_PROPERTY_POSSIBLE_STATES - 1; |
| break; |
| |
| case HP_WMI_PROPERTY_BASE_UNITS: |
| nsensor->base_units = value; |
| break; |
| |
| case HP_WMI_PROPERTY_UNIT_MODIFIER: |
| /* UnitModifier is signed. */ |
| nsensor->unit_modifier = (s32)value; |
| break; |
| |
| case HP_WMI_PROPERTY_CURRENT_READING: |
| nsensor->current_reading = value; |
| break; |
| |
| case HP_WMI_PROPERTY_RATE_UNITS: |
| nsensor->rate_units = value; |
| break; |
| |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| *out_is_new = is_new; |
| |
| return 0; |
| } |
| |
| /* update_numeric_sensor_from_wobj - update fungible sensor properties */ |
| static void |
| update_numeric_sensor_from_wobj(struct device *dev, |
| struct hp_wmi_numeric_sensor *nsensor, |
| const union acpi_object *wobj) |
| { |
| const union acpi_object *elements; |
| const union acpi_object *element; |
| const char *string; |
| bool is_new; |
| int offset; |
| u8 size; |
| int err; |
| |
| err = check_numeric_sensor_wobj(wobj, &size, &is_new); |
| if (err) |
| return; |
| |
| elements = wobj->package.elements; |
| |
| element = &elements[HP_WMI_PROPERTY_OPERATIONAL_STATUS]; |
| nsensor->operational_status = element->integer.value; |
| |
| /* |
| * In general, an index offset is needed after PossibleStates[0]. |
| * On a new variant, CurrentState is after PossibleStates[]. This is |
| * not the case on an old variant, but we still need to offset the |
| * read because CurrentState is where Size would be on a new variant. |
| */ |
| offset = is_new ? size - 1 : -2; |
| |
| element = &elements[HP_WMI_PROPERTY_CURRENT_STATE + offset]; |
| string = strim(element->string.pointer); |
| |
| if (strcmp(string, nsensor->current_state)) { |
| devm_kfree(dev, nsensor->current_state); |
| nsensor->current_state = hp_wmi_strdup(dev, string); |
| } |
| |
| /* Old variant: -2 (not -1) because it lacks the Size property. */ |
| if (!is_new) |
| offset = (int)size - 2; /* size is > 0, i.e. may be 1. */ |
| |
| element = &elements[HP_WMI_PROPERTY_UNIT_MODIFIER + offset]; |
| nsensor->unit_modifier = (s32)element->integer.value; |
| |
| element = &elements[HP_WMI_PROPERTY_CURRENT_READING + offset]; |
| nsensor->current_reading = element->integer.value; |
| } |
| |
| /* |
| * check_platform_events_wobj - validate a HPBIOS_PlatformEvents instance |
| * @wobj: pointer to WMI object instance to check |
| * |
| * Returns 0 on success, or a negative error code on error. |
| */ |
| static int check_platform_events_wobj(const union acpi_object *wobj) |
| { |
| return check_wobj(wobj, hp_wmi_platform_events_property_map, |
| HP_WMI_PLATFORM_EVENTS_PROPERTY_POSSIBLE_STATUS); |
| } |
| |
| static int |
| populate_platform_events_from_wobj(struct device *dev, |
| struct hp_wmi_platform_events *pevents, |
| union acpi_object *wobj) |
| { |
| int last_prop = HP_WMI_PLATFORM_EVENTS_PROPERTY_POSSIBLE_STATUS; |
| int prop = HP_WMI_PLATFORM_EVENTS_PROPERTY_NAME; |
| union acpi_object *element; |
| acpi_object_type type; |
| char *string; |
| u32 value; |
| int err; |
| |
| err = check_platform_events_wobj(wobj); |
| if (err) |
| return err; |
| |
| element = wobj->package.elements; |
| |
| for (; prop <= last_prop; prop++, element++) { |
| type = hp_wmi_platform_events_property_map[prop]; |
| |
| err = extract_acpi_value(dev, element, type, &value, &string); |
| if (err) |
| return err; |
| |
| switch (prop) { |
| case HP_WMI_PLATFORM_EVENTS_PROPERTY_NAME: |
| pevents->name = string; |
| break; |
| |
| case HP_WMI_PLATFORM_EVENTS_PROPERTY_DESCRIPTION: |
| pevents->description = string; |
| break; |
| |
| case HP_WMI_PLATFORM_EVENTS_PROPERTY_SOURCE_NAMESPACE: |
| if (strcasecmp(HP_WMI_EVENT_NAMESPACE, string)) |
| return -EINVAL; |
| |
| pevents->source_namespace = string; |
| break; |
| |
| case HP_WMI_PLATFORM_EVENTS_PROPERTY_SOURCE_CLASS: |
| if (strcasecmp(HP_WMI_EVENT_CLASS, string)) |
| return -EINVAL; |
| |
| pevents->source_class = string; |
| break; |
| |
| case HP_WMI_PLATFORM_EVENTS_PROPERTY_CATEGORY: |
| pevents->category = value; |
| break; |
| |
| case HP_WMI_PLATFORM_EVENTS_PROPERTY_POSSIBLE_SEVERITY: |
| pevents->possible_severity = value; |
| break; |
| |
| case HP_WMI_PLATFORM_EVENTS_PROPERTY_POSSIBLE_STATUS: |
| pevents->possible_status = value; |
| break; |
| |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * check_event_wobj - validate a HPBIOS_BIOSEvent instance |
| * @wobj: pointer to WMI object instance to check |
| * |
| * Returns 0 on success, or a negative error code on error. |
| */ |
| static int check_event_wobj(const union acpi_object *wobj) |
| { |
| return check_wobj(wobj, hp_wmi_event_property_map, |
| HP_WMI_EVENT_PROPERTY_STATUS); |
| } |
| |
| static int populate_event_from_wobj(struct hp_wmi_event *event, |
| union acpi_object *wobj) |
| { |
| int prop = HP_WMI_EVENT_PROPERTY_NAME; |
| union acpi_object *element; |
| int err; |
| |
| err = check_event_wobj(wobj); |
| if (err) |
| return err; |
| |
| element = wobj->package.elements; |
| |
| /* Extracted strings are NOT device-managed copies. */ |
| |
| for (; prop <= HP_WMI_EVENT_PROPERTY_CATEGORY; prop++, element++) { |
| switch (prop) { |
| case HP_WMI_EVENT_PROPERTY_NAME: |
| event->name = strim(element->string.pointer); |
| break; |
| |
| case HP_WMI_EVENT_PROPERTY_DESCRIPTION: |
| event->description = strim(element->string.pointer); |
| break; |
| |
| case HP_WMI_EVENT_PROPERTY_CATEGORY: |
| event->category = element->integer.value; |
| break; |
| |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * classify_event - classify an event |
| * @name: event name |
| * @category: event category |
| * |
| * Classify instances of both HPBIOS_PlatformEvents and HPBIOS_BIOSEvent from |
| * property values. Recognition criteria are based on multiple ACPI dumps [3]. |
| * |
| * Returns an enum hp_wmi_type value on success, |
| * or a negative value if the event type is unsupported. |
| */ |
| static int classify_event(const char *event_name, u32 category) |
| { |
| if (category != HP_WMI_CATEGORY_SENSOR) |
| return -EINVAL; |
| |
| /* Fan events have Name "X Stall". */ |
| if (strstr(event_name, HP_WMI_PATTERN_FAN_ALARM)) |
| return HP_WMI_TYPE_AIR_FLOW; |
| |
| /* Intrusion events have Name "Hood Intrusion". */ |
| if (!strcmp(event_name, HP_WMI_PATTERN_INTRUSION_ALARM)) |
| return HP_WMI_TYPE_INTRUSION; |
| |
| /* |
| * Temperature events have Name either "Thermal Caution" or |
| * "Thermal Critical". Deal only with "Thermal Critical" events. |
| * |
| * "Thermal Caution" events have Status "Stressed", informing us that |
| * the OperationalStatus of the related sensor has become "Stressed". |
| * However, this is already a fault condition that will clear itself |
| * when the sensor recovers, so we have no further interest in them. |
| */ |
| if (!strcmp(event_name, HP_WMI_PATTERN_TEMP_ALARM)) |
| return HP_WMI_TYPE_TEMPERATURE; |
| |
| return -EINVAL; |
| } |
| |
| /* |
| * interpret_info - interpret sensor for hwmon |
| * @info: pointer to sensor info struct |
| * |
| * Should be called after the numeric sensor member has been updated. |
| */ |
| static void interpret_info(struct hp_wmi_info *info) |
| { |
| const struct hp_wmi_numeric_sensor *nsensor = &info->nsensor; |
| |
| info->cached_val = scale_numeric_sensor(nsensor); |
| info->last_updated = jiffies; |
| } |
| |
| /* |
| * hp_wmi_update_info - poll WMI to update sensor info |
| * @state: pointer to driver state |
| * @info: pointer to sensor info struct |
| * |
| * Returns 0 on success, or a negative error code on error. |
| */ |
| static int hp_wmi_update_info(struct hp_wmi_sensors *state, |
| struct hp_wmi_info *info) |
| { |
| struct hp_wmi_numeric_sensor *nsensor = &info->nsensor; |
| struct device *dev = &state->wdev->dev; |
| const union acpi_object *wobj; |
| u8 instance = info->instance; |
| int ret = 0; |
| |
| if (time_after(jiffies, info->last_updated + HZ)) { |
| mutex_lock(&state->lock); |
| |
| wobj = hp_wmi_get_wobj(HP_WMI_NUMERIC_SENSOR_GUID, instance); |
| if (!wobj) { |
| ret = -EIO; |
| goto out_unlock; |
| } |
| |
| update_numeric_sensor_from_wobj(dev, nsensor, wobj); |
| |
| interpret_info(info); |
| |
| kfree(wobj); |
| |
| out_unlock: |
| mutex_unlock(&state->lock); |
| } |
| |
| return ret; |
| } |
| |
| static int basic_string_show(struct seq_file *seqf, void *ignored) |
| { |
| const char *str = seqf->private; |
| |
| seq_printf(seqf, "%s\n", str); |
| |
| return 0; |
| } |
| DEFINE_SHOW_ATTRIBUTE(basic_string); |
| |
| static int fungible_show(struct seq_file *seqf, enum hp_wmi_property prop) |
| { |
| struct hp_wmi_numeric_sensor *nsensor; |
| struct hp_wmi_sensors *state; |
| struct hp_wmi_info *info; |
| int err; |
| |
| info = seqf->private; |
| state = info->state; |
| nsensor = &info->nsensor; |
| |
| err = hp_wmi_update_info(state, info); |
| if (err) |
| return err; |
| |
| switch (prop) { |
| case HP_WMI_PROPERTY_OPERATIONAL_STATUS: |
| seq_printf(seqf, "%u\n", nsensor->operational_status); |
| break; |
| |
| case HP_WMI_PROPERTY_CURRENT_STATE: |
| seq_printf(seqf, "%s\n", nsensor->current_state); |
| break; |
| |
| case HP_WMI_PROPERTY_UNIT_MODIFIER: |
| seq_printf(seqf, "%d\n", nsensor->unit_modifier); |
| break; |
| |
| case HP_WMI_PROPERTY_CURRENT_READING: |
| seq_printf(seqf, "%u\n", nsensor->current_reading); |
| break; |
| |
| default: |
| return -EOPNOTSUPP; |
| } |
| |
| return 0; |
| } |
| |
| static int operational_status_show(struct seq_file *seqf, void *ignored) |
| { |
| return fungible_show(seqf, HP_WMI_PROPERTY_OPERATIONAL_STATUS); |
| } |
| DEFINE_SHOW_ATTRIBUTE(operational_status); |
| |
| static int current_state_show(struct seq_file *seqf, void *ignored) |
| { |
| return fungible_show(seqf, HP_WMI_PROPERTY_CURRENT_STATE); |
| } |
| DEFINE_SHOW_ATTRIBUTE(current_state); |
| |
| static int possible_states_show(struct seq_file *seqf, void *ignored) |
| { |
| struct hp_wmi_numeric_sensor *nsensor = seqf->private; |
| u8 i; |
| |
| for (i = 0; i < nsensor->size; i++) |
| seq_printf(seqf, "%s%s", i ? "," : "", |
| nsensor->possible_states[i]); |
| |
| seq_puts(seqf, "\n"); |
| |
| return 0; |
| } |
| DEFINE_SHOW_ATTRIBUTE(possible_states); |
| |
| static int unit_modifier_show(struct seq_file *seqf, void *ignored) |
| { |
| return fungible_show(seqf, HP_WMI_PROPERTY_UNIT_MODIFIER); |
| } |
| DEFINE_SHOW_ATTRIBUTE(unit_modifier); |
| |
| static int current_reading_show(struct seq_file *seqf, void *ignored) |
| { |
| return fungible_show(seqf, HP_WMI_PROPERTY_CURRENT_READING); |
| } |
| DEFINE_SHOW_ATTRIBUTE(current_reading); |
| |
| /* hp_wmi_devm_debugfs_remove - devm callback for debugfs cleanup */ |
| static void hp_wmi_devm_debugfs_remove(void *res) |
| { |
| debugfs_remove_recursive(res); |
| } |
| |
| /* hp_wmi_debugfs_init - create and populate debugfs directory tree */ |
| static void hp_wmi_debugfs_init(struct device *dev, struct hp_wmi_info *info, |
| struct hp_wmi_platform_events *pevents, |
| u8 icount, u8 pcount, bool is_new) |
| { |
| struct hp_wmi_numeric_sensor *nsensor; |
| char buf[HP_WMI_MAX_STR_SIZE]; |
| struct dentry *debugfs; |
| struct dentry *entries; |
| struct dentry *dir; |
| int err; |
| u8 i; |
| |
| /* dev_name() gives a not-very-friendly GUID for WMI devices. */ |
| scnprintf(buf, sizeof(buf), "hp-wmi-sensors-%u", dev->id); |
| |
| debugfs = debugfs_create_dir(buf, NULL); |
| if (IS_ERR(debugfs)) |
| return; |
| |
| err = devm_add_action_or_reset(dev, hp_wmi_devm_debugfs_remove, |
| debugfs); |
| if (err) |
| return; |
| |
| entries = debugfs_create_dir("sensor", debugfs); |
| |
| for (i = 0; i < icount; i++, info++) { |
| nsensor = &info->nsensor; |
| |
| scnprintf(buf, sizeof(buf), "%u", i); |
| dir = debugfs_create_dir(buf, entries); |
| |
| debugfs_create_file("name", 0444, dir, |
| (void *)nsensor->name, |
| &basic_string_fops); |
| |
| debugfs_create_file("description", 0444, dir, |
| (void *)nsensor->description, |
| &basic_string_fops); |
| |
| debugfs_create_u32("sensor_type", 0444, dir, |
| &nsensor->sensor_type); |
| |
| debugfs_create_file("other_sensor_type", 0444, dir, |
| (void *)nsensor->other_sensor_type, |
| &basic_string_fops); |
| |
| debugfs_create_file("operational_status", 0444, dir, |
| info, &operational_status_fops); |
| |
| debugfs_create_file("possible_states", 0444, dir, |
| nsensor, &possible_states_fops); |
| |
| debugfs_create_file("current_state", 0444, dir, |
| info, ¤t_state_fops); |
| |
| debugfs_create_u32("base_units", 0444, dir, |
| &nsensor->base_units); |
| |
| debugfs_create_file("unit_modifier", 0444, dir, |
| info, &unit_modifier_fops); |
| |
| debugfs_create_file("current_reading", 0444, dir, |
| info, ¤t_reading_fops); |
| |
| if (is_new) |
| debugfs_create_u32("rate_units", 0444, dir, |
| &nsensor->rate_units); |
| } |
| |
| if (!pcount) |
| return; |
| |
| entries = debugfs_create_dir("platform_events", debugfs); |
| |
| for (i = 0; i < pcount; i++, pevents++) { |
| scnprintf(buf, sizeof(buf), "%u", i); |
| dir = debugfs_create_dir(buf, entries); |
| |
| debugfs_create_file("name", 0444, dir, |
| (void *)pevents->name, |
| &basic_string_fops); |
| |
| debugfs_create_file("description", 0444, dir, |
| (void *)pevents->description, |
| &basic_string_fops); |
| |
| debugfs_create_file("source_namespace", 0444, dir, |
| (void *)pevents->source_namespace, |
| &basic_string_fops); |
| |
| debugfs_create_file("source_class", 0444, dir, |
| (void *)pevents->source_class, |
| &basic_string_fops); |
| |
| debugfs_create_u32("category", 0444, dir, |
| &pevents->category); |
| |
| debugfs_create_u32("possible_severity", 0444, dir, |
| &pevents->possible_severity); |
| |
| debugfs_create_u32("possible_status", 0444, dir, |
| &pevents->possible_status); |
| } |
| } |
| |
| static umode_t hp_wmi_hwmon_is_visible(const void *drvdata, |
| enum hwmon_sensor_types type, |
| u32 attr, int channel) |
| { |
| const struct hp_wmi_sensors *state = drvdata; |
| const struct hp_wmi_info *info; |
| |
| if (type == hwmon_intrusion) |
| return state->has_intrusion ? 0644 : 0; |
| |
| if (!state->info_map[type] || !state->info_map[type][channel]) |
| return 0; |
| |
| info = state->info_map[type][channel]; |
| |
| if ((type == hwmon_temp && attr == hwmon_temp_alarm) || |
| (type == hwmon_fan && attr == hwmon_fan_alarm)) |
| return info->has_alarm ? 0444 : 0; |
| |
| return 0444; |
| } |
| |
| static int hp_wmi_hwmon_read(struct device *dev, enum hwmon_sensor_types type, |
| u32 attr, int channel, long *out_val) |
| { |
| struct hp_wmi_sensors *state = dev_get_drvdata(dev); |
| const struct hp_wmi_numeric_sensor *nsensor; |
| struct hp_wmi_info *info; |
| int err; |
| |
| if (type == hwmon_intrusion) { |
| *out_val = state->intrusion ? 1 : 0; |
| |
| return 0; |
| } |
| |
| info = state->info_map[type][channel]; |
| |
| if ((type == hwmon_temp && attr == hwmon_temp_alarm) || |
| (type == hwmon_fan && attr == hwmon_fan_alarm)) { |
| *out_val = info->alarm ? 1 : 0; |
| info->alarm = false; |
| |
| return 0; |
| } |
| |
| nsensor = &info->nsensor; |
| |
| err = hp_wmi_update_info(state, info); |
| if (err) |
| return err; |
| |
| if ((type == hwmon_temp && attr == hwmon_temp_fault) || |
| (type == hwmon_fan && attr == hwmon_fan_fault)) |
| *out_val = numeric_sensor_has_fault(nsensor); |
| else |
| *out_val = info->cached_val; |
| |
| return 0; |
| } |
| |
| static int hp_wmi_hwmon_read_string(struct device *dev, |
| enum hwmon_sensor_types type, u32 attr, |
| int channel, const char **out_str) |
| { |
| const struct hp_wmi_sensors *state = dev_get_drvdata(dev); |
| const struct hp_wmi_info *info; |
| |
| info = state->info_map[type][channel]; |
| *out_str = info->nsensor.name; |
| |
| return 0; |
| } |
| |
| static int hp_wmi_hwmon_write(struct device *dev, enum hwmon_sensor_types type, |
| u32 attr, int channel, long val) |
| { |
| struct hp_wmi_sensors *state = dev_get_drvdata(dev); |
| |
| if (val) |
| return -EINVAL; |
| |
| mutex_lock(&state->lock); |
| |
| state->intrusion = false; |
| |
| mutex_unlock(&state->lock); |
| |
| return 0; |
| } |
| |
| static const struct hwmon_ops hp_wmi_hwmon_ops = { |
| .is_visible = hp_wmi_hwmon_is_visible, |
| .read = hp_wmi_hwmon_read, |
| .read_string = hp_wmi_hwmon_read_string, |
| .write = hp_wmi_hwmon_write, |
| }; |
| |
| static struct hwmon_chip_info hp_wmi_chip_info = { |
| .ops = &hp_wmi_hwmon_ops, |
| .info = NULL, |
| }; |
| |
| static struct hp_wmi_info *match_fan_event(struct hp_wmi_sensors *state, |
| const char *event_description) |
| { |
| struct hp_wmi_info **ptr_info = state->info_map[hwmon_fan]; |
| u8 fan_count = state->channel_count[hwmon_fan]; |
| struct hp_wmi_info *info; |
| const char *name; |
| u8 i; |
| |
| /* Fan event has Description "X Speed". Sensor has Name "X[ Speed]". */ |
| |
| for (i = 0; i < fan_count; i++, ptr_info++) { |
| info = *ptr_info; |
| name = info->nsensor.name; |
| |
| if (strstr(event_description, name)) |
| return info; |
| } |
| |
| return NULL; |
| } |
| |
| static u8 match_temp_events(struct hp_wmi_sensors *state, |
| const char *event_description, |
| struct hp_wmi_info *temp_info[]) |
| { |
| struct hp_wmi_info **ptr_info = state->info_map[hwmon_temp]; |
| u8 temp_count = state->channel_count[hwmon_temp]; |
| struct hp_wmi_info *info; |
| const char *name; |
| u8 count = 0; |
| bool is_cpu; |
| bool is_sys; |
| u8 i; |
| |
| /* Description is either "CPU Thermal Index" or "Chassis Thermal Index". */ |
| |
| is_cpu = !strcmp(event_description, HP_WMI_PATTERN_CPU_TEMP); |
| is_sys = !strcmp(event_description, HP_WMI_PATTERN_SYS_TEMP); |
| if (!is_cpu && !is_sys) |
| return 0; |
| |
| /* |
| * CPU event: Match one sensor with Name either "CPU Thermal Index" or |
| * "CPU Temperature", or multiple with Name(s) "CPU[#] Temperature". |
| * |
| * Chassis event: Match one sensor with Name either |
| * "Chassis Thermal Index" or "System Ambient Temperature". |
| */ |
| |
| for (i = 0; i < temp_count; i++, ptr_info++) { |
| info = *ptr_info; |
| name = info->nsensor.name; |
| |
| if ((is_cpu && (!strcmp(name, HP_WMI_PATTERN_CPU_TEMP) || |
| !strcmp(name, HP_WMI_PATTERN_CPU_TEMP2))) || |
| (is_sys && (!strcmp(name, HP_WMI_PATTERN_SYS_TEMP) || |
| !strcmp(name, HP_WMI_PATTERN_SYS_TEMP2)))) { |
| temp_info[0] = info; |
| return 1; |
| } |
| |
| if (is_cpu && (strstr(name, HP_WMI_PATTERN_CPU) && |
| strstr(name, HP_WMI_PATTERN_TEMP))) |
| temp_info[count++] = info; |
| } |
| |
| return count; |
| } |
| |
| /* hp_wmi_devm_debugfs_remove - devm callback for WMI event handler removal */ |
| static void hp_wmi_devm_notify_remove(void *ignored) |
| { |
| wmi_remove_notify_handler(HP_WMI_EVENT_GUID); |
| } |
| |
| /* hp_wmi_notify - WMI event notification handler */ |
| static void hp_wmi_notify(u32 value, void *context) |
| { |
| struct hp_wmi_info *temp_info[HP_WMI_MAX_INSTANCES] = {}; |
| struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; |
| struct hp_wmi_sensors *state = context; |
| struct device *dev = &state->wdev->dev; |
| struct hp_wmi_info *fan_info; |
| struct hp_wmi_event event; |
| union acpi_object *wobj; |
| acpi_status err; |
| int event_type; |
| u8 count; |
| |
| /* |
| * The following warning may occur in the kernel log: |
| * |
| * ACPI Warning: \_SB.WMID._WED: Return type mismatch - |
| * found Package, expected Integer/String/Buffer |
| * |
| * After using [4] to decode BMOF blobs found in [3], careless copying |
| * of BIOS code seems the most likely explanation for this warning. |
| * HP_WMI_EVENT_GUID refers to \\.\root\WMI\HPBIOS_BIOSEvent on |
| * business-class systems, but it refers to \\.\root\WMI\hpqBEvnt on |
| * non-business-class systems. Per the existing hp-wmi driver, it |
| * looks like an instance of hpqBEvnt delivered as event data may |
| * indeed take the form of a raw ACPI_BUFFER on non-business-class |
| * systems ("may" because ASL shows some BIOSes do strange things). |
| * |
| * In any case, we can ignore this warning, because we always validate |
| * the event data to ensure it is an ACPI_PACKAGE containing a |
| * HPBIOS_BIOSEvent instance. |
| */ |
| |
| mutex_lock(&state->lock); |
| |
| err = wmi_get_event_data(value, &out); |
| if (ACPI_FAILURE(err)) |
| goto out_unlock; |
| |
| wobj = out.pointer; |
| |
| err = populate_event_from_wobj(&event, wobj); |
| if (err) { |
| dev_warn(dev, "Bad event data (ACPI type %d)\n", wobj->type); |
| goto out_free_wobj; |
| } |
| |
| event_type = classify_event(event.name, event.category); |
| switch (event_type) { |
| case HP_WMI_TYPE_AIR_FLOW: |
| fan_info = match_fan_event(state, event.description); |
| if (fan_info) |
| fan_info->alarm = true; |
| break; |
| |
| case HP_WMI_TYPE_INTRUSION: |
| state->intrusion = true; |
| break; |
| |
| case HP_WMI_TYPE_TEMPERATURE: |
| count = match_temp_events(state, event.description, temp_info); |
| while (count) |
| temp_info[--count]->alarm = true; |
| break; |
| |
| default: |
| break; |
| } |
| |
| out_free_wobj: |
| kfree(wobj); |
| |
| out_unlock: |
| mutex_unlock(&state->lock); |
| } |
| |
| static int init_platform_events(struct device *dev, |
| struct hp_wmi_platform_events **out_pevents, |
| u8 *out_pcount) |
| { |
| struct hp_wmi_platform_events *pevents_arr; |
| struct hp_wmi_platform_events *pevents; |
| union acpi_object *wobj; |
| u8 count; |
| int err; |
| u8 i; |
| |
| count = hp_wmi_wobj_instance_count(HP_WMI_PLATFORM_EVENTS_GUID); |
| if (!count) { |
| *out_pcount = 0; |
| |
| dev_dbg(dev, "No platform events\n"); |
| |
| return 0; |
| } |
| |
| pevents_arr = devm_kcalloc(dev, count, sizeof(*pevents), GFP_KERNEL); |
| if (!pevents_arr) |
| return -ENOMEM; |
| |
| for (i = 0, pevents = pevents_arr; i < count; i++, pevents++) { |
| wobj = hp_wmi_get_wobj(HP_WMI_PLATFORM_EVENTS_GUID, i); |
| if (!wobj) |
| return -EIO; |
| |
| err = populate_platform_events_from_wobj(dev, pevents, wobj); |
| |
| kfree(wobj); |
| |
| if (err) |
| return err; |
| } |
| |
| *out_pevents = pevents_arr; |
| *out_pcount = count; |
| |
| dev_dbg(dev, "Found %u platform events\n", count); |
| |
| return 0; |
| } |
| |
| static int init_numeric_sensors(struct hp_wmi_sensors *state, |
| struct hp_wmi_info *connected[], |
| struct hp_wmi_info **out_info, |
| u8 *out_icount, u8 *out_count, |
| bool *out_is_new) |
| { |
| struct hp_wmi_info ***info_map = state->info_map; |
| u8 *channel_count = state->channel_count; |
| struct device *dev = &state->wdev->dev; |
| struct hp_wmi_numeric_sensor *nsensor; |
| u8 channel_index[hwmon_max] = {}; |
| enum hwmon_sensor_types type; |
| struct hp_wmi_info *info_arr; |
| struct hp_wmi_info *info; |
| union acpi_object *wobj; |
| u8 count = 0; |
| bool is_new; |
| u8 icount; |
| int wtype; |
| int err; |
| u8 c; |
| u8 i; |
| |
| icount = hp_wmi_wobj_instance_count(HP_WMI_NUMERIC_SENSOR_GUID); |
| if (!icount) |
| return -ENODATA; |
| |
| info_arr = devm_kcalloc(dev, icount, sizeof(*info), GFP_KERNEL); |
| if (!info_arr) |
| return -ENOMEM; |
| |
| for (i = 0, info = info_arr; i < icount; i++, info++) { |
| wobj = hp_wmi_get_wobj(HP_WMI_NUMERIC_SENSOR_GUID, i); |
| if (!wobj) |
| return -EIO; |
| |
| info->instance = i; |
| info->state = state; |
| nsensor = &info->nsensor; |
| |
| err = populate_numeric_sensor_from_wobj(dev, nsensor, wobj, |
| &is_new); |
| |
| kfree(wobj); |
| |
| if (err) |
| return err; |
| |
| if (!numeric_sensor_is_connected(nsensor)) |
| continue; |
| |
| wtype = classify_numeric_sensor(nsensor); |
| if (wtype < 0) |
| continue; |
| |
| type = hp_wmi_hwmon_type_map[wtype]; |
| |
| channel_count[type]++; |
| |
| info->type = type; |
| |
| interpret_info(info); |
| |
| connected[count++] = info; |
| } |
| |
| dev_dbg(dev, "Found %u sensors (%u connected)\n", i, count); |
| |
| for (i = 0; i < count; i++) { |
| info = connected[i]; |
| type = info->type; |
| c = channel_index[type]++; |
| |
| if (!info_map[type]) { |
| info_map[type] = devm_kcalloc(dev, channel_count[type], |
| sizeof(*info_map), |
| GFP_KERNEL); |
| if (!info_map[type]) |
| return -ENOMEM; |
| } |
| |
| info_map[type][c] = info; |
| } |
| |
| *out_info = info_arr; |
| *out_icount = icount; |
| *out_count = count; |
| *out_is_new = is_new; |
| |
| return 0; |
| } |
| |
| static bool find_event_attributes(struct hp_wmi_sensors *state, |
| struct hp_wmi_platform_events *pevents, |
| u8 pevents_count) |
| { |
| /* |
| * The existence of this HPBIOS_PlatformEvents instance: |
| * |
| * { |
| * Name = "Rear Chassis Fan0 Stall"; |
| * Description = "Rear Chassis Fan0 Speed"; |
| * Category = 3; // "Sensor" |
| * PossibleSeverity = 25; // "Critical Failure" |
| * PossibleStatus = 5; // "Predictive Failure" |
| * [...] |
| * } |
| * |
| * means that this HPBIOS_BIOSEvent instance may occur: |
| * |
| * { |
| * Name = "Rear Chassis Fan0 Stall"; |
| * Description = "Rear Chassis Fan0 Speed"; |
| * Category = 3; // "Sensor" |
| * Severity = 25; // "Critical Failure" |
| * Status = 5; // "Predictive Failure" |
| * } |
| * |
| * After the event occurs (e.g. because the fan was unplugged), |
| * polling the related HPBIOS_BIOSNumericSensor instance gives: |
| * |
| * { |
| * Name = "Rear Chassis Fan0"; |
| * Description = "Reports rear chassis fan0 speed"; |
| * OperationalStatus = 5; // "Predictive Failure", was 3 ("OK") |
| * CurrentReading = 0; |
| * [...] |
| * } |
| * |
| * In this example, the hwmon fan channel for "Rear Chassis Fan0" |
| * should support the alarm flag and have it be set if the related |
| * HPBIOS_BIOSEvent instance occurs. |
| * |
| * In addition to fan events, temperature (CPU/chassis) and intrusion |
| * events are relevant to hwmon [2]. Note that much information in [2] |
| * is unreliable; it is referenced in addition to ACPI dumps [3] merely |
| * to support the conclusion that sensor and event names/descriptions |
| * are systematic enough to allow this driver to match them. |
| * |
| * Complications and limitations: |
| * |
| * - Strings are freeform and may vary, cf. sensor Name "CPU0 Fan" |
| * on a Z420 vs. "CPU Fan Speed" on an EliteOne 800 G1. |
| * - Leading/trailing whitespace is a rare but real possibility [3]. |
| * - The HPBIOS_PlatformEvents object may not exist or its instances |
| * may show that the system only has e.g. BIOS setting-related |
| * events (cf. the ProBook 4540s and ProBook 470 G0 [3]). |
| */ |
| |
| struct hp_wmi_info *temp_info[HP_WMI_MAX_INSTANCES] = {}; |
| const char *event_description; |
| struct hp_wmi_info *fan_info; |
| bool has_events = false; |
| const char *event_name; |
| u32 event_category; |
| int event_type; |
| u8 count; |
| u8 i; |
| |
| for (i = 0; i < pevents_count; i++, pevents++) { |
| event_name = pevents->name; |
| event_description = pevents->description; |
| event_category = pevents->category; |
| |
| event_type = classify_event(event_name, event_category); |
| switch (event_type) { |
| case HP_WMI_TYPE_AIR_FLOW: |
| fan_info = match_fan_event(state, event_description); |
| if (!fan_info) |
| break; |
| |
| fan_info->has_alarm = true; |
| has_events = true; |
| break; |
| |
| case HP_WMI_TYPE_INTRUSION: |
| state->has_intrusion = true; |
| has_events = true; |
| break; |
| |
| case HP_WMI_TYPE_TEMPERATURE: |
| count = match_temp_events(state, event_description, |
| temp_info); |
| if (!count) |
| break; |
| |
| while (count) |
| temp_info[--count]->has_alarm = true; |
| has_events = true; |
| break; |
| |
| default: |
| break; |
| } |
| } |
| |
| return has_events; |
| } |
| |
| static int make_chip_info(struct hp_wmi_sensors *state, bool has_events) |
| { |
| const struct hwmon_channel_info **ptr_channel_info; |
| struct hp_wmi_info ***info_map = state->info_map; |
| u8 *channel_count = state->channel_count; |
| struct hwmon_channel_info *channel_info; |
| struct device *dev = &state->wdev->dev; |
| enum hwmon_sensor_types type; |
| u8 type_count = 0; |
| u32 *config; |
| u32 attr; |
| u8 count; |
| u8 i; |
| |
| if (channel_count[hwmon_temp]) |
| channel_count[hwmon_chip] = 1; |
| |
| if (has_events && state->has_intrusion) |
| channel_count[hwmon_intrusion] = 1; |
| |
| for (type = hwmon_chip; type < hwmon_max; type++) |
| if (channel_count[type]) |
| type_count++; |
| |
| channel_info = devm_kcalloc(dev, type_count, |
| sizeof(*channel_info), GFP_KERNEL); |
| if (!channel_info) |
| return -ENOMEM; |
| |
| ptr_channel_info = devm_kcalloc(dev, type_count + 1, |
| sizeof(*ptr_channel_info), GFP_KERNEL); |
| if (!ptr_channel_info) |
| return -ENOMEM; |
| |
| hp_wmi_chip_info.info = ptr_channel_info; |
| |
| for (type = hwmon_chip; type < hwmon_max; type++) { |
| count = channel_count[type]; |
| if (!count) |
| continue; |
| |
| config = devm_kcalloc(dev, count + 1, |
| sizeof(*config), GFP_KERNEL); |
| if (!config) |
| return -ENOMEM; |
| |
| attr = hp_wmi_hwmon_attributes[type]; |
| channel_info->type = type; |
| channel_info->config = config; |
| memset32(config, attr, count); |
| |
| *ptr_channel_info++ = channel_info++; |
| |
| if (!has_events || (type != hwmon_temp && type != hwmon_fan)) |
| continue; |
| |
| attr = type == hwmon_temp ? HWMON_T_ALARM : HWMON_F_ALARM; |
| |
| for (i = 0; i < count; i++) |
| if (info_map[type][i]->has_alarm) |
| config[i] |= attr; |
| } |
| |
| return 0; |
| } |
| |
| static bool add_event_handler(struct hp_wmi_sensors *state) |
| { |
| struct device *dev = &state->wdev->dev; |
| int err; |
| |
| err = wmi_install_notify_handler(HP_WMI_EVENT_GUID, |
| hp_wmi_notify, state); |
| if (err) { |
| dev_info(dev, "Failed to subscribe to WMI event\n"); |
| return false; |
| } |
| |
| err = devm_add_action_or_reset(dev, hp_wmi_devm_notify_remove, NULL); |
| if (err) |
| return false; |
| |
| return true; |
| } |
| |
| static int hp_wmi_sensors_init(struct hp_wmi_sensors *state) |
| { |
| struct hp_wmi_info *connected[HP_WMI_MAX_INSTANCES]; |
| struct hp_wmi_platform_events *pevents = NULL; |
| struct device *dev = &state->wdev->dev; |
| struct hp_wmi_info *info; |
| struct device *hwdev; |
| bool has_events; |
| bool is_new; |
| u8 icount; |
| u8 pcount; |
| u8 count; |
| int err; |
| |
| err = init_platform_events(dev, &pevents, &pcount); |
| if (err) |
| return err; |
| |
| err = init_numeric_sensors(state, connected, &info, |
| &icount, &count, &is_new); |
| if (err) |
| return err; |
| |
| if (IS_ENABLED(CONFIG_DEBUG_FS)) |
| hp_wmi_debugfs_init(dev, info, pevents, icount, pcount, is_new); |
| |
| if (!count) |
| return 0; /* No connected sensors; debugfs only. */ |
| |
| has_events = find_event_attributes(state, pevents, pcount); |
| |
| /* Survive failure to install WMI event handler. */ |
| if (has_events && !add_event_handler(state)) |
| has_events = false; |
| |
| err = make_chip_info(state, has_events); |
| if (err) |
| return err; |
| |
| hwdev = devm_hwmon_device_register_with_info(dev, "hp_wmi_sensors", |
| state, &hp_wmi_chip_info, |
| NULL); |
| return PTR_ERR_OR_ZERO(hwdev); |
| } |
| |
| static int hp_wmi_sensors_probe(struct wmi_device *wdev, const void *context) |
| { |
| struct device *dev = &wdev->dev; |
| struct hp_wmi_sensors *state; |
| |
| state = devm_kzalloc(dev, sizeof(*state), GFP_KERNEL); |
| if (!state) |
| return -ENOMEM; |
| |
| state->wdev = wdev; |
| |
| mutex_init(&state->lock); |
| |
| dev_set_drvdata(dev, state); |
| |
| return hp_wmi_sensors_init(state); |
| } |
| |
| static const struct wmi_device_id hp_wmi_sensors_id_table[] = { |
| { HP_WMI_NUMERIC_SENSOR_GUID, NULL }, |
| {}, |
| }; |
| |
| static struct wmi_driver hp_wmi_sensors_driver = { |
| .driver = { .name = "hp-wmi-sensors" }, |
| .id_table = hp_wmi_sensors_id_table, |
| .probe = hp_wmi_sensors_probe, |
| }; |
| module_wmi_driver(hp_wmi_sensors_driver); |
| |
| MODULE_AUTHOR("James Seo <james@equiv.tech>"); |
| MODULE_DESCRIPTION("HP WMI Sensors driver"); |
| MODULE_LICENSE("GPL"); |