| /* |
| * class-dual-role.c |
| * |
| * Copyright (C) 2015 Google, Inc. |
| * |
| * This software is licensed under the terms of the GNU General Public |
| * License version 2, as published by the Free Software Foundation, and |
| * may be copied, distributed, and modified under those terms. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| */ |
| |
| #include <linux/ctype.h> |
| #include <linux/device.h> |
| #include <linux/usb/class-dual-role.h> |
| #include <linux/err.h> |
| #include <linux/init.h> |
| #include <linux/module.h> |
| #include <linux/slab.h> |
| #include <linux/stat.h> |
| #include <linux/types.h> |
| |
| #define DUAL_ROLE_NOTIFICATION_TIMEOUT 2000 |
| |
| static ssize_t dual_role_store_property(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count); |
| static ssize_t dual_role_show_property(struct device *dev, |
| struct device_attribute *attr, |
| char *buf); |
| |
| #define DUAL_ROLE_ATTR(_name) \ |
| { \ |
| .attr = { .name = #_name }, \ |
| .show = dual_role_show_property, \ |
| .store = dual_role_store_property, \ |
| } |
| |
| static struct device_attribute dual_role_attrs[] = { |
| DUAL_ROLE_ATTR(supported_modes), |
| DUAL_ROLE_ATTR(mode), |
| DUAL_ROLE_ATTR(power_role), |
| DUAL_ROLE_ATTR(data_role), |
| DUAL_ROLE_ATTR(powers_vconn), |
| }; |
| |
| struct class *dual_role_class; |
| EXPORT_SYMBOL_GPL(dual_role_class); |
| |
| static struct device_type dual_role_dev_type; |
| |
| static char *kstrdupcase(const char *str, gfp_t gfp, bool to_upper) |
| { |
| char *ret, *ustr; |
| |
| ustr = ret = kmalloc(strlen(str) + 1, gfp); |
| |
| if (!ret) |
| return NULL; |
| |
| while (*str) |
| *ustr++ = to_upper ? toupper(*str++) : tolower(*str++); |
| |
| *ustr = 0; |
| |
| return ret; |
| } |
| |
| static void dual_role_changed_work(struct work_struct *work) |
| { |
| struct dual_role_phy_instance *dual_role = |
| container_of(work, struct dual_role_phy_instance, |
| changed_work); |
| |
| dev_dbg(&dual_role->dev, "%s\n", __func__); |
| kobject_uevent(&dual_role->dev.kobj, KOBJ_CHANGE); |
| } |
| |
| void dual_role_instance_changed(struct dual_role_phy_instance *dual_role) |
| { |
| dev_dbg(&dual_role->dev, "%s\n", __func__); |
| pm_wakeup_event(&dual_role->dev, DUAL_ROLE_NOTIFICATION_TIMEOUT); |
| schedule_work(&dual_role->changed_work); |
| } |
| EXPORT_SYMBOL_GPL(dual_role_instance_changed); |
| |
| int dual_role_get_property(struct dual_role_phy_instance *dual_role, |
| enum dual_role_property prop, |
| unsigned int *val) |
| { |
| return dual_role->desc->get_property(dual_role, prop, val); |
| } |
| EXPORT_SYMBOL_GPL(dual_role_get_property); |
| |
| int dual_role_set_property(struct dual_role_phy_instance *dual_role, |
| enum dual_role_property prop, |
| const unsigned int *val) |
| { |
| if (!dual_role->desc->set_property) |
| return -ENODEV; |
| |
| return dual_role->desc->set_property(dual_role, prop, val); |
| } |
| EXPORT_SYMBOL_GPL(dual_role_set_property); |
| |
| int dual_role_property_is_writeable(struct dual_role_phy_instance *dual_role, |
| enum dual_role_property prop) |
| { |
| if (!dual_role->desc->property_is_writeable) |
| return -ENODEV; |
| |
| return dual_role->desc->property_is_writeable(dual_role, prop); |
| } |
| EXPORT_SYMBOL_GPL(dual_role_property_is_writeable); |
| |
| static void dual_role_dev_release(struct device *dev) |
| { |
| struct dual_role_phy_instance *dual_role = |
| container_of(dev, struct dual_role_phy_instance, dev); |
| pr_debug("device: '%s': %s\n", dev_name(dev), __func__); |
| kfree(dual_role); |
| } |
| |
| static struct dual_role_phy_instance *__must_check |
| __dual_role_register(struct device *parent, |
| const struct dual_role_phy_desc *desc) |
| { |
| struct device *dev; |
| struct dual_role_phy_instance *dual_role; |
| int rc; |
| |
| dual_role = kzalloc(sizeof(*dual_role), GFP_KERNEL); |
| if (!dual_role) |
| return ERR_PTR(-ENOMEM); |
| |
| dev = &dual_role->dev; |
| |
| device_initialize(dev); |
| |
| dev->class = dual_role_class; |
| dev->type = &dual_role_dev_type; |
| dev->parent = parent; |
| dev->release = dual_role_dev_release; |
| dev_set_drvdata(dev, dual_role); |
| dual_role->desc = desc; |
| |
| rc = dev_set_name(dev, "%s", desc->name); |
| if (rc) |
| goto dev_set_name_failed; |
| |
| INIT_WORK(&dual_role->changed_work, dual_role_changed_work); |
| |
| rc = device_init_wakeup(dev, true); |
| if (rc) |
| goto wakeup_init_failed; |
| |
| rc = device_add(dev); |
| if (rc) |
| goto device_add_failed; |
| |
| dual_role_instance_changed(dual_role); |
| |
| return dual_role; |
| |
| device_add_failed: |
| device_init_wakeup(dev, false); |
| wakeup_init_failed: |
| dev_set_name_failed: |
| put_device(dev); |
| kfree(dual_role); |
| |
| return ERR_PTR(rc); |
| } |
| |
| static void dual_role_instance_unregister(struct dual_role_phy_instance |
| *dual_role) |
| { |
| cancel_work_sync(&dual_role->changed_work); |
| device_init_wakeup(&dual_role->dev, false); |
| device_unregister(&dual_role->dev); |
| } |
| |
| static void devm_dual_role_release(struct device *dev, void *res) |
| { |
| struct dual_role_phy_instance **dual_role = res; |
| |
| dual_role_instance_unregister(*dual_role); |
| } |
| |
| struct dual_role_phy_instance *__must_check |
| devm_dual_role_instance_register(struct device *parent, |
| const struct dual_role_phy_desc *desc) |
| { |
| struct dual_role_phy_instance **ptr, *dual_role; |
| |
| ptr = devres_alloc(devm_dual_role_release, sizeof(*ptr), GFP_KERNEL); |
| |
| if (!ptr) |
| return ERR_PTR(-ENOMEM); |
| dual_role = __dual_role_register(parent, desc); |
| if (IS_ERR(dual_role)) { |
| devres_free(ptr); |
| } else { |
| *ptr = dual_role; |
| devres_add(parent, ptr); |
| } |
| return dual_role; |
| } |
| EXPORT_SYMBOL_GPL(devm_dual_role_instance_register); |
| |
| static int devm_dual_role_match(struct device *dev, void *res, void *data) |
| { |
| struct dual_role_phy_instance **r = res; |
| |
| if (WARN_ON(!r || !*r)) |
| return 0; |
| |
| return *r == data; |
| } |
| |
| void devm_dual_role_instance_unregister(struct device *dev, |
| struct dual_role_phy_instance |
| *dual_role) |
| { |
| int rc; |
| |
| rc = devres_release(dev, devm_dual_role_release, |
| devm_dual_role_match, dual_role); |
| WARN_ON(rc); |
| } |
| EXPORT_SYMBOL_GPL(devm_dual_role_instance_unregister); |
| |
| void *dual_role_get_drvdata(struct dual_role_phy_instance *dual_role) |
| { |
| return dual_role->drv_data; |
| } |
| EXPORT_SYMBOL_GPL(dual_role_get_drvdata); |
| |
| /***************** Device attribute functions **************************/ |
| |
| /* port type */ |
| static char *supported_modes_text[] = { |
| "ufp dfp", "dfp", "ufp" |
| }; |
| |
| /* current mode */ |
| static char *mode_text[] = { |
| "ufp", "dfp", "none" |
| }; |
| |
| /* Power role */ |
| static char *pr_text[] = { |
| "source", "sink", "none" |
| }; |
| |
| /* Data role */ |
| static char *dr_text[] = { |
| "host", "device", "none" |
| }; |
| |
| /* Vconn supply */ |
| static char *vconn_supply_text[] = { |
| "n", "y" |
| }; |
| |
| static ssize_t dual_role_show_property(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| ssize_t ret = 0; |
| struct dual_role_phy_instance *dual_role = dev_get_drvdata(dev); |
| const ptrdiff_t off = attr - dual_role_attrs; |
| unsigned int value; |
| |
| if (off == DUAL_ROLE_PROP_SUPPORTED_MODES) { |
| value = dual_role->desc->supported_modes; |
| } else { |
| ret = dual_role_get_property(dual_role, off, &value); |
| |
| if (ret < 0) { |
| if (ret == -ENODATA) |
| dev_dbg(dev, |
| "driver has no data for `%s' property\n", |
| attr->attr.name); |
| else if (ret != -ENODEV) |
| dev_err(dev, |
| "driver failed to report `%s' property: %zd\n", |
| attr->attr.name, ret); |
| return ret; |
| } |
| } |
| |
| if (off == DUAL_ROLE_PROP_SUPPORTED_MODES) { |
| BUILD_BUG_ON(DUAL_ROLE_PROP_SUPPORTED_MODES_TOTAL != |
| ARRAY_SIZE(supported_modes_text)); |
| if (value < DUAL_ROLE_PROP_SUPPORTED_MODES_TOTAL) |
| return snprintf(buf, PAGE_SIZE, "%s\n", |
| supported_modes_text[value]); |
| else |
| return -EIO; |
| } else if (off == DUAL_ROLE_PROP_MODE) { |
| BUILD_BUG_ON(DUAL_ROLE_PROP_MODE_TOTAL != |
| ARRAY_SIZE(mode_text)); |
| if (value < DUAL_ROLE_PROP_MODE_TOTAL) |
| return snprintf(buf, PAGE_SIZE, "%s\n", |
| mode_text[value]); |
| else |
| return -EIO; |
| } else if (off == DUAL_ROLE_PROP_PR) { |
| BUILD_BUG_ON(DUAL_ROLE_PROP_PR_TOTAL != ARRAY_SIZE(pr_text)); |
| if (value < DUAL_ROLE_PROP_PR_TOTAL) |
| return snprintf(buf, PAGE_SIZE, "%s\n", |
| pr_text[value]); |
| else |
| return -EIO; |
| } else if (off == DUAL_ROLE_PROP_DR) { |
| BUILD_BUG_ON(DUAL_ROLE_PROP_DR_TOTAL != ARRAY_SIZE(dr_text)); |
| if (value < DUAL_ROLE_PROP_DR_TOTAL) |
| return snprintf(buf, PAGE_SIZE, "%s\n", |
| dr_text[value]); |
| else |
| return -EIO; |
| } else if (off == DUAL_ROLE_PROP_VCONN_SUPPLY) { |
| BUILD_BUG_ON(DUAL_ROLE_PROP_VCONN_SUPPLY_TOTAL != |
| ARRAY_SIZE(vconn_supply_text)); |
| if (value < DUAL_ROLE_PROP_VCONN_SUPPLY_TOTAL) |
| return snprintf(buf, PAGE_SIZE, "%s\n", |
| vconn_supply_text[value]); |
| else |
| return -EIO; |
| } else |
| return -EIO; |
| } |
| |
| static ssize_t dual_role_store_property(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| ssize_t ret; |
| struct dual_role_phy_instance *dual_role = dev_get_drvdata(dev); |
| const ptrdiff_t off = attr - dual_role_attrs; |
| unsigned int value; |
| int total, i; |
| char *dup_buf, **text_array; |
| bool result = false; |
| |
| dup_buf = kstrdupcase(buf, GFP_KERNEL, false); |
| switch (off) { |
| case DUAL_ROLE_PROP_MODE: |
| total = DUAL_ROLE_PROP_MODE_TOTAL; |
| text_array = mode_text; |
| break; |
| case DUAL_ROLE_PROP_PR: |
| total = DUAL_ROLE_PROP_PR_TOTAL; |
| text_array = pr_text; |
| break; |
| case DUAL_ROLE_PROP_DR: |
| total = DUAL_ROLE_PROP_DR_TOTAL; |
| text_array = dr_text; |
| break; |
| case DUAL_ROLE_PROP_VCONN_SUPPLY: |
| ret = strtobool(dup_buf, &result); |
| value = result; |
| if (!ret) |
| goto setprop; |
| default: |
| ret = -EINVAL; |
| goto error; |
| } |
| |
| for (i = 0; i <= total; i++) { |
| if (i == total) { |
| ret = -ENOTSUPP; |
| goto error; |
| } |
| if (!strncmp(*(text_array + i), dup_buf, |
| strlen(*(text_array + i)))) { |
| value = i; |
| break; |
| } |
| } |
| |
| setprop: |
| ret = dual_role->desc->set_property(dual_role, off, &value); |
| |
| error: |
| kfree(dup_buf); |
| |
| if (ret < 0) |
| return ret; |
| |
| return count; |
| } |
| |
| static umode_t dual_role_attr_is_visible(struct kobject *kobj, |
| struct attribute *attr, int attrno) |
| { |
| struct device *dev = container_of(kobj, struct device, kobj); |
| struct dual_role_phy_instance *dual_role = dev_get_drvdata(dev); |
| umode_t mode = S_IRUSR | S_IRGRP | S_IROTH; |
| int i; |
| |
| if (attrno == DUAL_ROLE_PROP_SUPPORTED_MODES) |
| return mode; |
| |
| for (i = 0; i < dual_role->desc->num_properties; i++) { |
| int property = dual_role->desc->properties[i]; |
| |
| if (property == attrno) { |
| if (dual_role->desc->property_is_writeable && |
| dual_role_property_is_writeable(dual_role, property) |
| > 0) |
| mode |= S_IWUSR; |
| |
| return mode; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static struct attribute *__dual_role_attrs[ARRAY_SIZE(dual_role_attrs) + 1]; |
| |
| static struct attribute_group dual_role_attr_group = { |
| .attrs = __dual_role_attrs, |
| .is_visible = dual_role_attr_is_visible, |
| }; |
| |
| static const struct attribute_group *dual_role_attr_groups[] = { |
| &dual_role_attr_group, |
| NULL, |
| }; |
| |
| void dual_role_init_attrs(struct device_type *dev_type) |
| { |
| int i; |
| |
| dev_type->groups = dual_role_attr_groups; |
| |
| for (i = 0; i < ARRAY_SIZE(dual_role_attrs); i++) |
| __dual_role_attrs[i] = &dual_role_attrs[i].attr; |
| } |
| |
| int dual_role_uevent(struct device *dev, struct kobj_uevent_env *env) |
| { |
| struct dual_role_phy_instance *dual_role = dev_get_drvdata(dev); |
| int ret = 0, j; |
| char *prop_buf; |
| char *attrname; |
| |
| dev_dbg(dev, "uevent\n"); |
| |
| if (!dual_role || !dual_role->desc) { |
| dev_dbg(dev, "No dual_role phy yet\n"); |
| return ret; |
| } |
| |
| dev_dbg(dev, "DUAL_ROLE_NAME=%s\n", dual_role->desc->name); |
| |
| ret = add_uevent_var(env, "DUAL_ROLE_NAME=%s", dual_role->desc->name); |
| if (ret) |
| return ret; |
| |
| prop_buf = (char *)get_zeroed_page(GFP_KERNEL); |
| if (!prop_buf) |
| return -ENOMEM; |
| |
| for (j = 0; j < dual_role->desc->num_properties; j++) { |
| struct device_attribute *attr; |
| char *line; |
| |
| attr = &dual_role_attrs[dual_role->desc->properties[j]]; |
| |
| ret = dual_role_show_property(dev, attr, prop_buf); |
| if (ret == -ENODEV || ret == -ENODATA) { |
| ret = 0; |
| continue; |
| } |
| |
| if (ret < 0) |
| goto out; |
| line = strnchr(prop_buf, PAGE_SIZE, '\n'); |
| if (line) |
| *line = 0; |
| |
| attrname = kstrdupcase(attr->attr.name, GFP_KERNEL, true); |
| if (!attrname) |
| ret = -ENOMEM; |
| |
| dev_dbg(dev, "prop %s=%s\n", attrname, prop_buf); |
| |
| ret = add_uevent_var(env, "DUAL_ROLE_%s=%s", attrname, |
| prop_buf); |
| kfree(attrname); |
| if (ret) |
| goto out; |
| } |
| |
| out: |
| free_page((unsigned long)prop_buf); |
| |
| return ret; |
| } |
| |
| /******************* Module Init ***********************************/ |
| |
| static int __init dual_role_class_init(void) |
| { |
| dual_role_class = class_create(THIS_MODULE, "dual_role_usb"); |
| |
| if (IS_ERR(dual_role_class)) |
| return PTR_ERR(dual_role_class); |
| |
| dual_role_class->dev_uevent = dual_role_uevent; |
| dual_role_init_attrs(&dual_role_dev_type); |
| |
| return 0; |
| } |
| |
| static void __exit dual_role_class_exit(void) |
| { |
| class_destroy(dual_role_class); |
| } |
| |
| subsys_initcall(dual_role_class_init); |
| module_exit(dual_role_class_exit); |