xref: /linux/drivers/leds/leds-qnap-mcu.c (revision 07fdad3a93756b872da7b53647715c48d0f4a2d0)
1 // SPDX-License-Identifier: GPL-2.0-only
2 /*
3  * Driver for LEDs found on QNAP MCU devices
4  *
5  * Copyright (C) 2024 Heiko Stuebner <heiko@sntech.de>
6  */
7 
8 #include <linux/leds.h>
9 #include <linux/mfd/qnap-mcu.h>
10 #include <linux/module.h>
11 #include <linux/platform_device.h>
12 #include <linux/slab.h>
13 #include <uapi/linux/uleds.h>
14 
15 enum qnap_mcu_err_led_mode {
16 	QNAP_MCU_ERR_LED_ON = 0,
17 	QNAP_MCU_ERR_LED_OFF = 1,
18 	QNAP_MCU_ERR_LED_BLINK_FAST = 2,
19 	QNAP_MCU_ERR_LED_BLINK_SLOW = 3,
20 };
21 
22 struct qnap_mcu_err_led {
23 	struct qnap_mcu *mcu;
24 	struct led_classdev cdev;
25 	char name[LED_MAX_NAME_SIZE];
26 	u8 num;
27 	u8 mode;
28 };
29 
30 static inline struct qnap_mcu_err_led *
31 		cdev_to_qnap_mcu_err_led(struct led_classdev *led_cdev)
32 {
33 	return container_of(led_cdev, struct qnap_mcu_err_led, cdev);
34 }
35 
36 static int qnap_mcu_err_led_set(struct led_classdev *led_cdev,
37 				enum led_brightness brightness)
38 {
39 	struct qnap_mcu_err_led *err_led = cdev_to_qnap_mcu_err_led(led_cdev);
40 	u8 cmd[] = { '@', 'R', '0' + err_led->num, '0' };
41 
42 	/* Don't disturb a possible set blink-mode if LED stays on */
43 	if (brightness != 0 && err_led->mode >= QNAP_MCU_ERR_LED_BLINK_FAST)
44 		return 0;
45 
46 	err_led->mode = brightness ? QNAP_MCU_ERR_LED_ON : QNAP_MCU_ERR_LED_OFF;
47 	cmd[3] = '0' + err_led->mode;
48 
49 	return qnap_mcu_exec_with_ack(err_led->mcu, cmd, sizeof(cmd));
50 }
51 
52 static int qnap_mcu_err_led_blink_set(struct led_classdev *led_cdev,
53 				      unsigned long *delay_on,
54 				      unsigned long *delay_off)
55 {
56 	struct qnap_mcu_err_led *err_led = cdev_to_qnap_mcu_err_led(led_cdev);
57 	u8 cmd[] = { '@', 'R', '0' + err_led->num, '0' };
58 
59 	/* LED is off, nothing to do */
60 	if (err_led->mode == QNAP_MCU_ERR_LED_OFF)
61 		return 0;
62 
63 	if (*delay_on < 500) {
64 		*delay_on = 100;
65 		*delay_off = 100;
66 		err_led->mode = QNAP_MCU_ERR_LED_BLINK_FAST;
67 	} else {
68 		*delay_on = 500;
69 		*delay_off = 500;
70 		err_led->mode = QNAP_MCU_ERR_LED_BLINK_SLOW;
71 	}
72 
73 	cmd[3] = '0' + err_led->mode;
74 
75 	return qnap_mcu_exec_with_ack(err_led->mcu, cmd, sizeof(cmd));
76 }
77 
78 static int qnap_mcu_register_err_led(struct device *dev, struct qnap_mcu *mcu, int num_err_led)
79 {
80 	struct qnap_mcu_err_led *err_led;
81 	int ret;
82 
83 	err_led = devm_kzalloc(dev, sizeof(*err_led), GFP_KERNEL);
84 	if (!err_led)
85 		return -ENOMEM;
86 
87 	err_led->mcu = mcu;
88 	err_led->num = num_err_led;
89 	err_led->mode = QNAP_MCU_ERR_LED_OFF;
90 
91 	scnprintf(err_led->name, LED_MAX_NAME_SIZE, "hdd%d:red:status", num_err_led + 1);
92 	err_led->cdev.name = err_led->name;
93 
94 	err_led->cdev.brightness_set_blocking = qnap_mcu_err_led_set;
95 	err_led->cdev.blink_set = qnap_mcu_err_led_blink_set;
96 	err_led->cdev.brightness = 0;
97 	err_led->cdev.max_brightness = 1;
98 
99 	ret = devm_led_classdev_register(dev, &err_led->cdev);
100 	if (ret)
101 		return ret;
102 
103 	return qnap_mcu_err_led_set(&err_led->cdev, 0);
104 }
105 
106 enum qnap_mcu_usb_led_mode {
107 	QNAP_MCU_USB_LED_ON = 0,
108 	QNAP_MCU_USB_LED_OFF = 2,
109 	QNAP_MCU_USB_LED_BLINK = 1,
110 };
111 
112 struct qnap_mcu_usb_led {
113 	struct qnap_mcu *mcu;
114 	struct led_classdev cdev;
115 	u8 mode;
116 };
117 
118 static inline struct qnap_mcu_usb_led *
119 		cdev_to_qnap_mcu_usb_led(struct led_classdev *led_cdev)
120 {
121 	return container_of(led_cdev, struct qnap_mcu_usb_led, cdev);
122 }
123 
124 static int qnap_mcu_usb_led_set(struct led_classdev *led_cdev,
125 				enum led_brightness brightness)
126 {
127 	struct qnap_mcu_usb_led *usb_led = cdev_to_qnap_mcu_usb_led(led_cdev);
128 	u8 cmd[] = { '@', 'C', 0 };
129 
130 	/* Don't disturb a possible set blink-mode if LED stays on */
131 	if (brightness != 0 && usb_led->mode == QNAP_MCU_USB_LED_BLINK)
132 		return 0;
133 
134 	usb_led->mode = brightness ? QNAP_MCU_USB_LED_ON : QNAP_MCU_USB_LED_OFF;
135 
136 	/*
137 	 * Byte 3 is shared between the usb led target on/off/blink
138 	 * and also the buzzer control (in the input driver)
139 	 */
140 	cmd[2] = 'E' + usb_led->mode;
141 
142 	return qnap_mcu_exec_with_ack(usb_led->mcu, cmd, sizeof(cmd));
143 }
144 
145 static int qnap_mcu_usb_led_blink_set(struct led_classdev *led_cdev,
146 				      unsigned long *delay_on,
147 				      unsigned long *delay_off)
148 {
149 	struct qnap_mcu_usb_led *usb_led = cdev_to_qnap_mcu_usb_led(led_cdev);
150 	u8 cmd[] = { '@', 'C', 0 };
151 
152 	/* LED is off, nothing to do */
153 	if (usb_led->mode == QNAP_MCU_USB_LED_OFF)
154 		return 0;
155 
156 	*delay_on = 250;
157 	*delay_off = 250;
158 	usb_led->mode = QNAP_MCU_USB_LED_BLINK;
159 
160 	/*
161 	 * Byte 3 is shared between the USB LED target on/off/blink
162 	 * and also the buzzer control (in the input driver)
163 	 */
164 	cmd[2] = 'E' + usb_led->mode;
165 
166 	return qnap_mcu_exec_with_ack(usb_led->mcu, cmd, sizeof(cmd));
167 }
168 
169 static int qnap_mcu_register_usb_led(struct device *dev, struct qnap_mcu *mcu)
170 {
171 	struct qnap_mcu_usb_led *usb_led;
172 	int ret;
173 
174 	usb_led = devm_kzalloc(dev, sizeof(*usb_led), GFP_KERNEL);
175 	if (!usb_led)
176 		return -ENOMEM;
177 
178 	usb_led->mcu = mcu;
179 	usb_led->mode = QNAP_MCU_USB_LED_OFF;
180 	usb_led->cdev.name = "usb:blue:disk";
181 	usb_led->cdev.brightness_set_blocking = qnap_mcu_usb_led_set;
182 	usb_led->cdev.blink_set = qnap_mcu_usb_led_blink_set;
183 	usb_led->cdev.brightness = 0;
184 	usb_led->cdev.max_brightness = 1;
185 
186 	ret = devm_led_classdev_register(dev, &usb_led->cdev);
187 	if (ret)
188 		return ret;
189 
190 	return qnap_mcu_usb_led_set(&usb_led->cdev, 0);
191 }
192 
193 enum qnap_mcu_status_led_mode {
194 	QNAP_MCU_STATUS_LED_OFF = 0,
195 	QNAP_MCU_STATUS_LED_ON = 1,
196 	QNAP_MCU_STATUS_LED_BLINK_FAST = 2, /* 500ms / 500ms */
197 	QNAP_MCU_STATUS_LED_BLINK_SLOW = 3, /* 1s / 1s */
198 };
199 
200 struct qnap_mcu_status_led {
201 	struct led_classdev cdev;
202 	struct qnap_mcu_status_led *red;
203 	u8 mode;
204 };
205 
206 struct qnap_mcu_status {
207 	struct qnap_mcu *mcu;
208 	struct qnap_mcu_status_led red;
209 	struct qnap_mcu_status_led green;
210 };
211 
212 static inline struct qnap_mcu_status_led *cdev_to_qnap_mcu_status_led(struct led_classdev *led_cdev)
213 {
214 	return container_of(led_cdev, struct qnap_mcu_status_led, cdev);
215 }
216 
217 static inline struct qnap_mcu_status *statusled_to_qnap_mcu_status(struct qnap_mcu_status_led *led)
218 {
219 	return container_of(led->red, struct qnap_mcu_status, red);
220 }
221 
222 static u8 qnap_mcu_status_led_encode(struct qnap_mcu_status *status)
223 {
224 	if (status->red.mode == QNAP_MCU_STATUS_LED_OFF) {
225 		switch (status->green.mode) {
226 		case QNAP_MCU_STATUS_LED_OFF:
227 			return '9';
228 		case QNAP_MCU_STATUS_LED_ON:
229 			return '6';
230 		case QNAP_MCU_STATUS_LED_BLINK_FAST:
231 			return '5';
232 		case QNAP_MCU_STATUS_LED_BLINK_SLOW:
233 			return 'A';
234 		}
235 	} else if (status->green.mode == QNAP_MCU_STATUS_LED_OFF) {
236 		switch (status->red.mode) {
237 		case QNAP_MCU_STATUS_LED_OFF:
238 			return '9';
239 		case QNAP_MCU_STATUS_LED_ON:
240 			return '7';
241 		case QNAP_MCU_STATUS_LED_BLINK_FAST:
242 			return '4';
243 		case QNAP_MCU_STATUS_LED_BLINK_SLOW:
244 			return 'B';
245 		}
246 	} else if (status->green.mode == QNAP_MCU_STATUS_LED_ON &&
247 		   status->red.mode == QNAP_MCU_STATUS_LED_ON) {
248 		return 'D';
249 	} else if (status->green.mode == QNAP_MCU_STATUS_LED_BLINK_SLOW &&
250 		   status->red.mode == QNAP_MCU_STATUS_LED_BLINK_SLOW) {
251 		return 'C';
252 	}
253 
254 	/*
255 	 * Here both LEDs are on in some fashion, either both blinking fast,
256 	 * or in different speeds, so default to fast blinking for both.
257 	 */
258 	return '8';
259 }
260 
261 static int qnap_mcu_status_led_update(struct qnap_mcu *mcu,
262 				      struct qnap_mcu_status *status)
263 {
264 	u8 cmd[] = { '@', 'C', 0 };
265 
266 	cmd[2] = qnap_mcu_status_led_encode(status);
267 
268 	return qnap_mcu_exec_with_ack(mcu, cmd, sizeof(cmd));
269 }
270 
271 static int qnap_mcu_status_led_set(struct led_classdev *led_cdev,
272 				   enum led_brightness brightness)
273 {
274 	struct qnap_mcu_status_led *status_led = cdev_to_qnap_mcu_status_led(led_cdev);
275 	struct qnap_mcu_status *base = statusled_to_qnap_mcu_status(status_led);
276 
277 	/* Don't disturb a possible set blink-mode if LED stays on */
278 	if (brightness != 0 && status_led->mode >= QNAP_MCU_STATUS_LED_BLINK_FAST)
279 		return 0;
280 
281 	status_led->mode = brightness ? QNAP_MCU_STATUS_LED_ON :
282 					QNAP_MCU_STATUS_LED_OFF;
283 
284 	return qnap_mcu_status_led_update(base->mcu, base);
285 }
286 
287 static int qnap_mcu_status_led_blink_set(struct led_classdev *led_cdev,
288 					 unsigned long *delay_on,
289 					 unsigned long *delay_off)
290 {
291 	struct qnap_mcu_status_led *status_led = cdev_to_qnap_mcu_status_led(led_cdev);
292 	struct qnap_mcu_status *base = statusled_to_qnap_mcu_status(status_led);
293 
294 	if (status_led->mode == QNAP_MCU_STATUS_LED_OFF)
295 		return 0;
296 
297 	if (*delay_on <= 500) {
298 		*delay_on = 500;
299 		*delay_off = 500;
300 		status_led->mode = QNAP_MCU_STATUS_LED_BLINK_FAST;
301 	} else {
302 		*delay_on = 1000;
303 		*delay_off = 1000;
304 		status_led->mode = QNAP_MCU_STATUS_LED_BLINK_SLOW;
305 	}
306 
307 	return qnap_mcu_status_led_update(base->mcu, base);
308 }
309 
310 static int qnap_mcu_register_status_leds(struct device *dev, struct qnap_mcu *mcu)
311 {
312 	struct qnap_mcu_status *status;
313 	int ret;
314 
315 	status = devm_kzalloc(dev, sizeof(*status), GFP_KERNEL);
316 	if (!status)
317 		return -ENOMEM;
318 
319 	status->mcu = mcu;
320 
321 	/*
322 	 * point to the red led, so that statusled_to_qnap_mcu_status
323 	 * can resolve the main status struct containing both leds
324 	 */
325 	status->red.red = &status->red;
326 	status->green.red = &status->red;
327 
328 	status->red.mode = QNAP_MCU_STATUS_LED_OFF;
329 	status->red.cdev.name = "red:status";
330 	status->red.cdev.brightness_set_blocking = qnap_mcu_status_led_set;
331 	status->red.cdev.blink_set = qnap_mcu_status_led_blink_set;
332 	status->red.cdev.brightness = 0;
333 	status->red.cdev.max_brightness = 1;
334 
335 	status->green.mode = QNAP_MCU_STATUS_LED_OFF;
336 	status->green.cdev.name = "green:status";
337 	status->green.cdev.brightness_set_blocking = qnap_mcu_status_led_set;
338 	status->green.cdev.blink_set = qnap_mcu_status_led_blink_set;
339 	status->green.cdev.brightness = 0;
340 	status->green.cdev.max_brightness = 1;
341 
342 	ret = devm_led_classdev_register(dev, &status->red.cdev);
343 	if (ret)
344 		return ret;
345 
346 	ret = devm_led_classdev_register(dev, &status->green.cdev);
347 	if (ret)
348 		return ret;
349 
350 	return qnap_mcu_status_led_update(status->mcu, status);
351 }
352 
353 static int qnap_mcu_leds_probe(struct platform_device *pdev)
354 {
355 	struct qnap_mcu *mcu = dev_get_drvdata(pdev->dev.parent);
356 	const struct qnap_mcu_variant *variant = pdev->dev.platform_data;
357 	int ret;
358 
359 	for (int i = 0; i < variant->num_drives; i++) {
360 		ret = qnap_mcu_register_err_led(&pdev->dev, mcu, i);
361 		if (ret)
362 			return dev_err_probe(&pdev->dev, ret,
363 					"failed to register error LED %d\n", i);
364 	}
365 
366 	if (variant->usb_led) {
367 		ret = qnap_mcu_register_usb_led(&pdev->dev, mcu);
368 		if (ret)
369 			return dev_err_probe(&pdev->dev, ret,
370 					"failed to register USB LED\n");
371 	}
372 
373 	ret = qnap_mcu_register_status_leds(&pdev->dev, mcu);
374 	if (ret)
375 		return dev_err_probe(&pdev->dev, ret,
376 				     "failed to register status LEDs\n");
377 
378 	return 0;
379 }
380 
381 static struct platform_driver qnap_mcu_leds_driver = {
382 	.probe = qnap_mcu_leds_probe,
383 	.driver = {
384 		.name = "qnap-mcu-leds",
385 	},
386 };
387 module_platform_driver(qnap_mcu_leds_driver);
388 
389 MODULE_ALIAS("platform:qnap-mcu-leds");
390 MODULE_AUTHOR("Heiko Stuebner <heiko@sntech.de>");
391 MODULE_DESCRIPTION("QNAP MCU LEDs driver");
392 MODULE_LICENSE("GPL");
393