| // SPDX-License-Identifier: GPL-2.0+ |
| // Expose the Chromebook Pixel lightbar to userspace |
| // |
| // Copyright (C) 2014 Google, Inc. |
| |
| #include <linux/ctype.h> |
| #include <linux/delay.h> |
| #include <linux/device.h> |
| #include <linux/fs.h> |
| #include <linux/kobject.h> |
| #include <linux/mfd/cros_ec.h> |
| #include <linux/mfd/cros_ec_commands.h> |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/sched.h> |
| #include <linux/types.h> |
| #include <linux/uaccess.h> |
| #include <linux/slab.h> |
| |
| #define DRV_NAME "cros-ec-lightbar" |
| |
| /* Rate-limit the lightbar interface to prevent DoS. */ |
| static unsigned long lb_interval_jiffies = 50 * HZ / 1000; |
| |
| /* |
| * Whether or not we have given userspace control of the lightbar. |
| * If this is true, we won't do anything during suspend/resume. |
| */ |
| static bool userspace_control; |
| |
| static ssize_t interval_msec_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| unsigned long msec = lb_interval_jiffies * 1000 / HZ; |
| |
| return scnprintf(buf, PAGE_SIZE, "%lu\n", msec); |
| } |
| |
| static ssize_t interval_msec_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| unsigned long msec; |
| |
| if (kstrtoul(buf, 0, &msec)) |
| return -EINVAL; |
| |
| lb_interval_jiffies = msec * HZ / 1000; |
| |
| return count; |
| } |
| |
| static DEFINE_MUTEX(lb_mutex); |
| /* Return 0 if able to throttle correctly, error otherwise */ |
| static int lb_throttle(void) |
| { |
| static unsigned long last_access; |
| unsigned long now, next_timeslot; |
| long delay; |
| int ret = 0; |
| |
| mutex_lock(&lb_mutex); |
| |
| now = jiffies; |
| next_timeslot = last_access + lb_interval_jiffies; |
| |
| if (time_before(now, next_timeslot)) { |
| delay = (long)(next_timeslot) - (long)now; |
| set_current_state(TASK_INTERRUPTIBLE); |
| if (schedule_timeout(delay) > 0) { |
| /* interrupted - just abort */ |
| ret = -EINTR; |
| goto out; |
| } |
| now = jiffies; |
| } |
| |
| last_access = now; |
| out: |
| mutex_unlock(&lb_mutex); |
| |
| return ret; |
| } |
| |
| static struct cros_ec_command *alloc_lightbar_cmd_msg(struct cros_ec_dev *ec) |
| { |
| struct cros_ec_command *msg; |
| int len; |
| |
| len = max(sizeof(struct ec_params_lightbar), |
| sizeof(struct ec_response_lightbar)); |
| |
| msg = kmalloc(sizeof(*msg) + len, GFP_KERNEL); |
| if (!msg) |
| return NULL; |
| |
| msg->version = 0; |
| msg->command = EC_CMD_LIGHTBAR_CMD + ec->cmd_offset; |
| msg->outsize = sizeof(struct ec_params_lightbar); |
| msg->insize = sizeof(struct ec_response_lightbar); |
| |
| return msg; |
| } |
| |
| static int get_lightbar_version(struct cros_ec_dev *ec, |
| uint32_t *ver_ptr, uint32_t *flg_ptr) |
| { |
| struct ec_params_lightbar *param; |
| struct ec_response_lightbar *resp; |
| struct cros_ec_command *msg; |
| int ret; |
| |
| msg = alloc_lightbar_cmd_msg(ec); |
| if (!msg) |
| return 0; |
| |
| param = (struct ec_params_lightbar *)msg->data; |
| param->cmd = LIGHTBAR_CMD_VERSION; |
| ret = cros_ec_cmd_xfer(ec->ec_dev, msg); |
| if (ret < 0) { |
| ret = 0; |
| goto exit; |
| } |
| |
| switch (msg->result) { |
| case EC_RES_INVALID_PARAM: |
| /* Pixel had no version command. */ |
| if (ver_ptr) |
| *ver_ptr = 0; |
| if (flg_ptr) |
| *flg_ptr = 0; |
| ret = 1; |
| goto exit; |
| |
| case EC_RES_SUCCESS: |
| resp = (struct ec_response_lightbar *)msg->data; |
| |
| /* Future devices w/lightbars should implement this command */ |
| if (ver_ptr) |
| *ver_ptr = resp->version.num; |
| if (flg_ptr) |
| *flg_ptr = resp->version.flags; |
| ret = 1; |
| goto exit; |
| } |
| |
| /* Anything else (ie, EC_RES_INVALID_COMMAND) - no lightbar */ |
| ret = 0; |
| exit: |
| kfree(msg); |
| return ret; |
| } |
| |
| static ssize_t version_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| uint32_t version = 0, flags = 0; |
| struct cros_ec_dev *ec = to_cros_ec_dev(dev); |
| int ret; |
| |
| ret = lb_throttle(); |
| if (ret) |
| return ret; |
| |
| /* This should always succeed, because we check during init. */ |
| if (!get_lightbar_version(ec, &version, &flags)) |
| return -EIO; |
| |
| return scnprintf(buf, PAGE_SIZE, "%d %d\n", version, flags); |
| } |
| |
| static ssize_t brightness_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct ec_params_lightbar *param; |
| struct cros_ec_command *msg; |
| int ret; |
| unsigned int val; |
| struct cros_ec_dev *ec = to_cros_ec_dev(dev); |
| |
| if (kstrtouint(buf, 0, &val)) |
| return -EINVAL; |
| |
| msg = alloc_lightbar_cmd_msg(ec); |
| if (!msg) |
| return -ENOMEM; |
| |
| param = (struct ec_params_lightbar *)msg->data; |
| param->cmd = LIGHTBAR_CMD_SET_BRIGHTNESS; |
| param->set_brightness.num = val; |
| ret = lb_throttle(); |
| if (ret) |
| goto exit; |
| |
| ret = cros_ec_cmd_xfer(ec->ec_dev, msg); |
| if (ret < 0) |
| goto exit; |
| |
| if (msg->result != EC_RES_SUCCESS) { |
| ret = -EINVAL; |
| goto exit; |
| } |
| |
| ret = count; |
| exit: |
| kfree(msg); |
| return ret; |
| } |
| |
| |
| /* |
| * We expect numbers, and we'll keep reading until we find them, skipping over |
| * any whitespace (sysfs guarantees that the input is null-terminated). Every |
| * four numbers are sent to the lightbar as <LED,R,G,B>. We fail at the first |
| * parsing error, if we don't parse any numbers, or if we have numbers left |
| * over. |
| */ |
| static ssize_t led_rgb_store(struct device *dev, struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct ec_params_lightbar *param; |
| struct cros_ec_command *msg; |
| struct cros_ec_dev *ec = to_cros_ec_dev(dev); |
| unsigned int val[4]; |
| int ret, i = 0, j = 0, ok = 0; |
| |
| msg = alloc_lightbar_cmd_msg(ec); |
| if (!msg) |
| return -ENOMEM; |
| |
| do { |
| /* Skip any whitespace */ |
| while (*buf && isspace(*buf)) |
| buf++; |
| |
| if (!*buf) |
| break; |
| |
| ret = sscanf(buf, "%i", &val[i++]); |
| if (ret == 0) |
| goto exit; |
| |
| if (i == 4) { |
| param = (struct ec_params_lightbar *)msg->data; |
| param->cmd = LIGHTBAR_CMD_SET_RGB; |
| param->set_rgb.led = val[0]; |
| param->set_rgb.red = val[1]; |
| param->set_rgb.green = val[2]; |
| param->set_rgb.blue = val[3]; |
| /* |
| * Throttle only the first of every four transactions, |
| * so that the user can update all four LEDs at once. |
| */ |
| if ((j++ % 4) == 0) { |
| ret = lb_throttle(); |
| if (ret) |
| goto exit; |
| } |
| |
| ret = cros_ec_cmd_xfer(ec->ec_dev, msg); |
| if (ret < 0) |
| goto exit; |
| |
| if (msg->result != EC_RES_SUCCESS) |
| goto exit; |
| |
| i = 0; |
| ok = 1; |
| } |
| |
| /* Skip over the number we just read */ |
| while (*buf && !isspace(*buf)) |
| buf++; |
| |
| } while (*buf); |
| |
| exit: |
| kfree(msg); |
| return (ok && i == 0) ? count : -EINVAL; |
| } |
| |
| static char const *seqname[] = { |
| "ERROR", "S5", "S3", "S0", "S5S3", "S3S0", |
| "S0S3", "S3S5", "STOP", "RUN", "KONAMI", |
| "TAP", "PROGRAM", |
| }; |
| |
| static ssize_t sequence_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct ec_params_lightbar *param; |
| struct ec_response_lightbar *resp; |
| struct cros_ec_command *msg; |
| int ret; |
| struct cros_ec_dev *ec = to_cros_ec_dev(dev); |
| |
| msg = alloc_lightbar_cmd_msg(ec); |
| if (!msg) |
| return -ENOMEM; |
| |
| param = (struct ec_params_lightbar *)msg->data; |
| param->cmd = LIGHTBAR_CMD_GET_SEQ; |
| ret = lb_throttle(); |
| if (ret) |
| goto exit; |
| |
| ret = cros_ec_cmd_xfer(ec->ec_dev, msg); |
| if (ret < 0) |
| goto exit; |
| |
| if (msg->result != EC_RES_SUCCESS) { |
| ret = scnprintf(buf, PAGE_SIZE, |
| "ERROR: EC returned %d\n", msg->result); |
| goto exit; |
| } |
| |
| resp = (struct ec_response_lightbar *)msg->data; |
| if (resp->get_seq.num >= ARRAY_SIZE(seqname)) |
| ret = scnprintf(buf, PAGE_SIZE, "%d\n", resp->get_seq.num); |
| else |
| ret = scnprintf(buf, PAGE_SIZE, "%s\n", |
| seqname[resp->get_seq.num]); |
| |
| exit: |
| kfree(msg); |
| return ret; |
| } |
| |
| static int lb_send_empty_cmd(struct cros_ec_dev *ec, uint8_t cmd) |
| { |
| struct ec_params_lightbar *param; |
| struct cros_ec_command *msg; |
| int ret; |
| |
| msg = alloc_lightbar_cmd_msg(ec); |
| if (!msg) |
| return -ENOMEM; |
| |
| param = (struct ec_params_lightbar *)msg->data; |
| param->cmd = cmd; |
| |
| ret = lb_throttle(); |
| if (ret) |
| goto error; |
| |
| ret = cros_ec_cmd_xfer(ec->ec_dev, msg); |
| if (ret < 0) |
| goto error; |
| if (msg->result != EC_RES_SUCCESS) { |
| ret = -EINVAL; |
| goto error; |
| } |
| ret = 0; |
| error: |
| kfree(msg); |
| |
| return ret; |
| } |
| |
| static int lb_manual_suspend_ctrl(struct cros_ec_dev *ec, uint8_t enable) |
| { |
| struct ec_params_lightbar *param; |
| struct cros_ec_command *msg; |
| int ret; |
| |
| msg = alloc_lightbar_cmd_msg(ec); |
| if (!msg) |
| return -ENOMEM; |
| |
| param = (struct ec_params_lightbar *)msg->data; |
| |
| param->cmd = LIGHTBAR_CMD_MANUAL_SUSPEND_CTRL; |
| param->manual_suspend_ctrl.enable = enable; |
| |
| ret = lb_throttle(); |
| if (ret) |
| goto error; |
| |
| ret = cros_ec_cmd_xfer(ec->ec_dev, msg); |
| if (ret < 0) |
| goto error; |
| if (msg->result != EC_RES_SUCCESS) { |
| ret = -EINVAL; |
| goto error; |
| } |
| ret = 0; |
| error: |
| kfree(msg); |
| |
| return ret; |
| } |
| |
| static ssize_t sequence_store(struct device *dev, struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct ec_params_lightbar *param; |
| struct cros_ec_command *msg; |
| unsigned int num; |
| int ret, len; |
| struct cros_ec_dev *ec = to_cros_ec_dev(dev); |
| |
| for (len = 0; len < count; len++) |
| if (!isalnum(buf[len])) |
| break; |
| |
| for (num = 0; num < ARRAY_SIZE(seqname); num++) |
| if (!strncasecmp(seqname[num], buf, len)) |
| break; |
| |
| if (num >= ARRAY_SIZE(seqname)) { |
| ret = kstrtouint(buf, 0, &num); |
| if (ret) |
| return ret; |
| } |
| |
| msg = alloc_lightbar_cmd_msg(ec); |
| if (!msg) |
| return -ENOMEM; |
| |
| param = (struct ec_params_lightbar *)msg->data; |
| param->cmd = LIGHTBAR_CMD_SEQ; |
| param->seq.num = num; |
| ret = lb_throttle(); |
| if (ret) |
| goto exit; |
| |
| ret = cros_ec_cmd_xfer(ec->ec_dev, msg); |
| if (ret < 0) |
| goto exit; |
| |
| if (msg->result != EC_RES_SUCCESS) { |
| ret = -EINVAL; |
| goto exit; |
| } |
| |
| ret = count; |
| exit: |
| kfree(msg); |
| return ret; |
| } |
| |
| static ssize_t program_store(struct device *dev, struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| int extra_bytes, max_size, ret; |
| struct ec_params_lightbar *param; |
| struct cros_ec_command *msg; |
| struct cros_ec_dev *ec = to_cros_ec_dev(dev); |
| |
| /* |
| * We might need to reject the program for size reasons. The EC |
| * enforces a maximum program size, but we also don't want to try |
| * and send a program that is too big for the protocol. In order |
| * to ensure the latter, we also need to ensure we have extra bytes |
| * to represent the rest of the packet. |
| */ |
| extra_bytes = sizeof(*param) - sizeof(param->set_program.data); |
| max_size = min(EC_LB_PROG_LEN, ec->ec_dev->max_request - extra_bytes); |
| if (count > max_size) { |
| dev_err(dev, "Program is %u bytes, too long to send (max: %u)", |
| (unsigned int)count, max_size); |
| |
| return -EINVAL; |
| } |
| |
| msg = alloc_lightbar_cmd_msg(ec); |
| if (!msg) |
| return -ENOMEM; |
| |
| ret = lb_throttle(); |
| if (ret) |
| goto exit; |
| |
| dev_info(dev, "Copying %zu byte program to EC", count); |
| |
| param = (struct ec_params_lightbar *)msg->data; |
| param->cmd = LIGHTBAR_CMD_SET_PROGRAM; |
| |
| param->set_program.size = count; |
| memcpy(param->set_program.data, buf, count); |
| |
| /* |
| * We need to set the message size manually or else it will use |
| * EC_LB_PROG_LEN. This might be too long, and the program |
| * is unlikely to use all of the space. |
| */ |
| msg->outsize = count + extra_bytes; |
| |
| ret = cros_ec_cmd_xfer(ec->ec_dev, msg); |
| if (ret < 0) |
| goto exit; |
| if (msg->result != EC_RES_SUCCESS) { |
| ret = -EINVAL; |
| goto exit; |
| } |
| |
| ret = count; |
| exit: |
| kfree(msg); |
| |
| return ret; |
| } |
| |
| static ssize_t userspace_control_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return scnprintf(buf, PAGE_SIZE, "%d\n", userspace_control); |
| } |
| |
| static ssize_t userspace_control_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, |
| size_t count) |
| { |
| bool enable; |
| int ret; |
| |
| ret = strtobool(buf, &enable); |
| if (ret < 0) |
| return ret; |
| |
| userspace_control = enable; |
| |
| return count; |
| } |
| |
| /* Module initialization */ |
| |
| static DEVICE_ATTR_RW(interval_msec); |
| static DEVICE_ATTR_RO(version); |
| static DEVICE_ATTR_WO(brightness); |
| static DEVICE_ATTR_WO(led_rgb); |
| static DEVICE_ATTR_RW(sequence); |
| static DEVICE_ATTR_WO(program); |
| static DEVICE_ATTR_RW(userspace_control); |
| |
| static struct attribute *__lb_cmds_attrs[] = { |
| &dev_attr_interval_msec.attr, |
| &dev_attr_version.attr, |
| &dev_attr_brightness.attr, |
| &dev_attr_led_rgb.attr, |
| &dev_attr_sequence.attr, |
| &dev_attr_program.attr, |
| &dev_attr_userspace_control.attr, |
| NULL, |
| }; |
| |
| struct attribute_group cros_ec_lightbar_attr_group = { |
| .name = "lightbar", |
| .attrs = __lb_cmds_attrs, |
| }; |
| |
| static int cros_ec_lightbar_probe(struct platform_device *pd) |
| { |
| struct cros_ec_dev *ec_dev = dev_get_drvdata(pd->dev.parent); |
| struct cros_ec_platform *pdata = dev_get_platdata(ec_dev->dev); |
| struct device *dev = &pd->dev; |
| int ret; |
| |
| /* |
| * Only instantiate the lightbar if the EC name is 'cros_ec'. Other EC |
| * devices like 'cros_pd' doesn't have a lightbar. |
| */ |
| if (strcmp(pdata->ec_name, CROS_EC_DEV_NAME) != 0) |
| return -ENODEV; |
| |
| /* |
| * Ask then for the lightbar version, if it's 0 then the 'cros_ec' |
| * doesn't have a lightbar. |
| */ |
| if (!get_lightbar_version(ec_dev, NULL, NULL)) |
| return -ENODEV; |
| |
| /* Take control of the lightbar from the EC. */ |
| lb_manual_suspend_ctrl(ec_dev, 1); |
| |
| ret = sysfs_create_group(&ec_dev->class_dev.kobj, |
| &cros_ec_lightbar_attr_group); |
| if (ret < 0) |
| dev_err(dev, "failed to create %s attributes. err=%d\n", |
| cros_ec_lightbar_attr_group.name, ret); |
| |
| return ret; |
| } |
| |
| static int cros_ec_lightbar_remove(struct platform_device *pd) |
| { |
| struct cros_ec_dev *ec_dev = dev_get_drvdata(pd->dev.parent); |
| |
| sysfs_remove_group(&ec_dev->class_dev.kobj, |
| &cros_ec_lightbar_attr_group); |
| |
| /* Let the EC take over the lightbar again. */ |
| lb_manual_suspend_ctrl(ec_dev, 0); |
| |
| return 0; |
| } |
| |
| static int __maybe_unused cros_ec_lightbar_resume(struct device *dev) |
| { |
| struct cros_ec_dev *ec_dev = dev_get_drvdata(dev); |
| |
| if (userspace_control) |
| return 0; |
| |
| return lb_send_empty_cmd(ec_dev, LIGHTBAR_CMD_RESUME); |
| } |
| |
| static int __maybe_unused cros_ec_lightbar_suspend(struct device *dev) |
| { |
| struct cros_ec_dev *ec_dev = dev_get_drvdata(dev); |
| |
| if (userspace_control) |
| return 0; |
| |
| return lb_send_empty_cmd(ec_dev, LIGHTBAR_CMD_SUSPEND); |
| } |
| |
| static SIMPLE_DEV_PM_OPS(cros_ec_lightbar_pm_ops, |
| cros_ec_lightbar_suspend, cros_ec_lightbar_resume); |
| |
| static struct platform_driver cros_ec_lightbar_driver = { |
| .driver = { |
| .name = DRV_NAME, |
| .pm = &cros_ec_lightbar_pm_ops, |
| }, |
| .probe = cros_ec_lightbar_probe, |
| .remove = cros_ec_lightbar_remove, |
| }; |
| |
| module_platform_driver(cros_ec_lightbar_driver); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_DESCRIPTION("Expose the Chromebook Pixel's lightbar to userspace"); |
| MODULE_ALIAS("platform:" DRV_NAME); |