HID: playstation: add DualSense battery support.
authorRoderick Colenbrander <roderick.colenbrander@sony.com>
Sun, 7 Feb 2021 21:48:58 +0000 (13:48 -0800)
committerBenjamin Tissoires <benjamin.tissoires@redhat.com>
Thu, 11 Feb 2021 15:57:06 +0000 (16:57 +0100)
Report DualSense battery status information through power_supply class.

Signed-off-by: Roderick Colenbrander <roderick.colenbrander@sony.com>
Reviewed-by: Barnabás Pőcze <pobrn@protonmail.com>
Signed-off-by: Benjamin Tissoires <benjamin.tissoires@redhat.com>
drivers/hid/Kconfig
drivers/hid/hid-playstation.c

index 3193d33..8fbd147 100644 (file)
@@ -856,6 +856,7 @@ config HID_PLANTRONICS
 config HID_PLAYSTATION
        tristate "PlayStation HID Driver"
        depends on HID
+       select POWER_SUPPLY
        help
          Provides support for Sony PS5 controllers including support for
          its special functionalities e.g. touchpad, lights and motion
index aca4963..3bb5091 100644 (file)
 /* Base class for playstation devices. */
 struct ps_device {
        struct hid_device *hdev;
+       spinlock_t lock;
+
+       struct power_supply_desc battery_desc;
+       struct power_supply *battery;
+       uint8_t battery_capacity;
+       int battery_status;
+
        uint8_t mac_address[6]; /* Note: stored in little endian order. */
 
        int (*parse_report)(struct ps_device *dev, struct hid_report *report, u8 *data, int size);
@@ -48,6 +55,11 @@ struct ps_device {
 #define DS_BUTTONS2_PS_HOME    BIT(0)
 #define DS_BUTTONS2_TOUCHPAD   BIT(1)
 
+/* Status field of DualSense input report. */
+#define DS_STATUS_BATTERY_CAPACITY     GENMASK(3, 0)
+#define DS_STATUS_CHARGING             GENMASK(7, 4)
+#define DS_STATUS_CHARGING_SHIFT       4
+
 struct dualsense {
        struct ps_device base;
        struct input_dev *gamepad;
@@ -140,6 +152,81 @@ static struct input_dev *ps_allocate_input_dev(struct hid_device *hdev, const ch
        return input_dev;
 }
 
+static enum power_supply_property ps_power_supply_props[] = {
+       POWER_SUPPLY_PROP_STATUS,
+       POWER_SUPPLY_PROP_PRESENT,
+       POWER_SUPPLY_PROP_CAPACITY,
+       POWER_SUPPLY_PROP_SCOPE,
+};
+
+static int ps_battery_get_property(struct power_supply *psy,
+               enum power_supply_property psp,
+               union power_supply_propval *val)
+{
+       struct ps_device *dev = power_supply_get_drvdata(psy);
+       uint8_t battery_capacity;
+       int battery_status;
+       unsigned long flags;
+       int ret;
+
+       spin_lock_irqsave(&dev->lock, flags);
+       battery_capacity = dev->battery_capacity;
+       battery_status = dev->battery_status;
+       spin_unlock_irqrestore(&dev->lock, flags);
+
+       switch (psp) {
+       case POWER_SUPPLY_PROP_STATUS:
+               val->intval = battery_status;
+               break;
+       case POWER_SUPPLY_PROP_PRESENT:
+               val->intval = 1;
+               break;
+       case POWER_SUPPLY_PROP_CAPACITY:
+               val->intval = battery_capacity;
+               break;
+       case POWER_SUPPLY_PROP_SCOPE:
+               val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+               break;
+       default:
+               ret = -EINVAL;
+               break;
+       }
+
+       return 0;
+}
+
+static int ps_device_register_battery(struct ps_device *dev)
+{
+       struct power_supply *battery;
+       struct power_supply_config battery_cfg = { .drv_data = dev };
+       int ret;
+
+       dev->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
+       dev->battery_desc.properties = ps_power_supply_props;
+       dev->battery_desc.num_properties = ARRAY_SIZE(ps_power_supply_props);
+       dev->battery_desc.get_property = ps_battery_get_property;
+       dev->battery_desc.name = devm_kasprintf(&dev->hdev->dev, GFP_KERNEL,
+                       "ps-controller-battery-%pMR", dev->mac_address);
+       if (!dev->battery_desc.name)
+               return -ENOMEM;
+
+       battery = devm_power_supply_register(&dev->hdev->dev, &dev->battery_desc, &battery_cfg);
+       if (IS_ERR(battery)) {
+               ret = PTR_ERR(battery);
+               hid_err(dev->hdev, "Unable to register battery device: %d\n", ret);
+               return ret;
+       }
+       dev->battery = battery;
+
+       ret = power_supply_powers(dev->battery, &dev->hdev->dev);
+       if (ret) {
+               hid_err(dev->hdev, "Unable to activate battery device: %d\n", ret);
+               return ret;
+       }
+
+       return 0;
+}
+
 static struct input_dev *ps_gamepad_create(struct hid_device *hdev)
 {
        struct input_dev *gamepad;
@@ -223,7 +310,9 @@ static int dualsense_parse_report(struct ps_device *ps_dev, struct hid_report *r
        struct hid_device *hdev = ps_dev->hdev;
        struct dualsense *ds = container_of(ps_dev, struct dualsense, base);
        struct dualsense_input_report *ds_report;
-       uint8_t value;
+       uint8_t battery_data, battery_capacity, charging_status, value;
+       int battery_status;
+       unsigned long flags;
 
        /*
         * DualSense in USB uses the full HID report for reportID 1, but
@@ -266,12 +355,49 @@ static int dualsense_parse_report(struct ps_device *ps_dev, struct hid_report *r
        input_report_key(ds->gamepad, BTN_MODE,   ds_report->buttons[2] & DS_BUTTONS2_PS_HOME);
        input_sync(ds->gamepad);
 
+       battery_data = ds_report->status & DS_STATUS_BATTERY_CAPACITY;
+       charging_status = (ds_report->status & DS_STATUS_CHARGING) >> DS_STATUS_CHARGING_SHIFT;
+
+       switch (charging_status) {
+       case 0x0:
+               /*
+                * Each unit of battery data corresponds to 10%
+                * 0 = 0-9%, 1 = 10-19%, .. and 10 = 100%
+                */
+               battery_capacity = min(battery_data * 10 + 5, 100);
+               battery_status = POWER_SUPPLY_STATUS_DISCHARGING;
+               break;
+       case 0x1:
+               battery_capacity = min(battery_data * 10 + 5, 100);
+               battery_status = POWER_SUPPLY_STATUS_CHARGING;
+               break;
+       case 0x2:
+               battery_capacity = 100;
+               battery_status = POWER_SUPPLY_STATUS_FULL;
+               break;
+       case 0xa: /* voltage or temperature out of range */
+       case 0xb: /* temperature error */
+               battery_capacity = 0;
+               battery_status = POWER_SUPPLY_STATUS_NOT_CHARGING;
+               break;
+       case 0xf: /* charging error */
+       default:
+               battery_capacity = 0;
+               battery_status = POWER_SUPPLY_STATUS_UNKNOWN;
+       }
+
+       spin_lock_irqsave(&ps_dev->lock, flags);
+       ps_dev->battery_capacity = battery_capacity;
+       ps_dev->battery_status = battery_status;
+       spin_unlock_irqrestore(&ps_dev->lock, flags);
+
        return 0;
 }
 
 static struct ps_device *dualsense_create(struct hid_device *hdev)
 {
        struct dualsense *ds;
+       struct ps_device *ps_dev;
        int ret;
 
        ds = devm_kzalloc(&hdev->dev, sizeof(*ds), GFP_KERNEL);
@@ -284,8 +410,12 @@ static struct ps_device *dualsense_create(struct hid_device *hdev)
         */
        hdev->version |= HID_PLAYSTATION_VERSION_PATCH;
 
-       ds->base.hdev = hdev;
-       ds->base.parse_report = dualsense_parse_report;
+       ps_dev = &ds->base;
+       ps_dev->hdev = hdev;
+       spin_lock_init(&ps_dev->lock);
+       ps_dev->battery_capacity = 100; /* initial value until parse_report. */
+       ps_dev->battery_status = POWER_SUPPLY_STATUS_UNKNOWN;
+       ps_dev->parse_report = dualsense_parse_report;
        hid_set_drvdata(hdev, ds);
 
        ret = dualsense_get_mac_address(ds);
@@ -301,6 +431,10 @@ static struct ps_device *dualsense_create(struct hid_device *hdev)
                goto err;
        }
 
+       ret = ps_device_register_battery(ps_dev);
+       if (ret)
+               goto err;
+
        return &ds->base;
 
 err: