/* * 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);