162306a36Sopenharmony_ci// SPDX-License-Identifier: GPL-2.0
262306a36Sopenharmony_ci/*
362306a36Sopenharmony_ci * CZ.NIC's Turris Omnia LEDs driver
462306a36Sopenharmony_ci *
562306a36Sopenharmony_ci * 2020, 2023 by Marek Behún <kabel@kernel.org>
662306a36Sopenharmony_ci */
762306a36Sopenharmony_ci
862306a36Sopenharmony_ci#include <linux/i2c.h>
962306a36Sopenharmony_ci#include <linux/led-class-multicolor.h>
1062306a36Sopenharmony_ci#include <linux/module.h>
1162306a36Sopenharmony_ci#include <linux/mutex.h>
1262306a36Sopenharmony_ci#include <linux/of.h>
1362306a36Sopenharmony_ci#include "leds.h"
1462306a36Sopenharmony_ci
1562306a36Sopenharmony_ci#define OMNIA_BOARD_LEDS	12
1662306a36Sopenharmony_ci#define OMNIA_LED_NUM_CHANNELS	3
1762306a36Sopenharmony_ci
1862306a36Sopenharmony_ci#define CMD_LED_MODE		3
1962306a36Sopenharmony_ci#define CMD_LED_MODE_LED(l)	((l) & 0x0f)
2062306a36Sopenharmony_ci#define CMD_LED_MODE_USER	0x10
2162306a36Sopenharmony_ci
2262306a36Sopenharmony_ci#define CMD_LED_STATE		4
2362306a36Sopenharmony_ci#define CMD_LED_STATE_LED(l)	((l) & 0x0f)
2462306a36Sopenharmony_ci#define CMD_LED_STATE_ON	0x10
2562306a36Sopenharmony_ci
2662306a36Sopenharmony_ci#define CMD_LED_COLOR		5
2762306a36Sopenharmony_ci#define CMD_LED_SET_BRIGHTNESS	7
2862306a36Sopenharmony_ci#define CMD_LED_GET_BRIGHTNESS	8
2962306a36Sopenharmony_ci
3062306a36Sopenharmony_cistruct omnia_led {
3162306a36Sopenharmony_ci	struct led_classdev_mc mc_cdev;
3262306a36Sopenharmony_ci	struct mc_subled subled_info[OMNIA_LED_NUM_CHANNELS];
3362306a36Sopenharmony_ci	int reg;
3462306a36Sopenharmony_ci};
3562306a36Sopenharmony_ci
3662306a36Sopenharmony_ci#define to_omnia_led(l)		container_of(l, struct omnia_led, mc_cdev)
3762306a36Sopenharmony_ci
3862306a36Sopenharmony_cistruct omnia_leds {
3962306a36Sopenharmony_ci	struct i2c_client *client;
4062306a36Sopenharmony_ci	struct mutex lock;
4162306a36Sopenharmony_ci	struct omnia_led leds[];
4262306a36Sopenharmony_ci};
4362306a36Sopenharmony_ci
4462306a36Sopenharmony_cistatic int omnia_cmd_write_u8(const struct i2c_client *client, u8 cmd, u8 val)
4562306a36Sopenharmony_ci{
4662306a36Sopenharmony_ci	u8 buf[2] = { cmd, val };
4762306a36Sopenharmony_ci
4862306a36Sopenharmony_ci	return i2c_master_send(client, buf, sizeof(buf));
4962306a36Sopenharmony_ci}
5062306a36Sopenharmony_ci
5162306a36Sopenharmony_cistatic int omnia_cmd_read_u8(const struct i2c_client *client, u8 cmd)
5262306a36Sopenharmony_ci{
5362306a36Sopenharmony_ci	struct i2c_msg msgs[2];
5462306a36Sopenharmony_ci	u8 reply;
5562306a36Sopenharmony_ci	int ret;
5662306a36Sopenharmony_ci
5762306a36Sopenharmony_ci	msgs[0].addr = client->addr;
5862306a36Sopenharmony_ci	msgs[0].flags = 0;
5962306a36Sopenharmony_ci	msgs[0].len = 1;
6062306a36Sopenharmony_ci	msgs[0].buf = &cmd;
6162306a36Sopenharmony_ci	msgs[1].addr = client->addr;
6262306a36Sopenharmony_ci	msgs[1].flags = I2C_M_RD;
6362306a36Sopenharmony_ci	msgs[1].len = 1;
6462306a36Sopenharmony_ci	msgs[1].buf = &reply;
6562306a36Sopenharmony_ci
6662306a36Sopenharmony_ci	ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs));
6762306a36Sopenharmony_ci	if (likely(ret == ARRAY_SIZE(msgs)))
6862306a36Sopenharmony_ci		return reply;
6962306a36Sopenharmony_ci	else if (ret < 0)
7062306a36Sopenharmony_ci		return ret;
7162306a36Sopenharmony_ci	else
7262306a36Sopenharmony_ci		return -EIO;
7362306a36Sopenharmony_ci}
7462306a36Sopenharmony_ci
7562306a36Sopenharmony_cistatic int omnia_led_brightness_set_blocking(struct led_classdev *cdev,
7662306a36Sopenharmony_ci					     enum led_brightness brightness)
7762306a36Sopenharmony_ci{
7862306a36Sopenharmony_ci	struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
7962306a36Sopenharmony_ci	struct omnia_leds *leds = dev_get_drvdata(cdev->dev->parent);
8062306a36Sopenharmony_ci	struct omnia_led *led = to_omnia_led(mc_cdev);
8162306a36Sopenharmony_ci	u8 buf[5], state;
8262306a36Sopenharmony_ci	int ret;
8362306a36Sopenharmony_ci
8462306a36Sopenharmony_ci	mutex_lock(&leds->lock);
8562306a36Sopenharmony_ci
8662306a36Sopenharmony_ci	led_mc_calc_color_components(&led->mc_cdev, brightness);
8762306a36Sopenharmony_ci
8862306a36Sopenharmony_ci	buf[0] = CMD_LED_COLOR;
8962306a36Sopenharmony_ci	buf[1] = led->reg;
9062306a36Sopenharmony_ci	buf[2] = mc_cdev->subled_info[0].brightness;
9162306a36Sopenharmony_ci	buf[3] = mc_cdev->subled_info[1].brightness;
9262306a36Sopenharmony_ci	buf[4] = mc_cdev->subled_info[2].brightness;
9362306a36Sopenharmony_ci
9462306a36Sopenharmony_ci	state = CMD_LED_STATE_LED(led->reg);
9562306a36Sopenharmony_ci	if (buf[2] || buf[3] || buf[4])
9662306a36Sopenharmony_ci		state |= CMD_LED_STATE_ON;
9762306a36Sopenharmony_ci
9862306a36Sopenharmony_ci	ret = omnia_cmd_write_u8(leds->client, CMD_LED_STATE, state);
9962306a36Sopenharmony_ci	if (ret >= 0 && (state & CMD_LED_STATE_ON))
10062306a36Sopenharmony_ci		ret = i2c_master_send(leds->client, buf, 5);
10162306a36Sopenharmony_ci
10262306a36Sopenharmony_ci	mutex_unlock(&leds->lock);
10362306a36Sopenharmony_ci
10462306a36Sopenharmony_ci	return ret;
10562306a36Sopenharmony_ci}
10662306a36Sopenharmony_ci
10762306a36Sopenharmony_cistatic int omnia_led_register(struct i2c_client *client, struct omnia_led *led,
10862306a36Sopenharmony_ci			      struct device_node *np)
10962306a36Sopenharmony_ci{
11062306a36Sopenharmony_ci	struct led_init_data init_data = {};
11162306a36Sopenharmony_ci	struct device *dev = &client->dev;
11262306a36Sopenharmony_ci	struct led_classdev *cdev;
11362306a36Sopenharmony_ci	int ret, color;
11462306a36Sopenharmony_ci
11562306a36Sopenharmony_ci	ret = of_property_read_u32(np, "reg", &led->reg);
11662306a36Sopenharmony_ci	if (ret || led->reg >= OMNIA_BOARD_LEDS) {
11762306a36Sopenharmony_ci		dev_warn(dev,
11862306a36Sopenharmony_ci			 "Node %pOF: must contain 'reg' property with values between 0 and %i\n",
11962306a36Sopenharmony_ci			 np, OMNIA_BOARD_LEDS - 1);
12062306a36Sopenharmony_ci		return 0;
12162306a36Sopenharmony_ci	}
12262306a36Sopenharmony_ci
12362306a36Sopenharmony_ci	ret = of_property_read_u32(np, "color", &color);
12462306a36Sopenharmony_ci	if (ret || color != LED_COLOR_ID_RGB) {
12562306a36Sopenharmony_ci		dev_warn(dev,
12662306a36Sopenharmony_ci			 "Node %pOF: must contain 'color' property with value LED_COLOR_ID_RGB\n",
12762306a36Sopenharmony_ci			 np);
12862306a36Sopenharmony_ci		return 0;
12962306a36Sopenharmony_ci	}
13062306a36Sopenharmony_ci
13162306a36Sopenharmony_ci	led->subled_info[0].color_index = LED_COLOR_ID_RED;
13262306a36Sopenharmony_ci	led->subled_info[0].channel = 0;
13362306a36Sopenharmony_ci	led->subled_info[1].color_index = LED_COLOR_ID_GREEN;
13462306a36Sopenharmony_ci	led->subled_info[1].channel = 1;
13562306a36Sopenharmony_ci	led->subled_info[2].color_index = LED_COLOR_ID_BLUE;
13662306a36Sopenharmony_ci	led->subled_info[2].channel = 2;
13762306a36Sopenharmony_ci
13862306a36Sopenharmony_ci	led->mc_cdev.subled_info = led->subled_info;
13962306a36Sopenharmony_ci	led->mc_cdev.num_colors = OMNIA_LED_NUM_CHANNELS;
14062306a36Sopenharmony_ci
14162306a36Sopenharmony_ci	init_data.fwnode = &np->fwnode;
14262306a36Sopenharmony_ci
14362306a36Sopenharmony_ci	cdev = &led->mc_cdev.led_cdev;
14462306a36Sopenharmony_ci	cdev->max_brightness = 255;
14562306a36Sopenharmony_ci	cdev->brightness_set_blocking = omnia_led_brightness_set_blocking;
14662306a36Sopenharmony_ci
14762306a36Sopenharmony_ci	/* put the LED into software mode */
14862306a36Sopenharmony_ci	ret = omnia_cmd_write_u8(client, CMD_LED_MODE,
14962306a36Sopenharmony_ci				 CMD_LED_MODE_LED(led->reg) |
15062306a36Sopenharmony_ci				 CMD_LED_MODE_USER);
15162306a36Sopenharmony_ci	if (ret < 0) {
15262306a36Sopenharmony_ci		dev_err(dev, "Cannot set LED %pOF to software mode: %i\n", np,
15362306a36Sopenharmony_ci			ret);
15462306a36Sopenharmony_ci		return ret;
15562306a36Sopenharmony_ci	}
15662306a36Sopenharmony_ci
15762306a36Sopenharmony_ci	/* disable the LED */
15862306a36Sopenharmony_ci	ret = omnia_cmd_write_u8(client, CMD_LED_STATE,
15962306a36Sopenharmony_ci				 CMD_LED_STATE_LED(led->reg));
16062306a36Sopenharmony_ci	if (ret < 0) {
16162306a36Sopenharmony_ci		dev_err(dev, "Cannot set LED %pOF brightness: %i\n", np, ret);
16262306a36Sopenharmony_ci		return ret;
16362306a36Sopenharmony_ci	}
16462306a36Sopenharmony_ci
16562306a36Sopenharmony_ci	ret = devm_led_classdev_multicolor_register_ext(dev, &led->mc_cdev,
16662306a36Sopenharmony_ci							&init_data);
16762306a36Sopenharmony_ci	if (ret < 0) {
16862306a36Sopenharmony_ci		dev_err(dev, "Cannot register LED %pOF: %i\n", np, ret);
16962306a36Sopenharmony_ci		return ret;
17062306a36Sopenharmony_ci	}
17162306a36Sopenharmony_ci
17262306a36Sopenharmony_ci	return 1;
17362306a36Sopenharmony_ci}
17462306a36Sopenharmony_ci
17562306a36Sopenharmony_ci/*
17662306a36Sopenharmony_ci * On the front panel of the Turris Omnia router there is also a button which
17762306a36Sopenharmony_ci * can be used to control the intensity of all the LEDs at once, so that if they
17862306a36Sopenharmony_ci * are too bright, user can dim them.
17962306a36Sopenharmony_ci * The microcontroller cycles between 8 levels of this global brightness (from
18062306a36Sopenharmony_ci * 100% to 0%), but this setting can have any integer value between 0 and 100.
18162306a36Sopenharmony_ci * It is therefore convenient to be able to change this setting from software.
18262306a36Sopenharmony_ci * We expose this setting via a sysfs attribute file called "brightness". This
18362306a36Sopenharmony_ci * file lives in the device directory of the LED controller, not an individual
18462306a36Sopenharmony_ci * LED, so it should not confuse users.
18562306a36Sopenharmony_ci */
18662306a36Sopenharmony_cistatic ssize_t brightness_show(struct device *dev, struct device_attribute *a,
18762306a36Sopenharmony_ci			       char *buf)
18862306a36Sopenharmony_ci{
18962306a36Sopenharmony_ci	struct i2c_client *client = to_i2c_client(dev);
19062306a36Sopenharmony_ci	int ret;
19162306a36Sopenharmony_ci
19262306a36Sopenharmony_ci	ret = omnia_cmd_read_u8(client, CMD_LED_GET_BRIGHTNESS);
19362306a36Sopenharmony_ci
19462306a36Sopenharmony_ci	if (ret < 0)
19562306a36Sopenharmony_ci		return ret;
19662306a36Sopenharmony_ci
19762306a36Sopenharmony_ci	return sysfs_emit(buf, "%d\n", ret);
19862306a36Sopenharmony_ci}
19962306a36Sopenharmony_ci
20062306a36Sopenharmony_cistatic ssize_t brightness_store(struct device *dev, struct device_attribute *a,
20162306a36Sopenharmony_ci				const char *buf, size_t count)
20262306a36Sopenharmony_ci{
20362306a36Sopenharmony_ci	struct i2c_client *client = to_i2c_client(dev);
20462306a36Sopenharmony_ci	unsigned long brightness;
20562306a36Sopenharmony_ci	int ret;
20662306a36Sopenharmony_ci
20762306a36Sopenharmony_ci	if (kstrtoul(buf, 10, &brightness))
20862306a36Sopenharmony_ci		return -EINVAL;
20962306a36Sopenharmony_ci
21062306a36Sopenharmony_ci	if (brightness > 100)
21162306a36Sopenharmony_ci		return -EINVAL;
21262306a36Sopenharmony_ci
21362306a36Sopenharmony_ci	ret = omnia_cmd_write_u8(client, CMD_LED_SET_BRIGHTNESS, brightness);
21462306a36Sopenharmony_ci
21562306a36Sopenharmony_ci	return ret < 0 ? ret : count;
21662306a36Sopenharmony_ci}
21762306a36Sopenharmony_cistatic DEVICE_ATTR_RW(brightness);
21862306a36Sopenharmony_ci
21962306a36Sopenharmony_cistatic struct attribute *omnia_led_controller_attrs[] = {
22062306a36Sopenharmony_ci	&dev_attr_brightness.attr,
22162306a36Sopenharmony_ci	NULL,
22262306a36Sopenharmony_ci};
22362306a36Sopenharmony_ciATTRIBUTE_GROUPS(omnia_led_controller);
22462306a36Sopenharmony_ci
22562306a36Sopenharmony_cistatic int omnia_leds_probe(struct i2c_client *client)
22662306a36Sopenharmony_ci{
22762306a36Sopenharmony_ci	struct device *dev = &client->dev;
22862306a36Sopenharmony_ci	struct device_node *np = dev_of_node(dev), *child;
22962306a36Sopenharmony_ci	struct omnia_leds *leds;
23062306a36Sopenharmony_ci	struct omnia_led *led;
23162306a36Sopenharmony_ci	int ret, count;
23262306a36Sopenharmony_ci
23362306a36Sopenharmony_ci	count = of_get_available_child_count(np);
23462306a36Sopenharmony_ci	if (!count) {
23562306a36Sopenharmony_ci		dev_err(dev, "LEDs are not defined in device tree!\n");
23662306a36Sopenharmony_ci		return -ENODEV;
23762306a36Sopenharmony_ci	} else if (count > OMNIA_BOARD_LEDS) {
23862306a36Sopenharmony_ci		dev_err(dev, "Too many LEDs defined in device tree!\n");
23962306a36Sopenharmony_ci		return -EINVAL;
24062306a36Sopenharmony_ci	}
24162306a36Sopenharmony_ci
24262306a36Sopenharmony_ci	leds = devm_kzalloc(dev, struct_size(leds, leds, count), GFP_KERNEL);
24362306a36Sopenharmony_ci	if (!leds)
24462306a36Sopenharmony_ci		return -ENOMEM;
24562306a36Sopenharmony_ci
24662306a36Sopenharmony_ci	leds->client = client;
24762306a36Sopenharmony_ci	i2c_set_clientdata(client, leds);
24862306a36Sopenharmony_ci
24962306a36Sopenharmony_ci	mutex_init(&leds->lock);
25062306a36Sopenharmony_ci
25162306a36Sopenharmony_ci	led = &leds->leds[0];
25262306a36Sopenharmony_ci	for_each_available_child_of_node(np, child) {
25362306a36Sopenharmony_ci		ret = omnia_led_register(client, led, child);
25462306a36Sopenharmony_ci		if (ret < 0) {
25562306a36Sopenharmony_ci			of_node_put(child);
25662306a36Sopenharmony_ci			return ret;
25762306a36Sopenharmony_ci		}
25862306a36Sopenharmony_ci
25962306a36Sopenharmony_ci		led += ret;
26062306a36Sopenharmony_ci	}
26162306a36Sopenharmony_ci
26262306a36Sopenharmony_ci	return 0;
26362306a36Sopenharmony_ci}
26462306a36Sopenharmony_ci
26562306a36Sopenharmony_cistatic void omnia_leds_remove(struct i2c_client *client)
26662306a36Sopenharmony_ci{
26762306a36Sopenharmony_ci	u8 buf[5];
26862306a36Sopenharmony_ci
26962306a36Sopenharmony_ci	/* put all LEDs into default (HW triggered) mode */
27062306a36Sopenharmony_ci	omnia_cmd_write_u8(client, CMD_LED_MODE,
27162306a36Sopenharmony_ci			   CMD_LED_MODE_LED(OMNIA_BOARD_LEDS));
27262306a36Sopenharmony_ci
27362306a36Sopenharmony_ci	/* set all LEDs color to [255, 255, 255] */
27462306a36Sopenharmony_ci	buf[0] = CMD_LED_COLOR;
27562306a36Sopenharmony_ci	buf[1] = OMNIA_BOARD_LEDS;
27662306a36Sopenharmony_ci	buf[2] = 255;
27762306a36Sopenharmony_ci	buf[3] = 255;
27862306a36Sopenharmony_ci	buf[4] = 255;
27962306a36Sopenharmony_ci
28062306a36Sopenharmony_ci	i2c_master_send(client, buf, 5);
28162306a36Sopenharmony_ci}
28262306a36Sopenharmony_ci
28362306a36Sopenharmony_cistatic const struct of_device_id of_omnia_leds_match[] = {
28462306a36Sopenharmony_ci	{ .compatible = "cznic,turris-omnia-leds", },
28562306a36Sopenharmony_ci	{},
28662306a36Sopenharmony_ci};
28762306a36Sopenharmony_ci
28862306a36Sopenharmony_cistatic const struct i2c_device_id omnia_id[] = {
28962306a36Sopenharmony_ci	{ "omnia", 0 },
29062306a36Sopenharmony_ci	{ }
29162306a36Sopenharmony_ci};
29262306a36Sopenharmony_ciMODULE_DEVICE_TABLE(i2c, omnia_id);
29362306a36Sopenharmony_ci
29462306a36Sopenharmony_cistatic struct i2c_driver omnia_leds_driver = {
29562306a36Sopenharmony_ci	.probe		= omnia_leds_probe,
29662306a36Sopenharmony_ci	.remove		= omnia_leds_remove,
29762306a36Sopenharmony_ci	.id_table	= omnia_id,
29862306a36Sopenharmony_ci	.driver		= {
29962306a36Sopenharmony_ci		.name	= "leds-turris-omnia",
30062306a36Sopenharmony_ci		.of_match_table = of_omnia_leds_match,
30162306a36Sopenharmony_ci		.dev_groups = omnia_led_controller_groups,
30262306a36Sopenharmony_ci	},
30362306a36Sopenharmony_ci};
30462306a36Sopenharmony_ci
30562306a36Sopenharmony_cimodule_i2c_driver(omnia_leds_driver);
30662306a36Sopenharmony_ci
30762306a36Sopenharmony_ciMODULE_AUTHOR("Marek Behun <kabel@kernel.org>");
30862306a36Sopenharmony_ciMODULE_DESCRIPTION("CZ.NIC's Turris Omnia LEDs");
30962306a36Sopenharmony_ciMODULE_LICENSE("GPL v2");
310