| // 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/module.h> | 
 | #include <linux/platform_data/cros_ec_commands.h> | 
 | #include <linux/platform_data/cros_ec_proto.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, | 
 | }; | 
 |  | 
 | static 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->parent); | 
 |  | 
 | 	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->parent); | 
 |  | 
 | 	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); |