#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/acpi.h>
#include <linux/bug.h>
#include <linux/cleanup.h>
#include <linux/component.h>
#include <linux/container_of.h>
#include <linux/debugfs.h>
#include <linux/device.h>
#include <linux/dev_printk.h>
#include <linux/err.h>
#include <linux/export.h>
#include <linux/gfp_types.h>
#include <linux/limits.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/mutex_types.h>
#include <linux/notifier.h>
#include <linux/overflow.h>
#include <linux/seq_file.h>
#include <linux/stddef.h>
#include <linux/types.h>
#include <linux/wmi.h>
#include "wmi-capdata.h"
#include "wmi-helpers.h"
#define LENOVO_CAPABILITY_DATA_00_GUID "362A3AFE-3D96-4665-8530-96DAD5BB300E"
#define LENOVO_CAPABILITY_DATA_01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154"
#define LENOVO_FAN_TEST_DATA_GUID "B642801B-3D21-45DE-90AE-6E86F164FB21"
#define ACPI_AC_NOTIFY_STATUS 0x80
#define LWMI_FEATURE_ID_FAN_TEST 0x05
#define LWMI_ATTR_ID_FAN_TEST \
lwmi_attr_id(LWMI_DEVICE_ID_FAN, LWMI_FEATURE_ID_FAN_TEST, \
LWMI_GZ_THERMAL_MODE_NONE, LWMI_TYPE_ID_NONE)
enum lwmi_cd_type {
LENOVO_CAPABILITY_DATA_00,
LENOVO_CAPABILITY_DATA_01,
LENOVO_FAN_TEST_DATA,
CD_TYPE_NONE = -1,
};
#define LWMI_CD_TABLE_ITEM(_type) \
[_type] = { \
.name = #_type, \
.type = _type, \
}
static const struct lwmi_cd_info {
const char *name;
enum lwmi_cd_type type;
} lwmi_cd_table[] = {
LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_00),
LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01),
LWMI_CD_TABLE_ITEM(LENOVO_FAN_TEST_DATA),
};
struct lwmi_cd_priv {
struct notifier_block acpi_nb;
struct wmi_device *wdev;
struct cd_list *list;
struct dentry *debugfs_dir;
struct lwmi_cd_sub_master_priv {
struct device *master_dev;
cd_list_cb_t master_cb;
struct cd_list *sub_component_list;
bool registered;
} *sub_master;
};
struct cd_list {
struct mutex list_mutex;
enum lwmi_cd_type type;
u8 count;
union {
DECLARE_FLEX_ARRAY(struct capdata00, cd00);
DECLARE_FLEX_ARRAY(struct capdata01, cd01);
DECLARE_FLEX_ARRAY(struct capdata_fan, cd_fan);
};
};
static struct wmi_driver lwmi_cd_driver;
static int lwmi_cd_match(struct device *dev, void *type)
{
struct lwmi_cd_priv *priv;
if (dev->driver != &lwmi_cd_driver.driver)
return false;
priv = dev_get_drvdata(dev);
return priv->list->type == *(enum lwmi_cd_type *)type;
}
void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr)
{
int i;
if (WARN_ON(*matchptr))
return;
for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
continue;
component_match_add(master, matchptr, lwmi_cd_match,
(void *)&lwmi_cd_table[i].type);
if (IS_ERR(*matchptr))
return;
}
}
EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CAPDATA");
static void lwmi_cd_call_master_cb(struct lwmi_cd_priv *priv)
{
struct cd_list *sub_component_list = priv->sub_master->sub_component_list;
if (!sub_component_list ||
!priv->sub_master->master_dev ||
!priv->sub_master->master_cb)
return;
if (PTR_ERR(sub_component_list) == -ENODEV)
sub_component_list = NULL;
else if (WARN_ON(IS_ERR(sub_component_list)))
return;
priv->sub_master->master_cb(priv->sub_master->master_dev,
sub_component_list);
priv->sub_master->master_cb = NULL;
priv->sub_master->master_dev = NULL;
}
static int lwmi_cd_component_bind(struct device *cd_dev,
struct device *om_dev, void *data)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
struct lwmi_cd_binder *binder = data;
switch (priv->list->type) {
case LENOVO_CAPABILITY_DATA_00:
binder->cd00_list = priv->list;
priv->sub_master->master_dev = om_dev;
priv->sub_master->master_cb = binder->cd_fan_list_cb;
lwmi_cd_call_master_cb(priv);
break;
case LENOVO_CAPABILITY_DATA_01:
binder->cd01_list = priv->list;
break;
default:
return -EINVAL;
}
return 0;
}
static void lwmi_cd_component_unbind(struct device *cd_dev,
struct device *om_dev, void *data)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
switch (priv->list->type) {
case LENOVO_CAPABILITY_DATA_00:
priv->sub_master->master_dev = NULL;
priv->sub_master->master_cb = NULL;
return;
default:
return;
}
}
static const struct component_ops lwmi_cd_component_ops = {
.bind = lwmi_cd_component_bind,
.unbind = lwmi_cd_component_unbind,
};
static int lwmi_cd_sub_master_bind(struct device *dev)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
struct cd_list *sub_component_list;
int ret;
ret = component_bind_all(dev, &sub_component_list);
if (ret)
return ret;
priv->sub_master->sub_component_list = sub_component_list;
lwmi_cd_call_master_cb(priv);
return 0;
}
static void lwmi_cd_sub_master_unbind(struct device *dev)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
priv->sub_master->sub_component_list = NULL;
component_unbind_all(dev, NULL);
}
static const struct component_master_ops lwmi_cd_sub_master_ops = {
.bind = lwmi_cd_sub_master_bind,
.unbind = lwmi_cd_sub_master_unbind,
};
static int lwmi_cd_sub_master_add(struct lwmi_cd_priv *priv,
enum lwmi_cd_type sub_component_type)
{
struct component_match *master_match = NULL;
int ret;
priv->sub_master = devm_kzalloc(&priv->wdev->dev, sizeof(*priv->sub_master), GFP_KERNEL);
if (!priv->sub_master)
return -ENOMEM;
if (sub_component_type == CD_TYPE_NONE) {
priv->sub_master->sub_component_list = ERR_PTR(-ENODEV);
priv->sub_master->registered = false;
return 0;
}
component_match_add(&priv->wdev->dev, &master_match, lwmi_cd_match,
(void *)&lwmi_cd_table[sub_component_type].type);
if (IS_ERR(master_match))
return PTR_ERR(master_match);
ret = component_master_add_with_match(&priv->wdev->dev, &lwmi_cd_sub_master_ops,
master_match);
if (ret)
return ret;
priv->sub_master->registered = true;
return 0;
}
static void lwmi_cd_sub_master_del(struct lwmi_cd_priv *priv)
{
if (!priv->sub_master->registered)
return;
component_master_del(&priv->wdev->dev, &lwmi_cd_sub_master_ops);
priv->sub_master->registered = false;
}
static int lwmi_cd_sub_component_bind(struct device *sc_dev,
struct device *sm_dev, void *data)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(sc_dev);
struct cd_list **listp = data;
*listp = priv->list;
return 0;
}
static const struct component_ops lwmi_cd_sub_component_ops = {
.bind = lwmi_cd_sub_component_bind,
};
#define DEF_LWMI_CDXX_GET_DATA(_cdxx, _cd_type, _output_t) \
int lwmi_##_cdxx##_get_data(struct cd_list *list, u32 attribute_id, _output_t *output) \
{ \
u8 idx; \
\
if (WARN_ON(list->type != _cd_type)) \
return -EINVAL; \
\
guard(mutex)(&list->list_mutex); \
for (idx = 0; idx < list->count; idx++) { \
if (list->_cdxx[idx].id != attribute_id) \
continue; \
memcpy(output, &list->_cdxx[idx], sizeof(list->_cdxx[idx])); \
return 0; \
} \
return -EINVAL; \
}
DEF_LWMI_CDXX_GET_DATA(cd00, LENOVO_CAPABILITY_DATA_00, struct capdata00);
EXPORT_SYMBOL_NS_GPL(lwmi_cd00_get_data, "LENOVO_WMI_CAPDATA");
DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01);
EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CAPDATA");
DEF_LWMI_CDXX_GET_DATA(cd_fan, LENOVO_FAN_TEST_DATA, struct capdata_fan);
EXPORT_SYMBOL_NS_GPL(lwmi_cd_fan_get_data, "LENOVO_WMI_CAPDATA");
static void lwmi_cd00_show(struct seq_file *s, struct capdata00 *cd00)
{
u8 dev = FIELD_GET(LWMI_ATTR_DEV_ID_MASK, cd00->id);
u8 feat = FIELD_GET(LWMI_ATTR_FEAT_ID_MASK, cd00->id);
u8 mode = FIELD_GET(LWMI_ATTR_MODE_ID_MASK, cd00->id);
u8 type = FIELD_GET(LWMI_ATTR_TYPE_ID_MASK, cd00->id);
bool extra = cd00->supported & ~(LWMI_SUPP_GET | LWMI_SUPP_SET | LWMI_SUPP_VALID);
bool get = cd00->supported & LWMI_SUPP_GET;
bool set = cd00->supported & LWMI_SUPP_SET;
bool valid = cd00->supported & LWMI_SUPP_VALID;
seq_printf(s, " id: 0x%08x [dev: %2u, feat: %2u, mode: %2u, type: %2u]\n",
cd00->id, dev, feat, mode, type);
seq_printf(s, " supported: 0x%08x [%c%c%c%c]\n", cd00->supported,
extra ? '+' : ' ',
get ? 'R' : ' ',
set ? 'W' : ' ',
valid ? 'V' : ' ');
seq_printf(s, " default_value: %u\n", cd00->default_value);
}
static void lwmi_cd01_show(struct seq_file *s, struct capdata01 *cd01)
{
lwmi_cd00_show(s, &cd01->cd00);
seq_printf(s, " step: %u\n", cd01->step);
seq_printf(s, " min_value: %u\n", cd01->min_value);
seq_printf(s, " max_value: %u\n", cd01->max_value);
}
static void lwmi_cd_fan_show(struct seq_file *s, struct capdata_fan *cd_fan)
{
seq_printf(s, " id: %u\n", cd_fan->id);
seq_printf(s, " min_rpm: %u\n", cd_fan->min_rpm);
seq_printf(s, " max_rpm: %u\n", cd_fan->max_rpm);
}
static int lwmi_cd_debugfs_show(struct seq_file *s, void *data)
{
struct lwmi_cd_priv *priv = s->private;
u8 idx;
guard(mutex)(&priv->list->list_mutex);
for (idx = 0; idx < priv->list->count; idx++) {
seq_printf(s, "%s[%u]:\n", lwmi_cd_table[priv->list->type].name, idx);
if (priv->list->type == LENOVO_CAPABILITY_DATA_00)
lwmi_cd00_show(s, &priv->list->cd00[idx]);
else if (priv->list->type == LENOVO_CAPABILITY_DATA_01)
lwmi_cd01_show(s, &priv->list->cd01[idx]);
else if (priv->list->type == LENOVO_FAN_TEST_DATA)
lwmi_cd_fan_show(s, &priv->list->cd_fan[idx]);
}
return 0;
}
DEFINE_SHOW_ATTRIBUTE(lwmi_cd_debugfs);
static void lwmi_cd_debugfs_add(struct lwmi_cd_priv *priv)
{
priv->debugfs_dir = lwmi_debugfs_create_dir(priv->wdev);
debugfs_create_file("capdata", 0444, priv->debugfs_dir, priv, &lwmi_cd_debugfs_fops);
}
static void lwmi_cd_debugfs_remove(struct lwmi_cd_priv *priv)
{
debugfs_remove_recursive(priv->debugfs_dir);
priv->debugfs_dir = NULL;
}
static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
{
size_t size;
int idx;
void *p;
switch (priv->list->type) {
case LENOVO_CAPABILITY_DATA_00:
p = &priv->list->cd00[0];
size = sizeof(priv->list->cd00[0]);
break;
case LENOVO_CAPABILITY_DATA_01:
p = &priv->list->cd01[0];
size = sizeof(priv->list->cd01[0]);
break;
case LENOVO_FAN_TEST_DATA:
return 0;
default:
return -EINVAL;
}
guard(mutex)(&priv->list->list_mutex);
for (idx = 0; idx < priv->list->count; idx++, p += size) {
union acpi_object *ret_obj __free(kfree) = NULL;
ret_obj = wmidev_block_query(priv->wdev, idx);
if (!ret_obj)
return -ENODEV;
if (ret_obj->type != ACPI_TYPE_BUFFER ||
ret_obj->buffer.length < size)
continue;
memcpy(p, ret_obj->buffer.pointer, size);
}
return 0;
}
static int lwmi_cd_fan_list_alloc_cache(struct lwmi_cd_priv *priv, struct cd_list **listptr)
{
struct cd_list *list;
size_t size;
u32 count;
int idx;
struct cd_fan_block {
u32 nr;
u32 data[];
} __packed * block;
union acpi_object *ret_obj __free(kfree) = wmidev_block_query(priv->wdev, 0);
if (!ret_obj)
return -ENODEV;
if (ret_obj->type == ACPI_TYPE_BUFFER) {
block = (struct cd_fan_block *)ret_obj->buffer.pointer;
size = ret_obj->buffer.length;
count = size >= sizeof(*block) ? block->nr : 0;
if (size < struct_size(block, data, count * 3)) {
dev_warn(&priv->wdev->dev,
"incomplete fan test data block: %zu < %zu, ignoring\n",
size, struct_size(block, data, count * 3));
count = 0;
} else if (count > U8_MAX) {
dev_warn(&priv->wdev->dev,
"too many fans reported: %u > %u, truncating\n",
count, U8_MAX);
count = U8_MAX;
}
} else {
count = 0;
}
list = devm_kzalloc(&priv->wdev->dev, struct_size(list, cd_fan, count), GFP_KERNEL);
if (!list)
return -ENOMEM;
for (idx = 0; idx < count; idx++) {
list->cd_fan[idx] = (struct capdata_fan) {
.id = block->data[idx],
.max_rpm = block->data[idx + block->nr],
.min_rpm = block->data[idx + (2 * block->nr)],
};
}
*listptr = list;
return count;
}
static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
{
struct cd_list *list;
size_t list_size;
int count, ret;
count = wmidev_instance_count(priv->wdev);
switch (type) {
case LENOVO_CAPABILITY_DATA_00:
list_size = struct_size(list, cd00, count);
break;
case LENOVO_CAPABILITY_DATA_01:
list_size = struct_size(list, cd01, count);
break;
case LENOVO_FAN_TEST_DATA:
count = lwmi_cd_fan_list_alloc_cache(priv, &list);
if (count < 0)
return count;
goto got_list;
default:
return -EINVAL;
}
list = devm_kzalloc(&priv->wdev->dev, list_size, GFP_KERNEL);
if (!list)
return -ENOMEM;
got_list:
ret = devm_mutex_init(&priv->wdev->dev, &list->list_mutex);
if (ret)
return ret;
list->type = type;
list->count = count;
priv->list = list;
return 0;
}
static int lwmi_cd_setup(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
{
int ret;
ret = lwmi_cd_alloc(priv, type);
if (ret)
return ret;
return lwmi_cd_cache(priv);
}
static int lwmi_cd01_notifier_call(struct notifier_block *nb, unsigned long action,
void *data)
{
struct acpi_bus_event *event = data;
struct lwmi_cd_priv *priv;
int ret;
if (strcmp(event->device_class, ACPI_AC_CLASS) != 0)
return NOTIFY_DONE;
priv = container_of(nb, struct lwmi_cd_priv, acpi_nb);
switch (event->type) {
case ACPI_AC_NOTIFY_STATUS:
ret = lwmi_cd_cache(priv);
if (ret)
return NOTIFY_BAD;
return NOTIFY_OK;
default:
return NOTIFY_DONE;
}
}
static void lwmi_cd01_unregister(void *data)
{
struct notifier_block *acpi_nb = data;
unregister_acpi_notifier(acpi_nb);
}
static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
{
const struct lwmi_cd_info *info = context;
struct lwmi_cd_priv *priv;
int ret;
if (!info)
return -EINVAL;
priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->wdev = wdev;
dev_set_drvdata(&wdev->dev, priv);
ret = lwmi_cd_setup(priv, info->type);
if (ret)
goto out;
switch (info->type) {
case LENOVO_CAPABILITY_DATA_00: {
enum lwmi_cd_type sub_component_type = LENOVO_FAN_TEST_DATA;
struct capdata00 capdata00;
ret = lwmi_cd00_get_data(priv->list, LWMI_ATTR_ID_FAN_TEST, &capdata00);
if (ret || !(capdata00.supported & LWMI_SUPP_VALID)) {
dev_dbg(&wdev->dev, "capdata00 declares no fan test support\n");
sub_component_type = CD_TYPE_NONE;
}
ret = lwmi_cd_sub_master_add(priv, sub_component_type);
if (ret)
goto out;
ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
if (ret)
lwmi_cd_sub_master_del(priv);
goto out;
}
case LENOVO_CAPABILITY_DATA_01:
priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
ret = register_acpi_notifier(&priv->acpi_nb);
if (ret)
goto out;
ret = devm_add_action_or_reset(&wdev->dev, lwmi_cd01_unregister,
&priv->acpi_nb);
if (ret)
goto out;
ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
goto out;
case LENOVO_FAN_TEST_DATA:
ret = component_add(&wdev->dev, &lwmi_cd_sub_component_ops);
goto out;
default:
return -EINVAL;
}
out:
if (ret) {
dev_err(&wdev->dev, "failed to register %s: %d\n",
info->name, ret);
} else {
lwmi_cd_debugfs_add(priv);
dev_dbg(&wdev->dev, "registered %s with %u items\n",
info->name, priv->list->count);
}
return ret;
}
static void lwmi_cd_remove(struct wmi_device *wdev)
{
struct lwmi_cd_priv *priv = dev_get_drvdata(&wdev->dev);
lwmi_cd_debugfs_remove(priv);
switch (priv->list->type) {
case LENOVO_CAPABILITY_DATA_00:
lwmi_cd_sub_master_del(priv);
fallthrough;
case LENOVO_CAPABILITY_DATA_01:
component_del(&wdev->dev, &lwmi_cd_component_ops);
break;
case LENOVO_FAN_TEST_DATA:
component_del(&wdev->dev, &lwmi_cd_sub_component_ops);
break;
default:
WARN_ON(1);
}
}
#define LWMI_CD_WDEV_ID(_type) \
.guid_string = _type##_GUID, \
.context = &lwmi_cd_table[_type],
static const struct wmi_device_id lwmi_cd_id_table[] = {
{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_00) },
{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_01) },
{ LWMI_CD_WDEV_ID(LENOVO_FAN_TEST_DATA) },
{}
};
static struct wmi_driver lwmi_cd_driver = {
.driver = {
.name = "lenovo_wmi_capdata",
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
.id_table = lwmi_cd_id_table,
.probe = lwmi_cd_probe,
.remove = lwmi_cd_remove,
.no_singleton = true,
};
module_wmi_driver(lwmi_cd_driver);
MODULE_IMPORT_NS("LENOVO_WMI_HELPERS");
MODULE_DEVICE_TABLE(wmi, lwmi_cd_id_table);
MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
MODULE_AUTHOR("Rong Zhang <i@rong.moe>");
MODULE_DESCRIPTION("Lenovo Capability Data WMI Driver");
MODULE_LICENSE("GPL");