| /* |
| * 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 <http://www.gnu.org/licenses/>. |
| */ |
| |
| #define pr_fmt(fmt) "cros_ec_lightbar: " fmt |
| |
| #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 "cros_ec_dev.h" |
| |
| /* Rate-limit the lightbar interface to prevent DoS. */ |
| static unsigned long lb_interval_jiffies = 50 * HZ / 1000; |
| |
| 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; |
| } |
| |
| #define INIT_MSG(P, R) { \ |
| .command = EC_CMD_LIGHTBAR_CMD, \ |
| .outsize = sizeof(*P), \ |
| .insize = sizeof(*R), \ |
| } |
| |
| static int get_lightbar_version(struct cros_ec_device *ec, |
| uint32_t *ver_ptr, uint32_t *flg_ptr) |
| { |
| struct ec_params_lightbar *param; |
| struct ec_response_lightbar *resp; |
| struct cros_ec_command msg = INIT_MSG(param, resp); |
| int ret; |
| |
| param = (struct ec_params_lightbar *)msg.outdata; |
| param->cmd = LIGHTBAR_CMD_VERSION; |
| ret = cros_ec_cmd_xfer(ec, &msg); |
| if (ret < 0) |
| return 0; |
| |
| 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; |
| return 1; |
| |
| case EC_RES_SUCCESS: |
| resp = (struct ec_response_lightbar *)msg.indata; |
| |
| /* Future devices w/lightbars should implement this command */ |
| if (ver_ptr) |
| *ver_ptr = resp->version.num; |
| if (flg_ptr) |
| *flg_ptr = resp->version.flags; |
| return 1; |
| } |
| |
| /* Anything else (ie, EC_RES_INVALID_COMMAND) - no lightbar */ |
| return 0; |
| } |
| |
| static ssize_t version_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| uint32_t version, flags; |
| struct cros_ec_device *ec = dev_get_drvdata(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 ec_response_lightbar *resp; |
| struct cros_ec_command msg = INIT_MSG(param, resp); |
| int ret; |
| unsigned int val; |
| struct cros_ec_device *ec = dev_get_drvdata(dev); |
| |
| if (kstrtouint(buf, 0, &val)) |
| return -EINVAL; |
| |
| param = (struct ec_params_lightbar *)msg.outdata; |
| param->cmd = LIGHTBAR_CMD_BRIGHTNESS; |
| param->brightness.num = val; |
| ret = lb_throttle(); |
| if (ret) |
| return ret; |
| |
| ret = cros_ec_cmd_xfer(ec, &msg); |
| if (ret < 0) |
| return ret; |
| |
| if (msg.result != EC_RES_SUCCESS) |
| return -EINVAL; |
| |
| return count; |
| } |
| |
| |
| /* |
| * 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 ec_response_lightbar *resp; |
| struct cros_ec_command msg = INIT_MSG(param, resp); |
| struct cros_ec_device *ec = dev_get_drvdata(dev); |
| unsigned int val[4]; |
| int ret, i = 0, j = 0, ok = 0; |
| |
| do { |
| /* Skip any whitespace */ |
| while (*buf && isspace(*buf)) |
| buf++; |
| |
| if (!*buf) |
| break; |
| |
| ret = sscanf(buf, "%i", &val[i++]); |
| if (ret == 0) |
| return -EINVAL; |
| |
| if (i == 4) { |
| param = (struct ec_params_lightbar *)msg.outdata; |
| param->cmd = LIGHTBAR_CMD_RGB; |
| param->rgb.led = val[0]; |
| param->rgb.red = val[1]; |
| param->rgb.green = val[2]; |
| param->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) |
| return ret; |
| } |
| |
| ret = cros_ec_cmd_xfer(ec, &msg); |
| if (ret < 0) |
| return ret; |
| |
| if (msg.result != EC_RES_SUCCESS) |
| return -EINVAL; |
| |
| i = 0; |
| ok = 1; |
| } |
| |
| /* Skip over the number we just read */ |
| while (*buf && !isspace(*buf)) |
| buf++; |
| |
| } while (*buf); |
| |
| return (ok && i == 0) ? count : -EINVAL; |
| } |
| |
| static const char const *seqname[] = { |
| "ERROR", "S5", "S3", "S0", "S5S3", "S3S0", |
| "S0S3", "S3S5", "STOP", "RUN", "PULSE", "TEST", "KONAMI", |
| }; |
| |
| 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 = INIT_MSG(param, resp); |
| int ret; |
| struct cros_ec_device *ec = dev_get_drvdata(dev); |
| |
| param = (struct ec_params_lightbar *)msg.outdata; |
| param->cmd = LIGHTBAR_CMD_GET_SEQ; |
| ret = lb_throttle(); |
| if (ret) |
| return ret; |
| |
| ret = cros_ec_cmd_xfer(ec, &msg); |
| if (ret < 0) |
| return ret; |
| |
| if (msg.result != EC_RES_SUCCESS) |
| return scnprintf(buf, PAGE_SIZE, |
| "ERROR: EC returned %d\n", msg.result); |
| |
| resp = (struct ec_response_lightbar *)msg.indata; |
| if (resp->get_seq.num >= ARRAY_SIZE(seqname)) |
| return scnprintf(buf, PAGE_SIZE, "%d\n", resp->get_seq.num); |
| else |
| return scnprintf(buf, PAGE_SIZE, "%s\n", |
| seqname[resp->get_seq.num]); |
| } |
| |
| static ssize_t sequence_store(struct device *dev, struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct ec_params_lightbar *param; |
| struct ec_response_lightbar *resp; |
| struct cros_ec_command msg = INIT_MSG(param, resp); |
| unsigned int num; |
| int ret, len; |
| struct cros_ec_device *ec = dev_get_drvdata(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; |
| } |
| |
| param = (struct ec_params_lightbar *)msg.outdata; |
| param->cmd = LIGHTBAR_CMD_SEQ; |
| param->seq.num = num; |
| ret = lb_throttle(); |
| if (ret) |
| return ret; |
| |
| ret = cros_ec_cmd_xfer(ec, &msg); |
| if (ret < 0) |
| return ret; |
| |
| if (msg.result != EC_RES_SUCCESS) |
| return -EINVAL; |
| |
| 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 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, |
| NULL, |
| }; |
| static struct attribute_group lb_cmds_attr_group = { |
| .name = "lightbar", |
| .attrs = __lb_cmds_attrs, |
| }; |
| |
| void ec_dev_lightbar_init(struct cros_ec_device *ec) |
| { |
| int ret = 0; |
| |
| /* Only instantiate this stuff if the EC has a lightbar */ |
| if (!get_lightbar_version(ec, NULL, NULL)) |
| return; |
| |
| ret = sysfs_create_group(&ec->vdev->kobj, &lb_cmds_attr_group); |
| if (ret) |
| pr_warn("sysfs_create_group() failed: %d\n", ret); |
| } |
| |
| void ec_dev_lightbar_remove(struct cros_ec_device *ec) |
| { |
| sysfs_remove_group(&ec->vdev->kobj, &lb_cmds_attr_group); |
| } |