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 <[email protected]>
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 *
cdev_to_qnap_mcu_err_led(struct led_classdev * led_cdev)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
qnap_mcu_err_led_set(struct led_classdev * led_cdev,enum led_brightness brightness)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
qnap_mcu_err_led_blink_set(struct led_classdev * led_cdev,unsigned long * delay_on,unsigned long * delay_off)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
qnap_mcu_register_err_led(struct device * dev,struct qnap_mcu * mcu,int num_err_led)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 = 1,
108 QNAP_MCU_USB_LED_OFF = 3,
109 QNAP_MCU_USB_LED_BLINK = 2,
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 *
cdev_to_qnap_mcu_usb_led(struct led_classdev * led_cdev)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
qnap_mcu_usb_led_set(struct led_classdev * led_cdev,enum led_brightness brightness)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] = 'D' + usb_led->mode;
141
142 return qnap_mcu_exec_with_ack(usb_led->mcu, cmd, sizeof(cmd));
143 }
144
qnap_mcu_usb_led_blink_set(struct led_classdev * led_cdev,unsigned long * delay_on,unsigned long * delay_off)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] = 'D' + usb_led->mode;
165
166 return qnap_mcu_exec_with_ack(usb_led->mcu, cmd, sizeof(cmd));
167 }
168
qnap_mcu_register_usb_led(struct device * dev,struct qnap_mcu * mcu)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
qnap_mcu_leds_probe(struct platform_device * pdev)193 static int qnap_mcu_leds_probe(struct platform_device *pdev)
194 {
195 struct qnap_mcu *mcu = dev_get_drvdata(pdev->dev.parent);
196 const struct qnap_mcu_variant *variant = pdev->dev.platform_data;
197 int ret;
198
199 for (int i = 0; i < variant->num_drives; i++) {
200 ret = qnap_mcu_register_err_led(&pdev->dev, mcu, i);
201 if (ret)
202 return dev_err_probe(&pdev->dev, ret,
203 "failed to register error LED %d\n", i);
204 }
205
206 if (variant->usb_led) {
207 ret = qnap_mcu_register_usb_led(&pdev->dev, mcu);
208 if (ret)
209 return dev_err_probe(&pdev->dev, ret,
210 "failed to register USB LED\n");
211 }
212
213 return 0;
214 }
215
216 static struct platform_driver qnap_mcu_leds_driver = {
217 .probe = qnap_mcu_leds_probe,
218 .driver = {
219 .name = "qnap-mcu-leds",
220 },
221 };
222 module_platform_driver(qnap_mcu_leds_driver);
223
224 MODULE_ALIAS("platform:qnap-mcu-leds");
225 MODULE_AUTHOR("Heiko Stuebner <[email protected]>");
226 MODULE_DESCRIPTION("QNAP MCU LEDs driver");
227 MODULE_LICENSE("GPL");
228