| /* | |
| * leds-blinkm.c | |
| * (c) Jan-Simon Möller (dl9pf@gmx.de) | |
| * | |
| * 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, write to the Free Software | |
| * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. | |
| */ | |
| #include <linux/module.h> | |
| #include <linux/slab.h> | |
| #include <linux/jiffies.h> | |
| #include <linux/i2c.h> | |
| #include <linux/err.h> | |
| #include <linux/mutex.h> | |
| #include <linux/sysfs.h> | |
| #include <linux/printk.h> | |
| #include <linux/pm_runtime.h> | |
| #include <linux/leds.h> | |
| #include <linux/delay.h> | |
| /* Addresses to scan - BlinkM is on 0x09 by default*/ | |
| static const unsigned short normal_i2c[] = { 0x09, I2C_CLIENT_END }; | |
| static int blinkm_transfer_hw(struct i2c_client *client, int cmd); | |
| static int blinkm_test_run(struct i2c_client *client); | |
| struct blinkm_led { | |
| struct i2c_client *i2c_client; | |
| struct led_classdev led_cdev; | |
| int id; | |
| }; | |
| #define cdev_to_blmled(c) container_of(c, struct blinkm_led, led_cdev) | |
| struct blinkm_data { | |
| struct i2c_client *i2c_client; | |
| struct mutex update_lock; | |
| /* used for led class interface */ | |
| struct blinkm_led blinkm_leds[3]; | |
| /* used for "blinkm" sysfs interface */ | |
| u8 red; /* color red */ | |
| u8 green; /* color green */ | |
| u8 blue; /* color blue */ | |
| /* next values to use for transfer */ | |
| u8 next_red; /* color red */ | |
| u8 next_green; /* color green */ | |
| u8 next_blue; /* color blue */ | |
| /* internal use */ | |
| u8 args[7]; /* set of args for transmission */ | |
| u8 i2c_addr; /* i2c addr */ | |
| u8 fw_ver; /* firmware version */ | |
| /* used, but not from userspace */ | |
| u8 hue; /* HSB hue */ | |
| u8 saturation; /* HSB saturation */ | |
| u8 brightness; /* HSB brightness */ | |
| u8 next_hue; /* HSB hue */ | |
| u8 next_saturation; /* HSB saturation */ | |
| u8 next_brightness; /* HSB brightness */ | |
| /* currently unused / todo */ | |
| u8 fade_speed; /* fade speed 1 - 255 */ | |
| s8 time_adjust; /* time adjust -128 - 127 */ | |
| u8 fade:1; /* fade on = 1, off = 0 */ | |
| u8 rand:1; /* rand fade mode on = 1 */ | |
| u8 script_id; /* script ID */ | |
| u8 script_repeats; /* repeats of script */ | |
| u8 script_startline; /* line to start */ | |
| }; | |
| /* Colors */ | |
| #define RED 0 | |
| #define GREEN 1 | |
| #define BLUE 2 | |
| /* mapping command names to cmd chars - see datasheet */ | |
| #define BLM_GO_RGB 0 | |
| #define BLM_FADE_RGB 1 | |
| #define BLM_FADE_HSB 2 | |
| #define BLM_FADE_RAND_RGB 3 | |
| #define BLM_FADE_RAND_HSB 4 | |
| #define BLM_PLAY_SCRIPT 5 | |
| #define BLM_STOP_SCRIPT 6 | |
| #define BLM_SET_FADE_SPEED 7 | |
| #define BLM_SET_TIME_ADJ 8 | |
| #define BLM_GET_CUR_RGB 9 | |
| #define BLM_WRITE_SCRIPT_LINE 10 | |
| #define BLM_READ_SCRIPT_LINE 11 | |
| #define BLM_SET_SCRIPT_LR 12 /* Length & Repeats */ | |
| #define BLM_SET_ADDR 13 | |
| #define BLM_GET_ADDR 14 | |
| #define BLM_GET_FW_VER 15 | |
| #define BLM_SET_STARTUP_PARAM 16 | |
| /* BlinkM Commands | |
| * as extracted out of the datasheet: | |
| * | |
| * cmdchar = command (ascii) | |
| * cmdbyte = command in hex | |
| * nr_args = number of arguments (to send) | |
| * nr_ret = number of return values (to read) | |
| * dir = direction (0 = read, 1 = write, 2 = both) | |
| * | |
| */ | |
| static const struct { | |
| char cmdchar; | |
| u8 cmdbyte; | |
| u8 nr_args; | |
| u8 nr_ret; | |
| u8 dir:2; | |
| } blinkm_cmds[17] = { | |
| /* cmdchar, cmdbyte, nr_args, nr_ret, dir */ | |
| { 'n', 0x6e, 3, 0, 1}, | |
| { 'c', 0x63, 3, 0, 1}, | |
| { 'h', 0x68, 3, 0, 1}, | |
| { 'C', 0x43, 3, 0, 1}, | |
| { 'H', 0x48, 3, 0, 1}, | |
| { 'p', 0x70, 3, 0, 1}, | |
| { 'o', 0x6f, 0, 0, 1}, | |
| { 'f', 0x66, 1, 0, 1}, | |
| { 't', 0x74, 1, 0, 1}, | |
| { 'g', 0x67, 0, 3, 0}, | |
| { 'W', 0x57, 7, 0, 1}, | |
| { 'R', 0x52, 2, 5, 2}, | |
| { 'L', 0x4c, 3, 0, 1}, | |
| { 'A', 0x41, 4, 0, 1}, | |
| { 'a', 0x61, 0, 1, 0}, | |
| { 'Z', 0x5a, 0, 1, 0}, | |
| { 'B', 0x42, 5, 0, 1}, | |
| }; | |
| static ssize_t show_color_common(struct device *dev, char *buf, int color) | |
| { | |
| struct i2c_client *client; | |
| struct blinkm_data *data; | |
| int ret; | |
| client = to_i2c_client(dev); | |
| data = i2c_get_clientdata(client); | |
| ret = blinkm_transfer_hw(client, BLM_GET_CUR_RGB); | |
| if (ret < 0) | |
| return ret; | |
| switch (color) { | |
| case RED: | |
| return scnprintf(buf, PAGE_SIZE, "%02X\n", data->red); | |
| case GREEN: | |
| return scnprintf(buf, PAGE_SIZE, "%02X\n", data->green); | |
| case BLUE: | |
| return scnprintf(buf, PAGE_SIZE, "%02X\n", data->blue); | |
| default: | |
| return -EINVAL; | |
| } | |
| return -EINVAL; | |
| } | |
| static int store_color_common(struct device *dev, const char *buf, int color) | |
| { | |
| struct i2c_client *client; | |
| struct blinkm_data *data; | |
| int ret; | |
| u8 value; | |
| client = to_i2c_client(dev); | |
| data = i2c_get_clientdata(client); | |
| ret = kstrtou8(buf, 10, &value); | |
| if (ret < 0) { | |
| dev_err(dev, "BlinkM: value too large!\n"); | |
| return ret; | |
| } | |
| switch (color) { | |
| case RED: | |
| data->next_red = value; | |
| break; | |
| case GREEN: | |
| data->next_green = value; | |
| break; | |
| case BLUE: | |
| data->next_blue = value; | |
| break; | |
| default: | |
| return -EINVAL; | |
| } | |
| dev_dbg(dev, "next_red = %d, next_green = %d, next_blue = %d\n", | |
| data->next_red, data->next_green, data->next_blue); | |
| /* if mode ... */ | |
| ret = blinkm_transfer_hw(client, BLM_GO_RGB); | |
| if (ret < 0) { | |
| dev_err(dev, "BlinkM: can't set RGB\n"); | |
| return ret; | |
| } | |
| return 0; | |
| } | |
| static ssize_t show_red(struct device *dev, struct device_attribute *attr, | |
| char *buf) | |
| { | |
| return show_color_common(dev, buf, RED); | |
| } | |
| static ssize_t store_red(struct device *dev, struct device_attribute *attr, | |
| const char *buf, size_t count) | |
| { | |
| int ret; | |
| ret = store_color_common(dev, buf, RED); | |
| if (ret < 0) | |
| return ret; | |
| return count; | |
| } | |
| static DEVICE_ATTR(red, S_IRUGO | S_IWUSR, show_red, store_red); | |
| static ssize_t show_green(struct device *dev, struct device_attribute *attr, | |
| char *buf) | |
| { | |
| return show_color_common(dev, buf, GREEN); | |
| } | |
| static ssize_t store_green(struct device *dev, struct device_attribute *attr, | |
| const char *buf, size_t count) | |
| { | |
| int ret; | |
| ret = store_color_common(dev, buf, GREEN); | |
| if (ret < 0) | |
| return ret; | |
| return count; | |
| } | |
| static DEVICE_ATTR(green, S_IRUGO | S_IWUSR, show_green, store_green); | |
| static ssize_t show_blue(struct device *dev, struct device_attribute *attr, | |
| char *buf) | |
| { | |
| return show_color_common(dev, buf, BLUE); | |
| } | |
| static ssize_t store_blue(struct device *dev, struct device_attribute *attr, | |
| const char *buf, size_t count) | |
| { | |
| int ret; | |
| ret = store_color_common(dev, buf, BLUE); | |
| if (ret < 0) | |
| return ret; | |
| return count; | |
| } | |
| static DEVICE_ATTR(blue, S_IRUGO | S_IWUSR, show_blue, store_blue); | |
| static ssize_t show_test(struct device *dev, struct device_attribute *attr, | |
| char *buf) | |
| { | |
| return scnprintf(buf, PAGE_SIZE, | |
| "#Write into test to start test sequence!#\n"); | |
| } | |
| static ssize_t store_test(struct device *dev, struct device_attribute *attr, | |
| const char *buf, size_t count) | |
| { | |
| struct i2c_client *client; | |
| int ret; | |
| client = to_i2c_client(dev); | |
| /*test */ | |
| ret = blinkm_test_run(client); | |
| if (ret < 0) | |
| return ret; | |
| return count; | |
| } | |
| static DEVICE_ATTR(test, S_IRUGO | S_IWUSR, show_test, store_test); | |
| /* TODO: HSB, fade, timeadj, script ... */ | |
| static struct attribute *blinkm_attrs[] = { | |
| &dev_attr_red.attr, | |
| &dev_attr_green.attr, | |
| &dev_attr_blue.attr, | |
| &dev_attr_test.attr, | |
| NULL, | |
| }; | |
| static struct attribute_group blinkm_group = { | |
| .name = "blinkm", | |
| .attrs = blinkm_attrs, | |
| }; | |
| static int blinkm_write(struct i2c_client *client, int cmd, u8 *arg) | |
| { | |
| int result; | |
| int i; | |
| int arglen = blinkm_cmds[cmd].nr_args; | |
| /* write out cmd to blinkm - always / default step */ | |
| result = i2c_smbus_write_byte(client, blinkm_cmds[cmd].cmdbyte); | |
| if (result < 0) | |
| return result; | |
| /* no args to write out */ | |
| if (arglen == 0) | |
| return 0; | |
| for (i = 0; i < arglen; i++) { | |
| /* repeat for arglen */ | |
| result = i2c_smbus_write_byte(client, arg[i]); | |
| if (result < 0) | |
| return result; | |
| } | |
| return 0; | |
| } | |
| static int blinkm_read(struct i2c_client *client, int cmd, u8 *arg) | |
| { | |
| int result; | |
| int i; | |
| int retlen = blinkm_cmds[cmd].nr_ret; | |
| for (i = 0; i < retlen; i++) { | |
| /* repeat for retlen */ | |
| result = i2c_smbus_read_byte(client); | |
| if (result < 0) | |
| return result; | |
| arg[i] = result; | |
| } | |
| return 0; | |
| } | |
| static int blinkm_transfer_hw(struct i2c_client *client, int cmd) | |
| { | |
| /* the protocol is simple but non-standard: | |
| * e.g. cmd 'g' (= 0x67) for "get device address" | |
| * - which defaults to 0x09 - would be the sequence: | |
| * a) write 0x67 to the device (byte write) | |
| * b) read the value (0x09) back right after (byte read) | |
| * | |
| * Watch out for "unfinished" sequences (i.e. not enough reads | |
| * or writes after a command. It will make the blinkM misbehave. | |
| * Sequence is key here. | |
| */ | |
| /* args / return are in private data struct */ | |
| struct blinkm_data *data = i2c_get_clientdata(client); | |
| /* We start hardware transfers which are not to be | |
| * mixed with other commands. Aquire a lock now. */ | |
| if (mutex_lock_interruptible(&data->update_lock) < 0) | |
| return -EAGAIN; | |
| /* switch cmd - usually write before reads */ | |
| switch (cmd) { | |
| case BLM_FADE_RAND_RGB: | |
| case BLM_GO_RGB: | |
| case BLM_FADE_RGB: | |
| data->args[0] = data->next_red; | |
| data->args[1] = data->next_green; | |
| data->args[2] = data->next_blue; | |
| blinkm_write(client, cmd, data->args); | |
| data->red = data->args[0]; | |
| data->green = data->args[1]; | |
| data->blue = data->args[2]; | |
| break; | |
| case BLM_FADE_HSB: | |
| case BLM_FADE_RAND_HSB: | |
| data->args[0] = data->next_hue; | |
| data->args[1] = data->next_saturation; | |
| data->args[2] = data->next_brightness; | |
| blinkm_write(client, cmd, data->args); | |
| data->hue = data->next_hue; | |
| data->saturation = data->next_saturation; | |
| data->brightness = data->next_brightness; | |
| break; | |
| case BLM_PLAY_SCRIPT: | |
| data->args[0] = data->script_id; | |
| data->args[1] = data->script_repeats; | |
| data->args[2] = data->script_startline; | |
| blinkm_write(client, cmd, data->args); | |
| break; | |
| case BLM_STOP_SCRIPT: | |
| blinkm_write(client, cmd, NULL); | |
| break; | |
| case BLM_GET_CUR_RGB: | |
| data->args[0] = data->red; | |
| data->args[1] = data->green; | |
| data->args[2] = data->blue; | |
| blinkm_write(client, cmd, NULL); | |
| blinkm_read(client, cmd, data->args); | |
| data->red = data->args[0]; | |
| data->green = data->args[1]; | |
| data->blue = data->args[2]; | |
| break; | |
| case BLM_GET_ADDR: | |
| data->args[0] = data->i2c_addr; | |
| blinkm_write(client, cmd, NULL); | |
| blinkm_read(client, cmd, data->args); | |
| data->i2c_addr = data->args[0]; | |
| break; | |
| case BLM_SET_TIME_ADJ: | |
| case BLM_SET_FADE_SPEED: | |
| case BLM_READ_SCRIPT_LINE: | |
| case BLM_WRITE_SCRIPT_LINE: | |
| case BLM_SET_SCRIPT_LR: | |
| case BLM_SET_ADDR: | |
| case BLM_GET_FW_VER: | |
| case BLM_SET_STARTUP_PARAM: | |
| dev_err(&client->dev, | |
| "BlinkM: cmd %d not implemented yet.\n", cmd); | |
| break; | |
| default: | |
| dev_err(&client->dev, "BlinkM: unknown command %d\n", cmd); | |
| mutex_unlock(&data->update_lock); | |
| return -EINVAL; | |
| } /* end switch(cmd) */ | |
| /* transfers done, unlock */ | |
| mutex_unlock(&data->update_lock); | |
| return 0; | |
| } | |
| static int blinkm_led_common_set(struct led_classdev *led_cdev, | |
| enum led_brightness value, int color) | |
| { | |
| /* led_brightness is 0, 127 or 255 - we just use it here as-is */ | |
| struct blinkm_led *led = cdev_to_blmled(led_cdev); | |
| struct blinkm_data *data = i2c_get_clientdata(led->i2c_client); | |
| switch (color) { | |
| case RED: | |
| /* bail out if there's no change */ | |
| if (data->next_red == (u8) value) | |
| return 0; | |
| data->next_red = (u8) value; | |
| break; | |
| case GREEN: | |
| /* bail out if there's no change */ | |
| if (data->next_green == (u8) value) | |
| return 0; | |
| data->next_green = (u8) value; | |
| break; | |
| case BLUE: | |
| /* bail out if there's no change */ | |
| if (data->next_blue == (u8) value) | |
| return 0; | |
| data->next_blue = (u8) value; | |
| break; | |
| default: | |
| dev_err(&led->i2c_client->dev, "BlinkM: unknown color.\n"); | |
| return -EINVAL; | |
| } | |
| blinkm_transfer_hw(led->i2c_client, BLM_GO_RGB); | |
| dev_dbg(&led->i2c_client->dev, | |
| "# DONE # next_red = %d, next_green = %d," | |
| " next_blue = %d\n", | |
| data->next_red, data->next_green, | |
| data->next_blue); | |
| return 0; | |
| } | |
| static int blinkm_led_red_set(struct led_classdev *led_cdev, | |
| enum led_brightness value) | |
| { | |
| return blinkm_led_common_set(led_cdev, value, RED); | |
| } | |
| static int blinkm_led_green_set(struct led_classdev *led_cdev, | |
| enum led_brightness value) | |
| { | |
| return blinkm_led_common_set(led_cdev, value, GREEN); | |
| } | |
| static int blinkm_led_blue_set(struct led_classdev *led_cdev, | |
| enum led_brightness value) | |
| { | |
| return blinkm_led_common_set(led_cdev, value, BLUE); | |
| } | |
| static void blinkm_init_hw(struct i2c_client *client) | |
| { | |
| int ret; | |
| ret = blinkm_transfer_hw(client, BLM_STOP_SCRIPT); | |
| ret = blinkm_transfer_hw(client, BLM_GO_RGB); | |
| } | |
| static int blinkm_test_run(struct i2c_client *client) | |
| { | |
| int ret; | |
| struct blinkm_data *data = i2c_get_clientdata(client); | |
| data->next_red = 0x01; | |
| data->next_green = 0x05; | |
| data->next_blue = 0x10; | |
| ret = blinkm_transfer_hw(client, BLM_GO_RGB); | |
| if (ret < 0) | |
| return ret; | |
| msleep(2000); | |
| data->next_red = 0x25; | |
| data->next_green = 0x10; | |
| data->next_blue = 0x31; | |
| ret = blinkm_transfer_hw(client, BLM_FADE_RGB); | |
| if (ret < 0) | |
| return ret; | |
| msleep(2000); | |
| data->next_hue = 0x50; | |
| data->next_saturation = 0x10; | |
| data->next_brightness = 0x20; | |
| ret = blinkm_transfer_hw(client, BLM_FADE_HSB); | |
| if (ret < 0) | |
| return ret; | |
| msleep(2000); | |
| return 0; | |
| } | |
| /* Return 0 if detection is successful, -ENODEV otherwise */ | |
| static int blinkm_detect(struct i2c_client *client, struct i2c_board_info *info) | |
| { | |
| struct i2c_adapter *adapter = client->adapter; | |
| int ret; | |
| int count = 99; | |
| u8 tmpargs[7]; | |
| if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA | |
| | I2C_FUNC_SMBUS_WORD_DATA | |
| | I2C_FUNC_SMBUS_WRITE_BYTE)) | |
| return -ENODEV; | |
| /* Now, we do the remaining detection. Simple for now. */ | |
| /* We might need more guards to protect other i2c slaves */ | |
| /* make sure the blinkM is balanced (read/writes) */ | |
| while (count > 0) { | |
| ret = blinkm_write(client, BLM_GET_ADDR, NULL); | |
| usleep_range(5000, 10000); | |
| ret = blinkm_read(client, BLM_GET_ADDR, tmpargs); | |
| usleep_range(5000, 10000); | |
| if (tmpargs[0] == 0x09) | |
| count = 0; | |
| count--; | |
| } | |
| /* Step 1: Read BlinkM address back - cmd_char 'a' */ | |
| ret = blinkm_write(client, BLM_GET_ADDR, NULL); | |
| if (ret < 0) | |
| return ret; | |
| usleep_range(20000, 30000); /* allow a small delay */ | |
| ret = blinkm_read(client, BLM_GET_ADDR, tmpargs); | |
| if (ret < 0) | |
| return ret; | |
| if (tmpargs[0] != 0x09) { | |
| dev_err(&client->dev, "enodev DEV ADDR = 0x%02X\n", tmpargs[0]); | |
| return -ENODEV; | |
| } | |
| strlcpy(info->type, "blinkm", I2C_NAME_SIZE); | |
| return 0; | |
| } | |
| static int blinkm_probe(struct i2c_client *client, | |
| const struct i2c_device_id *id) | |
| { | |
| struct blinkm_data *data; | |
| struct blinkm_led *led[3]; | |
| int err, i; | |
| char blinkm_led_name[28]; | |
| data = devm_kzalloc(&client->dev, | |
| sizeof(struct blinkm_data), GFP_KERNEL); | |
| if (!data) { | |
| err = -ENOMEM; | |
| goto exit; | |
| } | |
| data->i2c_addr = 0x09; | |
| data->i2c_addr = 0x08; | |
| /* i2c addr - use fake addr of 0x08 initially (real is 0x09) */ | |
| data->fw_ver = 0xfe; | |
| /* firmware version - use fake until we read real value | |
| * (currently broken - BlinkM confused!) */ | |
| data->script_id = 0x01; | |
| data->i2c_client = client; | |
| i2c_set_clientdata(client, data); | |
| mutex_init(&data->update_lock); | |
| /* Register sysfs hooks */ | |
| err = sysfs_create_group(&client->dev.kobj, &blinkm_group); | |
| if (err < 0) { | |
| dev_err(&client->dev, "couldn't register sysfs group\n"); | |
| goto exit; | |
| } | |
| for (i = 0; i < 3; i++) { | |
| /* RED = 0, GREEN = 1, BLUE = 2 */ | |
| led[i] = &data->blinkm_leds[i]; | |
| led[i]->i2c_client = client; | |
| led[i]->id = i; | |
| led[i]->led_cdev.max_brightness = 255; | |
| led[i]->led_cdev.flags = LED_CORE_SUSPENDRESUME; | |
| switch (i) { | |
| case RED: | |
| snprintf(blinkm_led_name, sizeof(blinkm_led_name), | |
| "blinkm-%d-%d-red", | |
| client->adapter->nr, | |
| client->addr); | |
| led[i]->led_cdev.name = blinkm_led_name; | |
| led[i]->led_cdev.brightness_set_blocking = | |
| blinkm_led_red_set; | |
| err = led_classdev_register(&client->dev, | |
| &led[i]->led_cdev); | |
| if (err < 0) { | |
| dev_err(&client->dev, | |
| "couldn't register LED %s\n", | |
| led[i]->led_cdev.name); | |
| goto failred; | |
| } | |
| break; | |
| case GREEN: | |
| snprintf(blinkm_led_name, sizeof(blinkm_led_name), | |
| "blinkm-%d-%d-green", | |
| client->adapter->nr, | |
| client->addr); | |
| led[i]->led_cdev.name = blinkm_led_name; | |
| led[i]->led_cdev.brightness_set_blocking = | |
| blinkm_led_green_set; | |
| err = led_classdev_register(&client->dev, | |
| &led[i]->led_cdev); | |
| if (err < 0) { | |
| dev_err(&client->dev, | |
| "couldn't register LED %s\n", | |
| led[i]->led_cdev.name); | |
| goto failgreen; | |
| } | |
| break; | |
| case BLUE: | |
| snprintf(blinkm_led_name, sizeof(blinkm_led_name), | |
| "blinkm-%d-%d-blue", | |
| client->adapter->nr, | |
| client->addr); | |
| led[i]->led_cdev.name = blinkm_led_name; | |
| led[i]->led_cdev.brightness_set_blocking = | |
| blinkm_led_blue_set; | |
| err = led_classdev_register(&client->dev, | |
| &led[i]->led_cdev); | |
| if (err < 0) { | |
| dev_err(&client->dev, | |
| "couldn't register LED %s\n", | |
| led[i]->led_cdev.name); | |
| goto failblue; | |
| } | |
| break; | |
| } /* end switch */ | |
| } /* end for */ | |
| /* Initialize the blinkm */ | |
| blinkm_init_hw(client); | |
| return 0; | |
| failblue: | |
| led_classdev_unregister(&led[GREEN]->led_cdev); | |
| failgreen: | |
| led_classdev_unregister(&led[RED]->led_cdev); | |
| failred: | |
| sysfs_remove_group(&client->dev.kobj, &blinkm_group); | |
| exit: | |
| return err; | |
| } | |
| static int blinkm_remove(struct i2c_client *client) | |
| { | |
| struct blinkm_data *data = i2c_get_clientdata(client); | |
| int ret = 0; | |
| int i; | |
| /* make sure no workqueue entries are pending */ | |
| for (i = 0; i < 3; i++) | |
| led_classdev_unregister(&data->blinkm_leds[i].led_cdev); | |
| /* reset rgb */ | |
| data->next_red = 0x00; | |
| data->next_green = 0x00; | |
| data->next_blue = 0x00; | |
| ret = blinkm_transfer_hw(client, BLM_FADE_RGB); | |
| if (ret < 0) | |
| dev_err(&client->dev, "Failure in blinkm_remove ignored. Continuing.\n"); | |
| /* reset hsb */ | |
| data->next_hue = 0x00; | |
| data->next_saturation = 0x00; | |
| data->next_brightness = 0x00; | |
| ret = blinkm_transfer_hw(client, BLM_FADE_HSB); | |
| if (ret < 0) | |
| dev_err(&client->dev, "Failure in blinkm_remove ignored. Continuing.\n"); | |
| /* red fade to off */ | |
| data->next_red = 0xff; | |
| ret = blinkm_transfer_hw(client, BLM_GO_RGB); | |
| if (ret < 0) | |
| dev_err(&client->dev, "Failure in blinkm_remove ignored. Continuing.\n"); | |
| /* off */ | |
| data->next_red = 0x00; | |
| ret = blinkm_transfer_hw(client, BLM_FADE_RGB); | |
| if (ret < 0) | |
| dev_err(&client->dev, "Failure in blinkm_remove ignored. Continuing.\n"); | |
| sysfs_remove_group(&client->dev.kobj, &blinkm_group); | |
| return 0; | |
| } | |
| static const struct i2c_device_id blinkm_id[] = { | |
| {"blinkm", 0}, | |
| {} | |
| }; | |
| MODULE_DEVICE_TABLE(i2c, blinkm_id); | |
| /* This is the driver that will be inserted */ | |
| static struct i2c_driver blinkm_driver = { | |
| .class = I2C_CLASS_HWMON, | |
| .driver = { | |
| .name = "blinkm", | |
| }, | |
| .probe = blinkm_probe, | |
| .remove = blinkm_remove, | |
| .id_table = blinkm_id, | |
| .detect = blinkm_detect, | |
| .address_list = normal_i2c, | |
| }; | |
| module_i2c_driver(blinkm_driver); | |
| MODULE_AUTHOR("Jan-Simon Moeller <dl9pf@gmx.de>"); | |
| MODULE_DESCRIPTION("BlinkM RGB LED driver"); | |
| MODULE_LICENSE("GPL"); | |