HID: hid-asus: Add BT keyboard dock battery monitoring support
authorNOGUCHI Hiroshi <drvlabo@gmail.com>
Tue, 29 Jan 2019 04:31:29 +0000 (13:31 +0900)
committerJiri Kosina <jkosina@suse.cz>
Wed, 13 Feb 2019 23:01:30 +0000 (00:01 +0100)
Add battery monitoring support for Transbook T100CHI/T90CHI's Bluetooth
keyboard dock.  They report rest battery level and charging status.

Signed-off-by: NOGUCHI Hiroshi <drvlabo@gmail.com>
Signed-off-by: Jiri Kosina <jkosina@suse.cz>
drivers/hid/hid-asus.c

index c7b4638..336aeae 100644 (file)
@@ -32,6 +32,7 @@
 #include <linux/platform_data/x86/asus-wmi.h>
 #include <linux/input/mt.h>
 #include <linux/usb.h> /* For to_usb_interface for T100 touchpad intf check */
+#include <linux/power_supply.h>
 
 #include "hid-ids.h"
 
@@ -61,6 +62,13 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
 #define CONTACT_TOUCH_MAJOR_MASK 0x07
 #define CONTACT_PRESSURE_MASK 0x7f
 
+#define        BATTERY_REPORT_ID       (0x03)
+#define        BATTERY_REPORT_SIZE     (1 + 8)
+#define        BATTERY_LEVEL_MAX       ((u8)255)
+#define        BATTERY_STAT_DISCONNECT (0)
+#define        BATTERY_STAT_CHARGING   (1)
+#define        BATTERY_STAT_FULL       (2)
+
 #define QUIRK_FIX_NOTEBOOK_REPORT      BIT(0)
 #define QUIRK_NO_INIT_REPORTS          BIT(1)
 #define QUIRK_SKIP_INPUT_MAPPING       BIT(2)
@@ -101,12 +109,21 @@ struct asus_touchpad_info {
 
 struct asus_drvdata {
        unsigned long quirks;
+       struct hid_device *hdev;
        struct input_dev *input;
        struct asus_kbd_leds *kbd_backlight;
        const struct asus_touchpad_info *tp;
        bool enable_backlight;
+       struct power_supply *battery;
+       struct power_supply_desc battery_desc;
+       int battery_capacity;
+       int battery_stat;
+       bool battery_in_query;
+       unsigned long battery_next_query;
 };
 
+static int asus_report_battery(struct asus_drvdata *, u8 *, int);
+
 static const struct asus_touchpad_info asus_i2c_tp = {
        .max_x = 2794,
        .max_y = 1758,
@@ -260,6 +277,9 @@ static int asus_raw_event(struct hid_device *hdev,
 {
        struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
 
+       if (drvdata->battery && data[0] == BATTERY_REPORT_ID)
+               return asus_report_battery(drvdata, data, size);
+
        if (drvdata->tp && data[0] == INPUT_REPORT_ID)
                return asus_report_input(drvdata, data, size);
 
@@ -429,6 +449,164 @@ static int asus_kbd_register_leds(struct hid_device *hdev)
        return ret;
 }
 
+/*
+ * [0]       REPORT_ID (same value defined in report descriptor)
+ * [1]      rest battery level. range [0..255]
+ * [2]..[7]  Bluetooth hardware address (MAC address)
+ * [8]       charging status
+ *            = 0 : AC offline / discharging
+ *            = 1 : AC online  / charging
+ *            = 2 : AC online  / fully charged
+ */
+static int asus_parse_battery(struct asus_drvdata *drvdata, u8 *data, int size)
+{
+       u8 sts;
+       u8 lvl;
+       int val;
+
+       lvl = data[1];
+       sts = data[8];
+
+       drvdata->battery_capacity = ((int)lvl * 100) / (int)BATTERY_LEVEL_MAX;
+
+       switch (sts) {
+       case BATTERY_STAT_CHARGING:
+               val = POWER_SUPPLY_STATUS_CHARGING;
+               break;
+       case BATTERY_STAT_FULL:
+               val = POWER_SUPPLY_STATUS_FULL;
+               break;
+       case BATTERY_STAT_DISCONNECT:
+       default:
+               val = POWER_SUPPLY_STATUS_DISCHARGING;
+               break;
+       }
+       drvdata->battery_stat = val;
+
+       return 0;
+}
+
+static int asus_report_battery(struct asus_drvdata *drvdata, u8 *data, int size)
+{
+       /* notify only the autonomous event by device */
+       if ((drvdata->battery_in_query == false) &&
+                        (size == BATTERY_REPORT_SIZE))
+               power_supply_changed(drvdata->battery);
+
+       return 0;
+}
+
+static int asus_battery_query(struct asus_drvdata *drvdata)
+{
+       u8 *buf;
+       int ret = 0;
+
+       buf = kmalloc(BATTERY_REPORT_SIZE, GFP_KERNEL);
+       if (!buf)
+               return -ENOMEM;
+
+       drvdata->battery_in_query = true;
+       ret = hid_hw_raw_request(drvdata->hdev, BATTERY_REPORT_ID,
+                               buf, BATTERY_REPORT_SIZE,
+                               HID_INPUT_REPORT, HID_REQ_GET_REPORT);
+       drvdata->battery_in_query = false;
+       if (ret == BATTERY_REPORT_SIZE)
+               ret = asus_parse_battery(drvdata, buf, BATTERY_REPORT_SIZE);
+       else
+               ret = -ENODATA;
+
+       kfree(buf);
+
+       return ret;
+}
+
+static enum power_supply_property asus_battery_props[] = {
+       POWER_SUPPLY_PROP_STATUS,
+       POWER_SUPPLY_PROP_PRESENT,
+       POWER_SUPPLY_PROP_CAPACITY,
+       POWER_SUPPLY_PROP_SCOPE,
+       POWER_SUPPLY_PROP_MODEL_NAME,
+};
+
+#define        QUERY_MIN_INTERVAL      (60 * HZ)       /* 60[sec] */
+
+static int asus_battery_get_property(struct power_supply *psy,
+                               enum power_supply_property psp,
+                               union power_supply_propval *val)
+{
+       struct asus_drvdata *drvdata = power_supply_get_drvdata(psy);
+       int ret = 0;
+
+       switch (psp) {
+       case POWER_SUPPLY_PROP_STATUS:
+       case POWER_SUPPLY_PROP_CAPACITY:
+               if (time_before(drvdata->battery_next_query, jiffies)) {
+                       drvdata->battery_next_query =
+                                        jiffies + QUERY_MIN_INTERVAL;
+                       ret = asus_battery_query(drvdata);
+                       if (ret)
+                               return ret;
+               }
+               if (psp == POWER_SUPPLY_PROP_STATUS)
+                       val->intval = drvdata->battery_stat;
+               else
+                       val->intval = drvdata->battery_capacity;
+               break;
+       case POWER_SUPPLY_PROP_PRESENT:
+               val->intval = 1;
+               break;
+       case POWER_SUPPLY_PROP_SCOPE:
+               val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+               break;
+       case POWER_SUPPLY_PROP_MODEL_NAME:
+               val->strval = drvdata->hdev->name;
+               break;
+       default:
+               ret = -EINVAL;
+               break;
+       }
+
+       return ret;
+}
+
+static int asus_battery_probe(struct hid_device *hdev)
+{
+       struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+       struct power_supply_config pscfg = { .drv_data = drvdata };
+       int ret = 0;
+
+       drvdata->battery_capacity = 0;
+       drvdata->battery_stat = POWER_SUPPLY_STATUS_UNKNOWN;
+       drvdata->battery_in_query = false;
+
+       drvdata->battery_desc.properties = asus_battery_props;
+       drvdata->battery_desc.num_properties = ARRAY_SIZE(asus_battery_props);
+       drvdata->battery_desc.get_property = asus_battery_get_property;
+       drvdata->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
+       drvdata->battery_desc.use_for_apm = 0;
+       drvdata->battery_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL,
+                                       "asus-keyboard-%s-battery",
+                                       strlen(hdev->uniq) ?
+                                       hdev->uniq : dev_name(&hdev->dev));
+       if (!drvdata->battery_desc.name)
+               return -ENOMEM;
+
+       drvdata->battery_next_query = jiffies;
+
+       drvdata->battery = devm_power_supply_register(&hdev->dev,
+                               &(drvdata->battery_desc), &pscfg);
+       if (IS_ERR(drvdata->battery)) {
+               ret = PTR_ERR(drvdata->battery);
+               drvdata->battery = NULL;
+               hid_err(hdev, "Unable to register battery device\n");
+               return ret;
+       }
+
+       power_supply_powers(drvdata->battery, &hdev->dev);
+
+       return ret;
+}
+
 static int asus_input_configured(struct hid_device *hdev, struct hid_input *hi)
 {
        struct input_dev *input = hi->input;
@@ -661,6 +839,10 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
 
        drvdata->quirks = id->driver_data;
 
+       /*
+        * T90CHI's keyboard dock returns same ID values as T100CHI's dock.
+        * Thus, identify T90CHI dock with product name string.
+        */
        if (strstr(hdev->name, "T90CHI")) {
                drvdata->quirks &= ~QUIRK_T100CHI;
                drvdata->quirks |= QUIRK_T90CHI;
@@ -700,6 +882,17 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
        if (drvdata->quirks & QUIRK_NO_INIT_REPORTS)
                hdev->quirks |= HID_QUIRK_NO_INIT_REPORTS;
 
+       drvdata->hdev = hdev;
+
+       if (drvdata->quirks & (QUIRK_T100CHI | QUIRK_T90CHI)) {
+               ret = asus_battery_probe(hdev);
+               if (ret) {
+                       hid_err(hdev,
+                           "Asus hid battery_probe failed: %d\n", ret);
+                       return ret;
+               }
+       }
+
        ret = hid_parse(hdev);
        if (ret) {
                hid_err(hdev, "Asus hid parse failed: %d\n", ret);