/* * cros_ec_lightbar - expose the Chromebook Pixel lightbar to userspace * * Copyright (C) 2014 Google, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #define pr_fmt(fmt) "cros_ec_lightbar: " fmt #include #include #include #include #include #include #include #include #include #include #include #include #include /* 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 struct cros_ec_dev *ec_with_lightbar; 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 . 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; } 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; if (ec != ec_with_lightbar) return 0; 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; } EXPORT_SYMBOL(lb_manual_suspend_ctrl); int lb_suspend(struct cros_ec_dev *ec) { if (userspace_control || ec != ec_with_lightbar) return 0; return lb_send_empty_cmd(ec, LIGHTBAR_CMD_SUSPEND); } EXPORT_SYMBOL(lb_suspend); int lb_resume(struct cros_ec_dev *ec) { if (userspace_control || ec != ec_with_lightbar) return 0; return lb_send_empty_cmd(ec, LIGHTBAR_CMD_RESUME); } EXPORT_SYMBOL(lb_resume); 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, }; bool ec_has_lightbar(struct cros_ec_dev *ec) { return !!get_lightbar_version(ec, NULL, NULL); } static umode_t cros_ec_lightbar_attrs_are_visible(struct kobject *kobj, struct attribute *a, int n) { struct device *dev = container_of(kobj, struct device, kobj); struct cros_ec_dev *ec = to_cros_ec_dev(dev); struct platform_device *pdev = to_platform_device(ec->dev); struct cros_ec_platform *pdata = pdev->dev.platform_data; int is_cros_ec; is_cros_ec = strcmp(pdata->ec_name, CROS_EC_DEV_NAME); if (is_cros_ec != 0) return 0; /* Only instantiate this stuff if the EC has a lightbar */ if (ec_has_lightbar(ec)) { ec_with_lightbar = ec; return a->mode; } return 0; } struct attribute_group cros_ec_lightbar_attr_group = { .name = "lightbar", .attrs = __lb_cmds_attrs, .is_visible = cros_ec_lightbar_attrs_are_visible, }; EXPORT_SYMBOL(cros_ec_lightbar_attr_group);