Message ID | 20190523093104.21210-1-ayman.bagabas@gmail.com (mailing list archive) |
---|---|
State | Not Applicable, archived |
Headers | show |
Series | [v1] platform/x86: Huawei laptop extras driver | expand |
I'm really sorry, I just realized the changes are a lot and unreadable. Changes should be splitted into a series and resubmitted. On Thu, 2019-05-23 at 05:31 -0400, Ayman Bagabas wrote: > This patch introduces new features to the driver and also moves the > driver from wmi_driver to platform_driver. > > This move is necessary because the driver is no longer only a hotkeys > driver and platform_driver offers easier future extensibility. The > patch > introduces a WMI BIOS interface that brings on new features and > enables > controlling micmute LED through this interface on supported models. > New > features are controlling battery charging thresholds and fn-lock > state > among with module parameters and quirks check. > > Currently, micmute LED is controlled through an ACPI method under EC. > This method ("SPIN", "WPIN") is specific to some models and wouldn't > work on all Huawei laptops. > > Using this interface, controlling this LED should work with any > model. > Except `MateBook X` from 2017, this one doesn't provide controlling > the > LED through this interface instead it uses another "legacy" interface > that is not "fully" implemented yet due to lack of hardware. > Currently, > this "legacy" interface is used for hotkeys on this specific model. A > quirk is set to use ACPI method to control micmute LED on this model. > > Some models that implement the new WMI BIOS interface can control > battery charging thresholds where it limits charging the battery once > it > reaches certain thresholds. > > The behavior of hotkeys is not the same among all models. Some models > require fn-lock to do things like `Ctrl-Ins` or `Alt-PrtSc`. By > default, > hotkeys behave as special keys (media keys, Ins, etc), but if a > modifier > is used (ctrl, alt, shift) these keys behave as F1-F12 keys. If the > Fn > key is toggled on, the hotkeys with or without a modifier, behave as > F1-F12 keys. This makes it impossible to use a modifier and `PrtSc` > or > `Ins`. > > Now, some models fix this by excluding `PrtSc` and `Ins` keys from > being treated as F11 and F12 keys with the use of a modifier. > However, > some models do not, and fixes this by the so called fn-lock. > > Fn-lock inverts the behavior of the top row from special keys to F1- > F12 > keys. So a modifier and a special key would be possible which make > things like `Alt-Ins` possible. Now, with fn-lock we would have 4 > modes: > * Fn-key off & fn-lock off - hotkeys treated as special keys using a > modifier gives F1-F12 keys. > * Fn-key on & fn-lock off - hotkeys treated as F1-F12 keys and using > a > modifier gives F1-F12. > * Fn-key off & fn-lock on - hotkeys are treated as F1-F12 keys and > using > a modifier gives special keys. > * Fn-key on & fn-lock on - hotkeys are treated as special keys and > using > a modifier gives special keys. > > The driver introduces two parameters to force reporting brightness > keys > and sleeping after setting a threshold value. > > All newer models that "fully" implement the new interface report > brightness key events twice, once through WMI and once through > acpi-video. Older models, such as `MateBook X`, don't report > brightness > events using WMI. This is implemented as a quirk and can be forced > using > module parameters. > > Some models don't allow setting thresholds to (0, 100), due to bad > ASL > code, which indicates reset values, instead, it only turns off > battery > charging protection. This would return the previously set values even > though battery protection is off which doesn't make sense. A sane > value > like (0, 100) indicates no charging protection, but since it's not > possible to set such values, (0, 0) is set before turning protection > off with (0, 100). This requires a delay after setting (0, 0) and > after > (0, 100) so that these values make their way to EC memory. > > These parameters are implemented as quirks along with `ec_micmute` > quirk > which controls the micmute LED through ACPI EC interface. > > Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com> > --- > drivers/platform/x86/huawei-wmi.c | 630 +++++++++++++++++++++++++--- > -- > 1 file changed, 541 insertions(+), 89 deletions(-) > > diff --git a/drivers/platform/x86/huawei-wmi.c > b/drivers/platform/x86/huawei-wmi.c > index 52fcac5b393a..3f945b4cf115 100644 > --- a/drivers/platform/x86/huawei-wmi.c > +++ b/drivers/platform/x86/huawei-wmi.c > @@ -1,32 +1,65 @@ > // SPDX-License-Identifier: GPL-2.0 > /* > - * Huawei WMI hotkeys > + * Huawei WMI laptop extras driver > * > * Copyright (C) 2018 Ayman Bagabas < > ayman.bagabas@gmail.com> > */ > > #include <linux/acpi.h> > +#include <linux/delay.h> > +#include <linux/dmi.h> > #include <linux/input.h> > #include <linux/input/sparse-keymap.h> > #include <linux/leds.h> > #include <linux/module.h> > +#include <linux/mutex.h> > +#include <linux/platform_device.h> > +#include <linux/sysfs.h> > #include <linux/wmi.h> > > /* > * Huawei WMI GUIDs > */ > -#define WMI0_EVENT_GUID "59142400-C6A3-40fa-BADB-8A2652834100" > +#define AMW0_METHOD_GUID "ABBC0F5B-8EA1-11D1-A000-C90629100000" > #define AMW0_EVENT_GUID "ABBC0F5C-8EA1-11D1-A000-C90629100000" > > +/* Legacy GUIDs */ > #define WMI0_EXPENSIVE_GUID "39142400-C6A3-40fa-BADB-8A2652834100" > +#define WMI0_EVENT_GUID "59142400-C6A3-40fa-BADB-8A2652834100" > + > +/* AMW0_commands */ > + > +enum wmaa_cmd { > + BATTERY_GET, /* \GBTT 0x00001103 */ > + BATTERY_SET, /* \SBTT 0xXXYY1003 */ > + FN_LOCK_GET, /* \GFRS 0x00000604 */ > + FN_LOCK_SET, /* \SFRS 0x000X0704 */ > + MICMUTE_LED, /* \SMLS 0x000X0b04 */ > +}; > + > +enum fn_state { > + FN_LOCK_OFF = 0x01, > + FN_LOCK_ON = 0x02, > +}; > + > +struct quirk_entry { > + bool battery_sleep; > + bool ec_micmute; > + bool report_brightness; > +}; > > -struct huawei_wmi_priv { > - struct input_dev *idev; > +static struct quirk_entry *quirks; > + > +struct huawei_wmi { > struct led_classdev cdev; > - acpi_handle handle; > - char *acpi_method; > + struct mutex wmi_lock; > + struct mutex battery_lock; > + struct input_dev *idev[2]; > + struct platform_device *pdev; > }; > > +struct platform_device *huawei_wmi_pdev; > + > static const struct key_entry huawei_wmi_keymap[] = { > { KE_KEY, 0x281, { KEY_BRIGHTNESSDOWN } }, > { KE_KEY, 0x282, { KEY_BRIGHTNESSUP } }, > @@ -37,73 +70,183 @@ static const struct key_entry > huawei_wmi_keymap[] = { > { KE_KEY, 0x289, { KEY_WLAN } }, > // Huawei |M| key > { KE_KEY, 0x28a, { KEY_CONFIG } }, > - // Keyboard backlight > + // Keyboard backlit > { KE_IGNORE, 0x293, { KEY_KBDILLUMTOGGLE } }, > { KE_IGNORE, 0x294, { KEY_KBDILLUMUP } }, > { KE_IGNORE, 0x295, { KEY_KBDILLUMUP } }, > { KE_END, 0 } > }; > > -static int huawei_wmi_micmute_led_set(struct led_classdev *led_cdev, > - enum led_brightness brightness) > +static bool battery_sleep; > +static bool report_brightness; > + > +module_param(battery_sleep, bool, 0444); > +MODULE_PARM_DESC(battery_sleep, > + "Delay after setting battery charging thresholds."); > +module_param(report_brightness, bool, 0444); > +MODULE_PARM_DESC(report_brightness, > + "Report brightness key events."); > + > +/* Quirks */ > + > +static int __init dmi_matched(const struct dmi_system_id *dmi) > { > - struct huawei_wmi_priv *priv = dev_get_drvdata(led_cdev->dev- > >parent); > + quirks = dmi->driver_data; > + return 1; > +} > + > +static struct quirk_entry quirk_unknown = { > +}; > + > +static struct quirk_entry quirk_battery_sleep = { > + .battery_sleep = true, > +}; > + > +static struct quirk_entry quirk_matebook_x = { > + .ec_micmute = true, > + .report_brightness = true, > +}; > + > +static const struct dmi_system_id huawei_quirks[] = { > + { > + .callback = dmi_matched, > + .ident = "Huawei MACH-WX9", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "HUAWEI"), > + DMI_MATCH(DMI_PRODUCT_NAME, "MACH-WX9"), > + }, > + .driver_data = &quirk_battery_sleep > + }, > + { > + .callback = dmi_matched, > + .ident = "Huawei MateBook X", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "HUAWEI"), > + DMI_MATCH(DMI_PRODUCT_NAME, "HUAWEI MateBook > X") > + }, > + .driver_data = &quirk_matebook_x > + }, > + { } > +}; > + > +/* Utils */ > + > +static int huawei_wmi_eval(struct device *dev, u8 *arg, > + u8 *buf, size_t buflen) > +{ > + struct huawei_wmi *huawei = dev_get_drvdata(dev); > + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; > + struct acpi_buffer in; > + union acpi_object *obj; > acpi_status status; > - union acpi_object args[3]; > - struct acpi_object_list arg_list = { > - .pointer = args, > - .count = ARRAY_SIZE(args), > - }; > - > - args[0].type = args[1].type = args[2].type = ACPI_TYPE_INTEGER; > - args[1].integer.value = 0x04; > - > - if (strcmp(priv->acpi_method, "SPIN") == 0) { > - args[0].integer.value = 0; > - args[2].integer.value = brightness ? 1 : 0; > - } else if (strcmp(priv->acpi_method, "WPIN") == 0) { > - args[0].integer.value = 1; > - args[2].integer.value = brightness ? 0 : 1; > - } else { > - return -EINVAL; > + size_t len; > + int err = -EIO; > + > + in.length = sizeof(u8) * 4; > + in.pointer = (u32 *)arg; > + mutex_lock(&huawei->wmi_lock); > + status = wmi_evaluate_method(AMW0_METHOD_GUID, 0, 1, &in, > &out); > + if (ACPI_FAILURE(status)) { > + dev_err(dev, "Failed to evaluate wmi method\n"); > + err = -ENODEV; > + goto wmi_eval_fail; > + } > + > + /* WMAA takes a 4 bytes buffer as an input. It returns a > package > + * with two buffer elements. The first buffer is 4 bytes long > and > + * the second is 0x100 (256) bytes long. The first buffer is > always > + * zeros. The second stores the output from every call. The > first > + * byte of the second buffer always have the return status of > the > + * called command. > + */ > + obj = out.pointer; > + if (!obj) > + goto wmi_eval_fail; > + if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count != 2) > { > + dev_err(dev, "Unknown response type %d\n", obj->type); > + goto wmi_eval_fail; > } > > - status = acpi_evaluate_object(priv->handle, priv->acpi_method, > &arg_list, NULL); > - if (ACPI_FAILURE(status)) > - return -ENXIO; > + obj = &(obj->package.elements[1]); > + if (!obj || obj->type != ACPI_TYPE_BUFFER) > + goto wmi_eval_fail; > > - return 0; > + if (buf) { > + len = min(buflen, obj->buffer.length); > + memcpy(buf, obj->buffer.pointer, len); > + } > + err = 0; > + > +wmi_eval_fail: > + mutex_unlock(&huawei->wmi_lock); > + kfree(out.pointer); > + return err; > } > > -static int huawei_wmi_leds_setup(struct wmi_device *wdev) > +static int huawei_wmi_cmd(struct device *dev, enum wmaa_cmd cmd, u8 > *arg, > + u8 *out, size_t outlen) > { > - struct huawei_wmi_priv *priv = dev_get_drvdata(&wdev->dev); > - > - priv->handle = ec_get_handle(); > - if (!priv->handle) > - return 0; > + u8 parm[4] = { 0 }; > + u8 buf[0x100] = { 0xff }; > + int err; > > - if (acpi_has_method(priv->handle, "SPIN")) > - priv->acpi_method = "SPIN"; > - else if (acpi_has_method(priv->handle, "WPIN")) > - priv->acpi_method = "WPIN"; > - else > - return 0; > + if (!arg) > + arg = parm; > + > + switch (cmd) { > + case BATTERY_SET: > + arg[0] = 0x03; > + arg[1] = 0x10; > + break; > + case BATTERY_GET: > + arg[0] = 0x03; > + arg[1] = 0x11; > + break; > + case FN_LOCK_GET: > + arg[0] = 0x04; > + arg[1] = 0x06; > + break; > + case FN_LOCK_SET: > + arg[0] = 0x04; > + arg[1] = 0x07; > + break; > + case MICMUTE_LED: > + arg[0] = 0x04; > + arg[1] = 0x0b; > + break; > + default: > + dev_err(dev, "Unsupported command, got: 0x%08x\n", > *(u32 *)arg); > + return -EINVAL; > + } > > - priv->cdev.name = "platform::micmute"; > - priv->cdev.max_brightness = 1; > - priv->cdev.brightness_set_blocking = > huawei_wmi_micmute_led_set; > - priv->cdev.default_trigger = "audio-micmute"; > - priv->cdev.brightness = ledtrig_audio_get(LED_AUDIO_MICMUTE); > - priv->cdev.dev = &wdev->dev; > - priv->cdev.flags = LED_CORE_SUSPENDRESUME; > + /* Some models require calling WMAA twice to execute > + * a command. We call WMAA and if we get a non-zero return > + * status we evaluate WMAA again. If we get another non-zero > + * return, we return -EIO. This way we don't need to > + * check for return status anywhere we call huawei_wmi_cmd. > + */ > + err = huawei_wmi_eval(dev, arg, buf, 0x100); > + if (err) > + return err; > + if (buf[0]) { > + err = huawei_wmi_eval(dev, arg, buf, 0x100); > + if (err) > + return err; > + if (buf[0]) { > + dev_err(dev, "Invalid response, got: %d\n", > buf[0]); > + return -EIO; > + } > + } > + if (out) > + memcpy(out, buf, outlen); > > - return devm_led_classdev_register(&wdev->dev, &priv->cdev); > + return 0; > } > > -static void huawei_wmi_process_key(struct wmi_device *wdev, int > code) > +/* Input */ > + > +static void huawei_wmi_process_key(struct input_dev *idev, int code) > { > - struct huawei_wmi_priv *priv = dev_get_drvdata(&wdev->dev); > const struct key_entry *key; > > /* > @@ -127,81 +270,390 @@ static void huawei_wmi_process_key(struct > wmi_device *wdev, int code) > kfree(response.pointer); > } > > - key = sparse_keymap_entry_from_scancode(priv->idev, code); > + key = sparse_keymap_entry_from_scancode(idev, code); > if (!key) { > - dev_info(&wdev->dev, "Unknown key pressed, code: > 0x%04x\n", code); > + dev_info(&idev->dev, "Unknown key pressed, code: > 0x%04x\n", code); > return; > } > > - sparse_keymap_report_entry(priv->idev, key, 1, true); > + if (quirks && !quirks->report_brightness && > + (key->sw.code == KEY_BRIGHTNESSDOWN || > + key->sw.code == KEY_BRIGHTNESSUP)) > + return; > + > + sparse_keymap_report_entry(idev, key, 1, true); > } > > -static void huawei_wmi_notify(struct wmi_device *wdev, > - union acpi_object *obj) > +static void huawei_wmi_input_notify(u32 value, void *context) > { > - if (obj->type == ACPI_TYPE_INTEGER) > - huawei_wmi_process_key(wdev, obj->integer.value); > + struct input_dev *idev = (struct input_dev *)context; > + struct acpi_buffer response = { ACPI_ALLOCATE_BUFFER, NULL }; > + union acpi_object *obj; > + acpi_status status; > + > + if (!idev) { > + dev_err(&huawei_wmi_pdev->dev, "No input_dev\n"); > + return; > + } > + > + status = wmi_get_event_data(value, &response); > + if (ACPI_FAILURE(status)) { > + dev_err(&idev->dev, "Unable to get event data\n"); > + return; > + } > + > + obj = (union acpi_object *)response.pointer; > + if (obj && obj->type == ACPI_TYPE_INTEGER) > + huawei_wmi_process_key(idev, obj->integer.value); > else > - dev_info(&wdev->dev, "Bad response type %d\n", obj- > >type); > + dev_err(&idev->dev, "Bad response type\n"); > + > + kfree(response.pointer); > } > > -static int huawei_wmi_input_setup(struct wmi_device *wdev) > +static int huawei_wmi_input_setup(struct platform_device *pdev, > + struct input_dev **idev) > { > - struct huawei_wmi_priv *priv = dev_get_drvdata(&wdev->dev); > int err; > > - priv->idev = devm_input_allocate_device(&wdev->dev); > - if (!priv->idev) > + *idev = devm_input_allocate_device(&pdev->dev); > + if (!*idev) > return -ENOMEM; > > - priv->idev->name = "Huawei WMI hotkeys"; > - priv->idev->phys = "wmi/input0"; > - priv->idev->id.bustype = BUS_HOST; > - priv->idev->dev.parent = &wdev->dev; > + (*idev)->name = "Huawei WMI hotkeys"; > + (*idev)->phys = "wmi/input0"; > + (*idev)->id.bustype = BUS_HOST; > + (*idev)->dev.parent = &pdev->dev; > + > + err = sparse_keymap_setup(*idev, huawei_wmi_keymap, NULL); > + if (err) > + return err; > + > + return input_register_device(*idev); > +} > + > +/* LEDs */ > + > +static void huawei_wmi_micmute_led_set(struct led_classdev > *led_cdev, > + enum led_brightness brightness) > +{ > + /* This is a workaround until the "legacy" interface is > implemented. */ > + if (quirks && quirks->ec_micmute) { > + char *acpi_method; > + acpi_handle handle; > + union acpi_object args[3]; > + struct acpi_object_list arg_list = { > + .pointer = args, > + .count = ARRAY_SIZE(args), > + }; > + > + handle = ec_get_handle(); > + if (!handle) { > + dev_err(led_cdev->dev->parent, "Failed to get > EC handle\n"); > + return; > + } > + > + args[0].type = args[1].type = args[2].type = > ACPI_TYPE_INTEGER; > + args[1].integer.value = 0x04; > + > + if (acpi_has_method(handle, "SPIN")) { > + acpi_method = "SPIN"; > + args[0].integer.value = 0; > + args[2].integer.value = brightness ? 1 : 0; > + } else if (acpi_has_method(handle, "WPIN")) { > + acpi_method = "WPIN"; > + args[0].integer.value = 1; > + args[2].integer.value = brightness ? 0 : 1; > + } else { > + return; > + } > + > + acpi_evaluate_object(handle, acpi_method, &arg_list, > NULL); > + } else { > + u8 arg[] = { 0, 0, brightness, 0 }; > + > + huawei_wmi_cmd(led_cdev->dev->parent, MICMUTE_LED, arg, > NULL, NULL); > + } > +} > + > +static int huawei_wmi_leds_setup(struct device *dev) > +{ > + struct huawei_wmi *huawei = dev_get_drvdata(dev); > > - err = sparse_keymap_setup(priv->idev, huawei_wmi_keymap, NULL); > + huawei->cdev.name = "platform::micmute"; > + huawei->cdev.max_brightness = 1; > + huawei->cdev.brightness_set = huawei_wmi_micmute_led_set; > + huawei->cdev.default_trigger = "audio-micmute"; > + huawei->cdev.brightness = ledtrig_audio_get(LED_AUDIO_MICMUTE); > + huawei->cdev.dev = dev->parent; > + huawei->cdev.flags = LED_CORE_SUSPENDRESUME; > + > + return devm_led_classdev_register(dev, &huawei->cdev); > +} > + > +/* Battery protection */ > + > +static int huawei_wmi_battery_get(struct device *dev, int *low, int > *high) > +{ > + struct huawei_wmi *huawei = dev_get_drvdata(dev); > + u8 ret[0x100] = { 0 }; > + int err, i = 0x100; > + > + mutex_lock(&huawei->battery_lock); > + err = huawei_wmi_cmd(dev, BATTERY_GET, NULL, ret, 0x100); > + mutex_unlock(&huawei->battery_lock); > if (err) > return err; > > - return input_register_device(priv->idev); > + /* Returned buffer positions battery thresholds either in index > + * 3 and 2 or in 2 and 1. 0 reserved for return status. We > + * find the first non-zero value. > + */ > + while (i > 0 && !ret[i--]) > + ; > + *low = ret[i]; > + *high = ret[i+1]; > + > + return 0; > } > > -static int huawei_wmi_probe(struct wmi_device *wdev) > +static int huawei_wmi_battery_set(struct device *dev, int low, int > high) > { > - struct huawei_wmi_priv *priv; > + struct huawei_wmi *huawei = dev_get_drvdata(dev); > + u8 arg[] = { 0, 0, low, high }; > int err; > > - priv = devm_kzalloc(&wdev->dev, sizeof(struct huawei_wmi_priv), > GFP_KERNEL); > - if (!priv) > - return -ENOMEM; > + /* This is an edge case were some models turn battery > protection > + * off without changing their thresholds values. We clear the > + * values before turning off protection. We need a blocking > delay to > + * make sure these values make their way to EC. > + */ > + if (low == 0 && high == 100) > + huawei_wmi_battery_set(dev, 0, 0); > > - dev_set_drvdata(&wdev->dev, priv); > + mutex_lock(&huawei->battery_lock); > + err = huawei_wmi_cmd(dev, BATTERY_SET, arg, NULL, NULL); > + if (quirks && quirks->battery_sleep) > + msleep(1000); > + mutex_unlock(&huawei->battery_lock); > > - err = huawei_wmi_input_setup(wdev); > + return err; > +} > + > +/* Fn lock */ > + > +static int huawei_wmi_fn_lock_get(struct device *dev, int *on) > +{ > + u8 ret[0x100] = { 0 }; > + int err, i = 0; > + > + err = huawei_wmi_cmd(dev, FN_LOCK_GET, NULL, ret, 0x100); > if (err) > return err; > > - return huawei_wmi_leds_setup(wdev); > + /* Find the first non-zero value */ > + while (i <= 0x100 && !ret[i++]) > + ; > + *on = (ret[i-1] == FN_LOCK_OFF) ? 0 : 1; > + > + return 0; > } > > -static const struct wmi_device_id huawei_wmi_id_table[] = { > - { .guid_string = WMI0_EVENT_GUID }, > - { .guid_string = AMW0_EVENT_GUID }, > - { } > +static int huawei_wmi_fn_lock_set(struct device *dev, int on) > +{ > + u8 arg[] = { 0, 0, (on) ? FN_LOCK_ON : FN_LOCK_OFF, 0 }; > + > + return huawei_wmi_cmd(dev, FN_LOCK_SET, arg, NULL, NULL); > +} > + > +/* sysfs */ > + > +static ssize_t charge_thresholds_store(struct device *dev, > + struct device_attribute *attr, > + const char *buf, size_t size) > +{ > + int low, high, err; > + > + if (sscanf(buf, "%d %d", &low, &high) != 2 || > + low < 0 || high > 100 || > + low > high) > + return -EINVAL; > + > + err = huawei_wmi_battery_set(dev, low, high); > + if (err) > + return err; > + > + return size; > +} > + > +static ssize_t fn_lock_state_store(struct device *dev, > + struct device_attribute *attr, > + const char *buf, size_t size) > +{ > + int on, err; > + > + if (kstrtoint(buf, 10, &on) || > + on < 0 || on > 1) > + return -EINVAL; > + > + err = huawei_wmi_fn_lock_set(dev, on); > + if (err) > + return err; > + > + return size; > +} > + > +static ssize_t charge_thresholds_show(struct device *dev, > + struct device_attribute *attr, > + char *buf) > +{ > + int err, low = -1, high = -1; > + > + err = huawei_wmi_battery_get(dev, &low, &high); > + if (err) > + return err; > + > + return sprintf(buf, "%d %d\n", low, high); > +} > + > +static ssize_t fn_lock_state_show(struct device *dev, > + struct device_attribute *attr, > + char *buf) > +{ > + int err, on = -1; > + > + err = huawei_wmi_fn_lock_get(dev, &on); > + if (err) > + return err; > + > + return sprintf(buf, "%d\n", on); > +} > + > +static DEVICE_ATTR_RW(charge_thresholds); > +static DEVICE_ATTR_RW(fn_lock_state); > + > +static struct attribute *huawei_wmi_attrs[] = { > + &dev_attr_charge_thresholds.attr, > + &dev_attr_fn_lock_state.attr, > + NULL > +}; > + > +static const struct attribute_group huawei_wmi_group = { > + .attrs = huawei_wmi_attrs > }; > > -static struct wmi_driver huawei_wmi_driver = { > +/* Huawei driver */ > + > +static int huawei_wmi_probe(struct platform_device *pdev) > +{ > + struct huawei_wmi *huawei; > + int err; > + > + huawei = devm_kzalloc(&pdev->dev, sizeof(struct huawei_wmi), > GFP_KERNEL); > + if (!huawei) > + return -ENOMEM; > + > + huawei->pdev = pdev; > + dev_set_drvdata(&pdev->dev, huawei); > + > + if (wmi_has_guid(WMI0_EVENT_GUID)) { > + err = huawei_wmi_input_setup(pdev, &huawei->idev[0]); > + if (err) > + dev_err(&pdev->dev, "Failed to setup input > device\n"); > + err = wmi_install_notify_handler(WMI0_EVENT_GUID, > + huawei_wmi_input_notify, huawei- > >idev[0]); > + if (err) > + dev_err(&pdev->dev, "Failed to install notify > handler\n"); > + } > + > + if (wmi_has_guid(AMW0_EVENT_GUID)) { > + err = huawei_wmi_input_setup(pdev, &huawei->idev[1]); > + if (err) > + dev_err(&pdev->dev, "Failed to setup input > device\n"); > + err = wmi_install_notify_handler(AMW0_EVENT_GUID, > + huawei_wmi_input_notify, huawei- > >idev[1]); > + if (err) > + dev_err(&pdev->dev, "Failed to install notify > handler\n"); > + } > + > + if (wmi_has_guid(AMW0_METHOD_GUID)) { > + > + mutex_init(&huawei->wmi_lock); > + mutex_init(&huawei->battery_lock); > + > + err = sysfs_create_group(&pdev->dev.kobj, > &huawei_wmi_group); > + if (err) { > + dev_err(&pdev->dev, "Failed to create sysfs > interface\n"); > + return err; > + } > + > + err = huawei_wmi_leds_setup(&pdev->dev); > + if (err) > + dev_err(&pdev->dev, "Failed to setup leds\n"); > + } > + > + return 0; > +} > + > +static int huawei_wmi_remove(struct platform_device *pdev) > +{ > + if (wmi_has_guid(WMI0_EVENT_GUID)) > + wmi_remove_notify_handler(WMI0_EVENT_GUID); > + > + if (wmi_has_guid(AMW0_EVENT_GUID)) > + wmi_remove_notify_handler(AMW0_EVENT_GUID); > + > + if (wmi_has_guid(AMW0_METHOD_GUID)) > + sysfs_remove_group(&pdev->dev.kobj, &huawei_wmi_group); > + > + return 0; > +} > + > +static struct platform_driver huawei_wmi_driver = { > .driver = { > .name = "huawei-wmi", > }, > - .id_table = huawei_wmi_id_table, > .probe = huawei_wmi_probe, > - .notify = huawei_wmi_notify, > + .remove = huawei_wmi_remove, > }; > > -module_wmi_driver(huawei_wmi_driver); > +static __init int huawei_wmi_init(void) > +{ > + int err; > + > + quirks = &quirk_unknown; > + dmi_check_system(huawei_quirks); > + quirks->battery_sleep |= battery_sleep; > + quirks->report_brightness |= report_brightness; > + > + err = platform_driver_register(&huawei_wmi_driver); > + if (err) { > + pr_err("Failed to register platform driver\n"); > + return err; > + } > + > + huawei_wmi_pdev = platform_device_register_simple("huawei-wmi", > -1, NULL, 0); > + if (IS_ERR(huawei_wmi_pdev)) { > + pr_err("Failed to register platform device\n"); > + platform_driver_unregister(&huawei_wmi_driver); > + return PTR_ERR(huawei_wmi_pdev); > + } > + > + return 0; > +} > + > +static __exit void huawei_wmi_exit(void) > +{ > + platform_device_unregister(huawei_wmi_pdev); > + platform_driver_unregister(&huawei_wmi_driver); > +} > + > +module_init(huawei_wmi_init); > +module_exit(huawei_wmi_exit); > > -MODULE_DEVICE_TABLE(wmi, huawei_wmi_id_table); > +MODULE_ALIAS("wmi:"WMI0_EVENT_GUID); > +MODULE_ALIAS("wmi:"AMW0_EVENT_GUID); > +MODULE_ALIAS("wmi:"AMW0_METHOD_GUID); > MODULE_AUTHOR("Ayman Bagabas <ayman.bagabas@gmail.com>"); > -MODULE_DESCRIPTION("Huawei WMI hotkeys"); > +MODULE_DESCRIPTION("Huawei WMI laptop extras driver"); > MODULE_LICENSE("GPL v2");
diff --git a/drivers/platform/x86/huawei-wmi.c b/drivers/platform/x86/huawei-wmi.c index 52fcac5b393a..3f945b4cf115 100644 --- a/drivers/platform/x86/huawei-wmi.c +++ b/drivers/platform/x86/huawei-wmi.c @@ -1,32 +1,65 @@ // SPDX-License-Identifier: GPL-2.0 /* - * Huawei WMI hotkeys + * Huawei WMI laptop extras driver * * Copyright (C) 2018 Ayman Bagabas <ayman.bagabas@gmail.com> */ #include <linux/acpi.h> +#include <linux/delay.h> +#include <linux/dmi.h> #include <linux/input.h> #include <linux/input/sparse-keymap.h> #include <linux/leds.h> #include <linux/module.h> +#include <linux/mutex.h> +#include <linux/platform_device.h> +#include <linux/sysfs.h> #include <linux/wmi.h> /* * Huawei WMI GUIDs */ -#define WMI0_EVENT_GUID "59142400-C6A3-40fa-BADB-8A2652834100" +#define AMW0_METHOD_GUID "ABBC0F5B-8EA1-11D1-A000-C90629100000" #define AMW0_EVENT_GUID "ABBC0F5C-8EA1-11D1-A000-C90629100000" +/* Legacy GUIDs */ #define WMI0_EXPENSIVE_GUID "39142400-C6A3-40fa-BADB-8A2652834100" +#define WMI0_EVENT_GUID "59142400-C6A3-40fa-BADB-8A2652834100" + +/* AMW0_commands */ + +enum wmaa_cmd { + BATTERY_GET, /* \GBTT 0x00001103 */ + BATTERY_SET, /* \SBTT 0xXXYY1003 */ + FN_LOCK_GET, /* \GFRS 0x00000604 */ + FN_LOCK_SET, /* \SFRS 0x000X0704 */ + MICMUTE_LED, /* \SMLS 0x000X0b04 */ +}; + +enum fn_state { + FN_LOCK_OFF = 0x01, + FN_LOCK_ON = 0x02, +}; + +struct quirk_entry { + bool battery_sleep; + bool ec_micmute; + bool report_brightness; +}; -struct huawei_wmi_priv { - struct input_dev *idev; +static struct quirk_entry *quirks; + +struct huawei_wmi { struct led_classdev cdev; - acpi_handle handle; - char *acpi_method; + struct mutex wmi_lock; + struct mutex battery_lock; + struct input_dev *idev[2]; + struct platform_device *pdev; }; +struct platform_device *huawei_wmi_pdev; + static const struct key_entry huawei_wmi_keymap[] = { { KE_KEY, 0x281, { KEY_BRIGHTNESSDOWN } }, { KE_KEY, 0x282, { KEY_BRIGHTNESSUP } }, @@ -37,73 +70,183 @@ static const struct key_entry huawei_wmi_keymap[] = { { KE_KEY, 0x289, { KEY_WLAN } }, // Huawei |M| key { KE_KEY, 0x28a, { KEY_CONFIG } }, - // Keyboard backlight + // Keyboard backlit { KE_IGNORE, 0x293, { KEY_KBDILLUMTOGGLE } }, { KE_IGNORE, 0x294, { KEY_KBDILLUMUP } }, { KE_IGNORE, 0x295, { KEY_KBDILLUMUP } }, { KE_END, 0 } }; -static int huawei_wmi_micmute_led_set(struct led_classdev *led_cdev, - enum led_brightness brightness) +static bool battery_sleep; +static bool report_brightness; + +module_param(battery_sleep, bool, 0444); +MODULE_PARM_DESC(battery_sleep, + "Delay after setting battery charging thresholds."); +module_param(report_brightness, bool, 0444); +MODULE_PARM_DESC(report_brightness, + "Report brightness key events."); + +/* Quirks */ + +static int __init dmi_matched(const struct dmi_system_id *dmi) { - struct huawei_wmi_priv *priv = dev_get_drvdata(led_cdev->dev->parent); + quirks = dmi->driver_data; + return 1; +} + +static struct quirk_entry quirk_unknown = { +}; + +static struct quirk_entry quirk_battery_sleep = { + .battery_sleep = true, +}; + +static struct quirk_entry quirk_matebook_x = { + .ec_micmute = true, + .report_brightness = true, +}; + +static const struct dmi_system_id huawei_quirks[] = { + { + .callback = dmi_matched, + .ident = "Huawei MACH-WX9", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HUAWEI"), + DMI_MATCH(DMI_PRODUCT_NAME, "MACH-WX9"), + }, + .driver_data = &quirk_battery_sleep + }, + { + .callback = dmi_matched, + .ident = "Huawei MateBook X", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HUAWEI"), + DMI_MATCH(DMI_PRODUCT_NAME, "HUAWEI MateBook X") + }, + .driver_data = &quirk_matebook_x + }, + { } +}; + +/* Utils */ + +static int huawei_wmi_eval(struct device *dev, u8 *arg, + u8 *buf, size_t buflen) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer in; + union acpi_object *obj; acpi_status status; - union acpi_object args[3]; - struct acpi_object_list arg_list = { - .pointer = args, - .count = ARRAY_SIZE(args), - }; - - args[0].type = args[1].type = args[2].type = ACPI_TYPE_INTEGER; - args[1].integer.value = 0x04; - - if (strcmp(priv->acpi_method, "SPIN") == 0) { - args[0].integer.value = 0; - args[2].integer.value = brightness ? 1 : 0; - } else if (strcmp(priv->acpi_method, "WPIN") == 0) { - args[0].integer.value = 1; - args[2].integer.value = brightness ? 0 : 1; - } else { - return -EINVAL; + size_t len; + int err = -EIO; + + in.length = sizeof(u8) * 4; + in.pointer = (u32 *)arg; + mutex_lock(&huawei->wmi_lock); + status = wmi_evaluate_method(AMW0_METHOD_GUID, 0, 1, &in, &out); + if (ACPI_FAILURE(status)) { + dev_err(dev, "Failed to evaluate wmi method\n"); + err = -ENODEV; + goto wmi_eval_fail; + } + + /* WMAA takes a 4 bytes buffer as an input. It returns a package + * with two buffer elements. The first buffer is 4 bytes long and + * the second is 0x100 (256) bytes long. The first buffer is always + * zeros. The second stores the output from every call. The first + * byte of the second buffer always have the return status of the + * called command. + */ + obj = out.pointer; + if (!obj) + goto wmi_eval_fail; + if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count != 2) { + dev_err(dev, "Unknown response type %d\n", obj->type); + goto wmi_eval_fail; } - status = acpi_evaluate_object(priv->handle, priv->acpi_method, &arg_list, NULL); - if (ACPI_FAILURE(status)) - return -ENXIO; + obj = &(obj->package.elements[1]); + if (!obj || obj->type != ACPI_TYPE_BUFFER) + goto wmi_eval_fail; - return 0; + if (buf) { + len = min(buflen, obj->buffer.length); + memcpy(buf, obj->buffer.pointer, len); + } + err = 0; + +wmi_eval_fail: + mutex_unlock(&huawei->wmi_lock); + kfree(out.pointer); + return err; } -static int huawei_wmi_leds_setup(struct wmi_device *wdev) +static int huawei_wmi_cmd(struct device *dev, enum wmaa_cmd cmd, u8 *arg, + u8 *out, size_t outlen) { - struct huawei_wmi_priv *priv = dev_get_drvdata(&wdev->dev); - - priv->handle = ec_get_handle(); - if (!priv->handle) - return 0; + u8 parm[4] = { 0 }; + u8 buf[0x100] = { 0xff }; + int err; - if (acpi_has_method(priv->handle, "SPIN")) - priv->acpi_method = "SPIN"; - else if (acpi_has_method(priv->handle, "WPIN")) - priv->acpi_method = "WPIN"; - else - return 0; + if (!arg) + arg = parm; + + switch (cmd) { + case BATTERY_SET: + arg[0] = 0x03; + arg[1] = 0x10; + break; + case BATTERY_GET: + arg[0] = 0x03; + arg[1] = 0x11; + break; + case FN_LOCK_GET: + arg[0] = 0x04; + arg[1] = 0x06; + break; + case FN_LOCK_SET: + arg[0] = 0x04; + arg[1] = 0x07; + break; + case MICMUTE_LED: + arg[0] = 0x04; + arg[1] = 0x0b; + break; + default: + dev_err(dev, "Unsupported command, got: 0x%08x\n", *(u32 *)arg); + return -EINVAL; + } - priv->cdev.name = "platform::micmute"; - priv->cdev.max_brightness = 1; - priv->cdev.brightness_set_blocking = huawei_wmi_micmute_led_set; - priv->cdev.default_trigger = "audio-micmute"; - priv->cdev.brightness = ledtrig_audio_get(LED_AUDIO_MICMUTE); - priv->cdev.dev = &wdev->dev; - priv->cdev.flags = LED_CORE_SUSPENDRESUME; + /* Some models require calling WMAA twice to execute + * a command. We call WMAA and if we get a non-zero return + * status we evaluate WMAA again. If we get another non-zero + * return, we return -EIO. This way we don't need to + * check for return status anywhere we call huawei_wmi_cmd. + */ + err = huawei_wmi_eval(dev, arg, buf, 0x100); + if (err) + return err; + if (buf[0]) { + err = huawei_wmi_eval(dev, arg, buf, 0x100); + if (err) + return err; + if (buf[0]) { + dev_err(dev, "Invalid response, got: %d\n", buf[0]); + return -EIO; + } + } + if (out) + memcpy(out, buf, outlen); - return devm_led_classdev_register(&wdev->dev, &priv->cdev); + return 0; } -static void huawei_wmi_process_key(struct wmi_device *wdev, int code) +/* Input */ + +static void huawei_wmi_process_key(struct input_dev *idev, int code) { - struct huawei_wmi_priv *priv = dev_get_drvdata(&wdev->dev); const struct key_entry *key; /* @@ -127,81 +270,390 @@ static void huawei_wmi_process_key(struct wmi_device *wdev, int code) kfree(response.pointer); } - key = sparse_keymap_entry_from_scancode(priv->idev, code); + key = sparse_keymap_entry_from_scancode(idev, code); if (!key) { - dev_info(&wdev->dev, "Unknown key pressed, code: 0x%04x\n", code); + dev_info(&idev->dev, "Unknown key pressed, code: 0x%04x\n", code); return; } - sparse_keymap_report_entry(priv->idev, key, 1, true); + if (quirks && !quirks->report_brightness && + (key->sw.code == KEY_BRIGHTNESSDOWN || + key->sw.code == KEY_BRIGHTNESSUP)) + return; + + sparse_keymap_report_entry(idev, key, 1, true); } -static void huawei_wmi_notify(struct wmi_device *wdev, - union acpi_object *obj) +static void huawei_wmi_input_notify(u32 value, void *context) { - if (obj->type == ACPI_TYPE_INTEGER) - huawei_wmi_process_key(wdev, obj->integer.value); + struct input_dev *idev = (struct input_dev *)context; + struct acpi_buffer response = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *obj; + acpi_status status; + + if (!idev) { + dev_err(&huawei_wmi_pdev->dev, "No input_dev\n"); + return; + } + + status = wmi_get_event_data(value, &response); + if (ACPI_FAILURE(status)) { + dev_err(&idev->dev, "Unable to get event data\n"); + return; + } + + obj = (union acpi_object *)response.pointer; + if (obj && obj->type == ACPI_TYPE_INTEGER) + huawei_wmi_process_key(idev, obj->integer.value); else - dev_info(&wdev->dev, "Bad response type %d\n", obj->type); + dev_err(&idev->dev, "Bad response type\n"); + + kfree(response.pointer); } -static int huawei_wmi_input_setup(struct wmi_device *wdev) +static int huawei_wmi_input_setup(struct platform_device *pdev, + struct input_dev **idev) { - struct huawei_wmi_priv *priv = dev_get_drvdata(&wdev->dev); int err; - priv->idev = devm_input_allocate_device(&wdev->dev); - if (!priv->idev) + *idev = devm_input_allocate_device(&pdev->dev); + if (!*idev) return -ENOMEM; - priv->idev->name = "Huawei WMI hotkeys"; - priv->idev->phys = "wmi/input0"; - priv->idev->id.bustype = BUS_HOST; - priv->idev->dev.parent = &wdev->dev; + (*idev)->name = "Huawei WMI hotkeys"; + (*idev)->phys = "wmi/input0"; + (*idev)->id.bustype = BUS_HOST; + (*idev)->dev.parent = &pdev->dev; + + err = sparse_keymap_setup(*idev, huawei_wmi_keymap, NULL); + if (err) + return err; + + return input_register_device(*idev); +} + +/* LEDs */ + +static void huawei_wmi_micmute_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + /* This is a workaround until the "legacy" interface is implemented. */ + if (quirks && quirks->ec_micmute) { + char *acpi_method; + acpi_handle handle; + union acpi_object args[3]; + struct acpi_object_list arg_list = { + .pointer = args, + .count = ARRAY_SIZE(args), + }; + + handle = ec_get_handle(); + if (!handle) { + dev_err(led_cdev->dev->parent, "Failed to get EC handle\n"); + return; + } + + args[0].type = args[1].type = args[2].type = ACPI_TYPE_INTEGER; + args[1].integer.value = 0x04; + + if (acpi_has_method(handle, "SPIN")) { + acpi_method = "SPIN"; + args[0].integer.value = 0; + args[2].integer.value = brightness ? 1 : 0; + } else if (acpi_has_method(handle, "WPIN")) { + acpi_method = "WPIN"; + args[0].integer.value = 1; + args[2].integer.value = brightness ? 0 : 1; + } else { + return; + } + + acpi_evaluate_object(handle, acpi_method, &arg_list, NULL); + } else { + u8 arg[] = { 0, 0, brightness, 0 }; + + huawei_wmi_cmd(led_cdev->dev->parent, MICMUTE_LED, arg, NULL, NULL); + } +} + +static int huawei_wmi_leds_setup(struct device *dev) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); - err = sparse_keymap_setup(priv->idev, huawei_wmi_keymap, NULL); + huawei->cdev.name = "platform::micmute"; + huawei->cdev.max_brightness = 1; + huawei->cdev.brightness_set = huawei_wmi_micmute_led_set; + huawei->cdev.default_trigger = "audio-micmute"; + huawei->cdev.brightness = ledtrig_audio_get(LED_AUDIO_MICMUTE); + huawei->cdev.dev = dev->parent; + huawei->cdev.flags = LED_CORE_SUSPENDRESUME; + + return devm_led_classdev_register(dev, &huawei->cdev); +} + +/* Battery protection */ + +static int huawei_wmi_battery_get(struct device *dev, int *low, int *high) +{ + struct huawei_wmi *huawei = dev_get_drvdata(dev); + u8 ret[0x100] = { 0 }; + int err, i = 0x100; + + mutex_lock(&huawei->battery_lock); + err = huawei_wmi_cmd(dev, BATTERY_GET, NULL, ret, 0x100); + mutex_unlock(&huawei->battery_lock); if (err) return err; - return input_register_device(priv->idev); + /* Returned buffer positions battery thresholds either in index + * 3 and 2 or in 2 and 1. 0 reserved for return status. We + * find the first non-zero value. + */ + while (i > 0 && !ret[i--]) + ; + *low = ret[i]; + *high = ret[i+1]; + + return 0; } -static int huawei_wmi_probe(struct wmi_device *wdev) +static int huawei_wmi_battery_set(struct device *dev, int low, int high) { - struct huawei_wmi_priv *priv; + struct huawei_wmi *huawei = dev_get_drvdata(dev); + u8 arg[] = { 0, 0, low, high }; int err; - priv = devm_kzalloc(&wdev->dev, sizeof(struct huawei_wmi_priv), GFP_KERNEL); - if (!priv) - return -ENOMEM; + /* This is an edge case were some models turn battery protection + * off without changing their thresholds values. We clear the + * values before turning off protection. We need a blocking delay to + * make sure these values make their way to EC. + */ + if (low == 0 && high == 100) + huawei_wmi_battery_set(dev, 0, 0); - dev_set_drvdata(&wdev->dev, priv); + mutex_lock(&huawei->battery_lock); + err = huawei_wmi_cmd(dev, BATTERY_SET, arg, NULL, NULL); + if (quirks && quirks->battery_sleep) + msleep(1000); + mutex_unlock(&huawei->battery_lock); - err = huawei_wmi_input_setup(wdev); + return err; +} + +/* Fn lock */ + +static int huawei_wmi_fn_lock_get(struct device *dev, int *on) +{ + u8 ret[0x100] = { 0 }; + int err, i = 0; + + err = huawei_wmi_cmd(dev, FN_LOCK_GET, NULL, ret, 0x100); if (err) return err; - return huawei_wmi_leds_setup(wdev); + /* Find the first non-zero value */ + while (i <= 0x100 && !ret[i++]) + ; + *on = (ret[i-1] == FN_LOCK_OFF) ? 0 : 1; + + return 0; } -static const struct wmi_device_id huawei_wmi_id_table[] = { - { .guid_string = WMI0_EVENT_GUID }, - { .guid_string = AMW0_EVENT_GUID }, - { } +static int huawei_wmi_fn_lock_set(struct device *dev, int on) +{ + u8 arg[] = { 0, 0, (on) ? FN_LOCK_ON : FN_LOCK_OFF, 0 }; + + return huawei_wmi_cmd(dev, FN_LOCK_SET, arg, NULL, NULL); +} + +/* sysfs */ + +static ssize_t charge_thresholds_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int low, high, err; + + if (sscanf(buf, "%d %d", &low, &high) != 2 || + low < 0 || high > 100 || + low > high) + return -EINVAL; + + err = huawei_wmi_battery_set(dev, low, high); + if (err) + return err; + + return size; +} + +static ssize_t fn_lock_state_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int on, err; + + if (kstrtoint(buf, 10, &on) || + on < 0 || on > 1) + return -EINVAL; + + err = huawei_wmi_fn_lock_set(dev, on); + if (err) + return err; + + return size; +} + +static ssize_t charge_thresholds_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int err, low = -1, high = -1; + + err = huawei_wmi_battery_get(dev, &low, &high); + if (err) + return err; + + return sprintf(buf, "%d %d\n", low, high); +} + +static ssize_t fn_lock_state_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int err, on = -1; + + err = huawei_wmi_fn_lock_get(dev, &on); + if (err) + return err; + + return sprintf(buf, "%d\n", on); +} + +static DEVICE_ATTR_RW(charge_thresholds); +static DEVICE_ATTR_RW(fn_lock_state); + +static struct attribute *huawei_wmi_attrs[] = { + &dev_attr_charge_thresholds.attr, + &dev_attr_fn_lock_state.attr, + NULL +}; + +static const struct attribute_group huawei_wmi_group = { + .attrs = huawei_wmi_attrs }; -static struct wmi_driver huawei_wmi_driver = { +/* Huawei driver */ + +static int huawei_wmi_probe(struct platform_device *pdev) +{ + struct huawei_wmi *huawei; + int err; + + huawei = devm_kzalloc(&pdev->dev, sizeof(struct huawei_wmi), GFP_KERNEL); + if (!huawei) + return -ENOMEM; + + huawei->pdev = pdev; + dev_set_drvdata(&pdev->dev, huawei); + + if (wmi_has_guid(WMI0_EVENT_GUID)) { + err = huawei_wmi_input_setup(pdev, &huawei->idev[0]); + if (err) + dev_err(&pdev->dev, "Failed to setup input device\n"); + err = wmi_install_notify_handler(WMI0_EVENT_GUID, + huawei_wmi_input_notify, huawei->idev[0]); + if (err) + dev_err(&pdev->dev, "Failed to install notify handler\n"); + } + + if (wmi_has_guid(AMW0_EVENT_GUID)) { + err = huawei_wmi_input_setup(pdev, &huawei->idev[1]); + if (err) + dev_err(&pdev->dev, "Failed to setup input device\n"); + err = wmi_install_notify_handler(AMW0_EVENT_GUID, + huawei_wmi_input_notify, huawei->idev[1]); + if (err) + dev_err(&pdev->dev, "Failed to install notify handler\n"); + } + + if (wmi_has_guid(AMW0_METHOD_GUID)) { + + mutex_init(&huawei->wmi_lock); + mutex_init(&huawei->battery_lock); + + err = sysfs_create_group(&pdev->dev.kobj, &huawei_wmi_group); + if (err) { + dev_err(&pdev->dev, "Failed to create sysfs interface\n"); + return err; + } + + err = huawei_wmi_leds_setup(&pdev->dev); + if (err) + dev_err(&pdev->dev, "Failed to setup leds\n"); + } + + return 0; +} + +static int huawei_wmi_remove(struct platform_device *pdev) +{ + if (wmi_has_guid(WMI0_EVENT_GUID)) + wmi_remove_notify_handler(WMI0_EVENT_GUID); + + if (wmi_has_guid(AMW0_EVENT_GUID)) + wmi_remove_notify_handler(AMW0_EVENT_GUID); + + if (wmi_has_guid(AMW0_METHOD_GUID)) + sysfs_remove_group(&pdev->dev.kobj, &huawei_wmi_group); + + return 0; +} + +static struct platform_driver huawei_wmi_driver = { .driver = { .name = "huawei-wmi", }, - .id_table = huawei_wmi_id_table, .probe = huawei_wmi_probe, - .notify = huawei_wmi_notify, + .remove = huawei_wmi_remove, }; -module_wmi_driver(huawei_wmi_driver); +static __init int huawei_wmi_init(void) +{ + int err; + + quirks = &quirk_unknown; + dmi_check_system(huawei_quirks); + quirks->battery_sleep |= battery_sleep; + quirks->report_brightness |= report_brightness; + + err = platform_driver_register(&huawei_wmi_driver); + if (err) { + pr_err("Failed to register platform driver\n"); + return err; + } + + huawei_wmi_pdev = platform_device_register_simple("huawei-wmi", -1, NULL, 0); + if (IS_ERR(huawei_wmi_pdev)) { + pr_err("Failed to register platform device\n"); + platform_driver_unregister(&huawei_wmi_driver); + return PTR_ERR(huawei_wmi_pdev); + } + + return 0; +} + +static __exit void huawei_wmi_exit(void) +{ + platform_device_unregister(huawei_wmi_pdev); + platform_driver_unregister(&huawei_wmi_driver); +} + +module_init(huawei_wmi_init); +module_exit(huawei_wmi_exit); -MODULE_DEVICE_TABLE(wmi, huawei_wmi_id_table); +MODULE_ALIAS("wmi:"WMI0_EVENT_GUID); +MODULE_ALIAS("wmi:"AMW0_EVENT_GUID); +MODULE_ALIAS("wmi:"AMW0_METHOD_GUID); MODULE_AUTHOR("Ayman Bagabas <ayman.bagabas@gmail.com>"); -MODULE_DESCRIPTION("Huawei WMI hotkeys"); +MODULE_DESCRIPTION("Huawei WMI laptop extras driver"); MODULE_LICENSE("GPL v2");
This patch introduces new features to the driver and also moves the driver from wmi_driver to platform_driver. This move is necessary because the driver is no longer only a hotkeys driver and platform_driver offers easier future extensibility. The patch introduces a WMI BIOS interface that brings on new features and enables controlling micmute LED through this interface on supported models. New features are controlling battery charging thresholds and fn-lock state among with module parameters and quirks check. Currently, micmute LED is controlled through an ACPI method under EC. This method ("SPIN", "WPIN") is specific to some models and wouldn't work on all Huawei laptops. Using this interface, controlling this LED should work with any model. Except `MateBook X` from 2017, this one doesn't provide controlling the LED through this interface instead it uses another "legacy" interface that is not "fully" implemented yet due to lack of hardware. Currently, this "legacy" interface is used for hotkeys on this specific model. A quirk is set to use ACPI method to control micmute LED on this model. Some models that implement the new WMI BIOS interface can control battery charging thresholds where it limits charging the battery once it reaches certain thresholds. The behavior of hotkeys is not the same among all models. Some models require fn-lock to do things like `Ctrl-Ins` or `Alt-PrtSc`. By default, hotkeys behave as special keys (media keys, Ins, etc), but if a modifier is used (ctrl, alt, shift) these keys behave as F1-F12 keys. If the Fn key is toggled on, the hotkeys with or without a modifier, behave as F1-F12 keys. This makes it impossible to use a modifier and `PrtSc` or `Ins`. Now, some models fix this by excluding `PrtSc` and `Ins` keys from being treated as F11 and F12 keys with the use of a modifier. However, some models do not, and fixes this by the so called fn-lock. Fn-lock inverts the behavior of the top row from special keys to F1-F12 keys. So a modifier and a special key would be possible which make things like `Alt-Ins` possible. Now, with fn-lock we would have 4 modes: * Fn-key off & fn-lock off - hotkeys treated as special keys using a modifier gives F1-F12 keys. * Fn-key on & fn-lock off - hotkeys treated as F1-F12 keys and using a modifier gives F1-F12. * Fn-key off & fn-lock on - hotkeys are treated as F1-F12 keys and using a modifier gives special keys. * Fn-key on & fn-lock on - hotkeys are treated as special keys and using a modifier gives special keys. The driver introduces two parameters to force reporting brightness keys and sleeping after setting a threshold value. All newer models that "fully" implement the new interface report brightness key events twice, once through WMI and once through acpi-video. Older models, such as `MateBook X`, don't report brightness events using WMI. This is implemented as a quirk and can be forced using module parameters. Some models don't allow setting thresholds to (0, 100), due to bad ASL code, which indicates reset values, instead, it only turns off battery charging protection. This would return the previously set values even though battery protection is off which doesn't make sense. A sane value like (0, 100) indicates no charging protection, but since it's not possible to set such values, (0, 0) is set before turning protection off with (0, 100). This requires a delay after setting (0, 0) and after (0, 100) so that these values make their way to EC memory. These parameters are implemented as quirks along with `ec_micmute` quirk which controls the micmute LED through ACPI EC interface. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com> --- drivers/platform/x86/huawei-wmi.c | 630 +++++++++++++++++++++++++----- 1 file changed, 541 insertions(+), 89 deletions(-)