Commit | Line | Data |
---|---|---|
22d8de62 JK |
1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* | |
3 | * LED state routines for driver control interface | |
4 | * Copyright (c) 2021 by Jaroslav Kysela <perex@perex.cz> | |
5 | */ | |
6 | ||
7 | #include <linux/slab.h> | |
8 | #include <linux/module.h> | |
9 | #include <linux/leds.h> | |
10 | #include <sound/core.h> | |
11 | #include <sound/control.h> | |
12 | ||
13 | MODULE_AUTHOR("Jaroslav Kysela <perex@perex.cz>"); | |
14 | MODULE_DESCRIPTION("ALSA control interface to LED trigger code."); | |
15 | MODULE_LICENSE("GPL"); | |
16 | ||
17 | #define MAX_LED (((SNDRV_CTL_ELEM_ACCESS_MIC_LED - SNDRV_CTL_ELEM_ACCESS_SPK_LED) \ | |
18 | >> SNDRV_CTL_ELEM_ACCESS_LED_SHIFT) + 1) | |
19 | ||
cb17fe00 JK |
20 | enum snd_ctl_led_mode { |
21 | MODE_FOLLOW_MUTE = 0, | |
22 | MODE_FOLLOW_ROUTE, | |
23 | MODE_OFF, | |
24 | MODE_ON, | |
25 | }; | |
26 | ||
22d8de62 | 27 | struct snd_ctl_led { |
cb17fe00 JK |
28 | struct device dev; |
29 | struct list_head controls; | |
30 | const char *name; | |
31 | unsigned int group; | |
32 | enum led_audio trigger_type; | |
33 | enum snd_ctl_led_mode mode; | |
34 | }; | |
35 | ||
36 | struct snd_ctl_led_ctl { | |
22d8de62 JK |
37 | struct list_head list; |
38 | struct snd_card *card; | |
39 | unsigned int access; | |
40 | struct snd_kcontrol *kctl; | |
41 | unsigned int index_offset; | |
42 | }; | |
43 | ||
44 | static DEFINE_MUTEX(snd_ctl_led_mutex); | |
22d8de62 | 45 | static bool snd_ctl_led_card_valid[SNDRV_CARDS]; |
cb17fe00 JK |
46 | static struct snd_ctl_led snd_ctl_leds[MAX_LED] = { |
47 | { | |
48 | .name = "speaker", | |
49 | .group = (SNDRV_CTL_ELEM_ACCESS_SPK_LED >> SNDRV_CTL_ELEM_ACCESS_LED_SHIFT) - 1, | |
50 | .trigger_type = LED_AUDIO_MUTE, | |
51 | .mode = MODE_FOLLOW_MUTE, | |
52 | }, | |
53 | { | |
54 | .name = "mic", | |
55 | .group = (SNDRV_CTL_ELEM_ACCESS_MIC_LED >> SNDRV_CTL_ELEM_ACCESS_LED_SHIFT) - 1, | |
56 | .trigger_type = LED_AUDIO_MICMUTE, | |
57 | .mode = MODE_FOLLOW_MUTE, | |
58 | }, | |
59 | }; | |
22d8de62 JK |
60 | |
61 | #define UPDATE_ROUTE(route, cb) \ | |
62 | do { \ | |
63 | int route2 = (cb); \ | |
64 | if (route2 >= 0) \ | |
65 | route = route < 0 ? route2 : (route | route2); \ | |
66 | } while (0) | |
67 | ||
68 | static inline unsigned int access_to_group(unsigned int access) | |
69 | { | |
70 | return ((access & SNDRV_CTL_ELEM_ACCESS_LED_MASK) >> | |
71 | SNDRV_CTL_ELEM_ACCESS_LED_SHIFT) - 1; | |
72 | } | |
73 | ||
74 | static inline unsigned int group_to_access(unsigned int group) | |
75 | { | |
76 | return (group + 1) << SNDRV_CTL_ELEM_ACCESS_LED_SHIFT; | |
77 | } | |
78 | ||
cb17fe00 | 79 | static struct snd_ctl_led *snd_ctl_led_get_by_access(unsigned int access) |
22d8de62 JK |
80 | { |
81 | unsigned int group = access_to_group(access); | |
82 | if (group >= MAX_LED) | |
83 | return NULL; | |
cb17fe00 | 84 | return &snd_ctl_leds[group]; |
22d8de62 JK |
85 | } |
86 | ||
cb17fe00 | 87 | static int snd_ctl_led_get(struct snd_ctl_led_ctl *lctl) |
22d8de62 JK |
88 | { |
89 | struct snd_kcontrol *kctl = lctl->kctl; | |
90 | struct snd_ctl_elem_info info; | |
91 | struct snd_ctl_elem_value value; | |
92 | unsigned int i; | |
93 | int result; | |
94 | ||
95 | memset(&info, 0, sizeof(info)); | |
96 | info.id = kctl->id; | |
97 | info.id.index += lctl->index_offset; | |
98 | info.id.numid += lctl->index_offset; | |
99 | result = kctl->info(kctl, &info); | |
100 | if (result < 0) | |
101 | return -1; | |
102 | memset(&value, 0, sizeof(value)); | |
103 | value.id = info.id; | |
104 | result = kctl->get(kctl, &value); | |
105 | if (result < 0) | |
106 | return -1; | |
107 | if (info.type == SNDRV_CTL_ELEM_TYPE_BOOLEAN || | |
108 | info.type == SNDRV_CTL_ELEM_TYPE_INTEGER) { | |
109 | for (i = 0; i < info.count; i++) | |
110 | if (value.value.integer.value[i] != info.value.integer.min) | |
111 | return 1; | |
112 | } else if (info.type == SNDRV_CTL_ELEM_TYPE_INTEGER64) { | |
113 | for (i = 0; i < info.count; i++) | |
114 | if (value.value.integer64.value[i] != info.value.integer64.min) | |
115 | return 1; | |
116 | } | |
117 | return 0; | |
118 | } | |
119 | ||
120 | static void snd_ctl_led_set_state(struct snd_card *card, unsigned int access, | |
121 | struct snd_kcontrol *kctl, unsigned int ioff) | |
122 | { | |
cb17fe00 JK |
123 | struct snd_ctl_led *led; |
124 | struct snd_ctl_led_ctl *lctl; | |
22d8de62 JK |
125 | int route; |
126 | bool found; | |
127 | ||
cb17fe00 JK |
128 | led = snd_ctl_led_get_by_access(access); |
129 | if (!led) | |
22d8de62 | 130 | return; |
22d8de62 JK |
131 | route = -1; |
132 | found = false; | |
133 | mutex_lock(&snd_ctl_led_mutex); | |
134 | /* the card may not be registered (active) at this point */ | |
135 | if (card && !snd_ctl_led_card_valid[card->number]) { | |
136 | mutex_unlock(&snd_ctl_led_mutex); | |
137 | return; | |
138 | } | |
cb17fe00 | 139 | list_for_each_entry(lctl, &led->controls, list) { |
22d8de62 JK |
140 | if (lctl->kctl == kctl && lctl->index_offset == ioff) |
141 | found = true; | |
142 | UPDATE_ROUTE(route, snd_ctl_led_get(lctl)); | |
143 | } | |
144 | if (!found && kctl && card) { | |
145 | lctl = kzalloc(sizeof(*lctl), GFP_KERNEL); | |
146 | if (lctl) { | |
147 | lctl->card = card; | |
148 | lctl->access = access; | |
149 | lctl->kctl = kctl; | |
150 | lctl->index_offset = ioff; | |
cb17fe00 | 151 | list_add(&lctl->list, &led->controls); |
22d8de62 JK |
152 | UPDATE_ROUTE(route, snd_ctl_led_get(lctl)); |
153 | } | |
154 | } | |
155 | mutex_unlock(&snd_ctl_led_mutex); | |
cb17fe00 JK |
156 | switch (led->mode) { |
157 | case MODE_OFF: route = 1; break; | |
158 | case MODE_ON: route = 0; break; | |
159 | case MODE_FOLLOW_ROUTE: if (route >= 0) route ^= 1; break; | |
160 | case MODE_FOLLOW_MUTE: /* noop */ break; | |
161 | } | |
22d8de62 | 162 | if (route >= 0) |
cb17fe00 | 163 | ledtrig_audio_set(led->trigger_type, route ? LED_OFF : LED_ON); |
22d8de62 JK |
164 | } |
165 | ||
cb17fe00 | 166 | static struct snd_ctl_led_ctl *snd_ctl_led_find(struct snd_kcontrol *kctl, unsigned int ioff) |
22d8de62 JK |
167 | { |
168 | struct list_head *controls; | |
cb17fe00 | 169 | struct snd_ctl_led_ctl *lctl; |
22d8de62 JK |
170 | unsigned int group; |
171 | ||
172 | for (group = 0; group < MAX_LED; group++) { | |
cb17fe00 | 173 | controls = &snd_ctl_leds[group].controls; |
22d8de62 JK |
174 | list_for_each_entry(lctl, controls, list) |
175 | if (lctl->kctl == kctl && lctl->index_offset == ioff) | |
176 | return lctl; | |
177 | } | |
178 | return NULL; | |
179 | } | |
180 | ||
181 | static unsigned int snd_ctl_led_remove(struct snd_kcontrol *kctl, unsigned int ioff, | |
182 | unsigned int access) | |
183 | { | |
cb17fe00 | 184 | struct snd_ctl_led_ctl *lctl; |
22d8de62 JK |
185 | unsigned int ret = 0; |
186 | ||
187 | mutex_lock(&snd_ctl_led_mutex); | |
188 | lctl = snd_ctl_led_find(kctl, ioff); | |
189 | if (lctl && (access == 0 || access != lctl->access)) { | |
190 | ret = lctl->access; | |
191 | list_del(&lctl->list); | |
192 | kfree(lctl); | |
193 | } | |
194 | mutex_unlock(&snd_ctl_led_mutex); | |
195 | return ret; | |
196 | } | |
197 | ||
198 | static void snd_ctl_led_notify(struct snd_card *card, unsigned int mask, | |
199 | struct snd_kcontrol *kctl, unsigned int ioff) | |
200 | { | |
201 | struct snd_kcontrol_volatile *vd; | |
202 | unsigned int access, access2; | |
203 | ||
204 | if (mask == SNDRV_CTL_EVENT_MASK_REMOVE) { | |
205 | access = snd_ctl_led_remove(kctl, ioff, 0); | |
206 | if (access) | |
207 | snd_ctl_led_set_state(card, access, NULL, 0); | |
208 | } else if (mask & SNDRV_CTL_EVENT_MASK_INFO) { | |
209 | vd = &kctl->vd[ioff]; | |
210 | access = vd->access & SNDRV_CTL_ELEM_ACCESS_LED_MASK; | |
211 | access2 = snd_ctl_led_remove(kctl, ioff, access); | |
212 | if (access2) | |
213 | snd_ctl_led_set_state(card, access2, NULL, 0); | |
214 | if (access) | |
215 | snd_ctl_led_set_state(card, access, kctl, ioff); | |
216 | } else if ((mask & (SNDRV_CTL_EVENT_MASK_ADD | | |
217 | SNDRV_CTL_EVENT_MASK_VALUE)) != 0) { | |
218 | vd = &kctl->vd[ioff]; | |
219 | access = vd->access & SNDRV_CTL_ELEM_ACCESS_LED_MASK; | |
220 | if (access) | |
221 | snd_ctl_led_set_state(card, access, kctl, ioff); | |
222 | } | |
223 | } | |
224 | ||
225 | static void snd_ctl_led_refresh(void) | |
226 | { | |
227 | unsigned int group; | |
228 | ||
229 | for (group = 0; group < MAX_LED; group++) | |
230 | snd_ctl_led_set_state(NULL, group_to_access(group), NULL, 0); | |
231 | } | |
232 | ||
233 | static void snd_ctl_led_clean(struct snd_card *card) | |
234 | { | |
235 | unsigned int group; | |
cb17fe00 JK |
236 | struct snd_ctl_led *led; |
237 | struct snd_ctl_led_ctl *lctl; | |
22d8de62 JK |
238 | |
239 | for (group = 0; group < MAX_LED; group++) { | |
cb17fe00 | 240 | led = &snd_ctl_leds[group]; |
22d8de62 | 241 | repeat: |
cb17fe00 | 242 | list_for_each_entry(lctl, &led->controls, list) |
22d8de62 JK |
243 | if (!card || lctl->card == card) { |
244 | list_del(&lctl->list); | |
245 | kfree(lctl); | |
246 | goto repeat; | |
247 | } | |
248 | } | |
249 | } | |
250 | ||
251 | static void snd_ctl_led_register(struct snd_card *card) | |
252 | { | |
253 | struct snd_kcontrol *kctl; | |
254 | unsigned int ioff; | |
255 | ||
256 | if (snd_BUG_ON(card->number < 0 || | |
257 | card->number >= ARRAY_SIZE(snd_ctl_led_card_valid))) | |
258 | return; | |
259 | mutex_lock(&snd_ctl_led_mutex); | |
260 | snd_ctl_led_card_valid[card->number] = true; | |
261 | mutex_unlock(&snd_ctl_led_mutex); | |
262 | /* the register callback is already called with held card->controls_rwsem */ | |
263 | list_for_each_entry(kctl, &card->controls, list) | |
264 | for (ioff = 0; ioff < kctl->count; ioff++) | |
265 | snd_ctl_led_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, kctl, ioff); | |
266 | snd_ctl_led_refresh(); | |
267 | } | |
268 | ||
269 | static void snd_ctl_led_disconnect(struct snd_card *card) | |
270 | { | |
271 | mutex_lock(&snd_ctl_led_mutex); | |
272 | snd_ctl_led_card_valid[card->number] = false; | |
273 | snd_ctl_led_clean(card); | |
274 | mutex_unlock(&snd_ctl_led_mutex); | |
275 | snd_ctl_led_refresh(); | |
276 | } | |
277 | ||
cb17fe00 JK |
278 | /* |
279 | * sysfs | |
280 | */ | |
281 | ||
282 | static ssize_t show_mode(struct device *dev, | |
283 | struct device_attribute *attr, char *buf) | |
284 | { | |
285 | struct snd_ctl_led *led = container_of(dev, struct snd_ctl_led, dev); | |
286 | const char *str; | |
287 | ||
288 | switch (led->mode) { | |
289 | case MODE_FOLLOW_MUTE: str = "follow-mute"; break; | |
290 | case MODE_FOLLOW_ROUTE: str = "follow-route"; break; | |
291 | case MODE_ON: str = "on"; break; | |
292 | case MODE_OFF: str = "off"; break; | |
293 | } | |
294 | return sprintf(buf, "%s\n", str); | |
295 | } | |
296 | ||
297 | static ssize_t store_mode(struct device *dev, struct device_attribute *attr, | |
298 | const char *buf, size_t count) | |
299 | { | |
300 | struct snd_ctl_led *led = container_of(dev, struct snd_ctl_led, dev); | |
301 | char _buf[16]; | |
302 | size_t l = min(count, sizeof(_buf) - 1) + 1; | |
303 | enum snd_ctl_led_mode mode; | |
304 | ||
305 | memcpy(_buf, buf, l); | |
306 | _buf[l] = '\0'; | |
307 | if (strstr(_buf, "mute")) | |
308 | mode = MODE_FOLLOW_MUTE; | |
309 | else if (strstr(_buf, "route")) | |
310 | mode = MODE_FOLLOW_ROUTE; | |
311 | else if (strncmp(_buf, "off", 3) == 0 || strncmp(_buf, "0", 1) == 0) | |
312 | mode = MODE_OFF; | |
313 | else if (strncmp(_buf, "on", 2) == 0 || strncmp(_buf, "1", 1) == 0) | |
314 | mode = MODE_ON; | |
315 | else | |
316 | return count; | |
317 | ||
318 | mutex_lock(&snd_ctl_led_mutex); | |
319 | led->mode = mode; | |
320 | mutex_unlock(&snd_ctl_led_mutex); | |
321 | ||
322 | snd_ctl_led_set_state(NULL, group_to_access(led->group), NULL, 0); | |
323 | return count; | |
324 | } | |
325 | ||
326 | static ssize_t show_brightness(struct device *dev, | |
327 | struct device_attribute *attr, char *buf) | |
328 | { | |
329 | struct snd_ctl_led *led = container_of(dev, struct snd_ctl_led, dev); | |
330 | ||
331 | return sprintf(buf, "%u\n", ledtrig_audio_get(led->trigger_type)); | |
332 | } | |
333 | ||
334 | static DEVICE_ATTR(mode, 0644, show_mode, store_mode); | |
335 | static DEVICE_ATTR(brightness, 0444, show_brightness, NULL); | |
336 | ||
337 | static struct attribute *snd_ctl_led_dev_attrs[] = { | |
338 | &dev_attr_mode.attr, | |
339 | &dev_attr_brightness.attr, | |
340 | NULL, | |
341 | }; | |
342 | ||
343 | static const struct attribute_group snd_ctl_led_dev_attr_group = { | |
344 | .attrs = snd_ctl_led_dev_attrs, | |
345 | }; | |
346 | ||
347 | static const struct attribute_group *snd_ctl_led_dev_attr_groups[] = { | |
348 | &snd_ctl_led_dev_attr_group, | |
349 | NULL, | |
350 | }; | |
351 | ||
352 | static struct device snd_ctl_led_dev; | |
353 | ||
22d8de62 JK |
354 | /* |
355 | * Control layer registration | |
356 | */ | |
357 | static struct snd_ctl_layer_ops snd_ctl_led_lops = { | |
358 | .module_name = SND_CTL_LAYER_MODULE_LED, | |
359 | .lregister = snd_ctl_led_register, | |
360 | .ldisconnect = snd_ctl_led_disconnect, | |
361 | .lnotify = snd_ctl_led_notify, | |
362 | }; | |
363 | ||
364 | static int __init snd_ctl_led_init(void) | |
365 | { | |
cb17fe00 | 366 | struct snd_ctl_led *led; |
22d8de62 JK |
367 | unsigned int group; |
368 | ||
cb17fe00 JK |
369 | device_initialize(&snd_ctl_led_dev); |
370 | snd_ctl_led_dev.class = sound_class; | |
371 | dev_set_name(&snd_ctl_led_dev, "ctl-led"); | |
372 | if (device_add(&snd_ctl_led_dev)) { | |
373 | put_device(&snd_ctl_led_dev); | |
374 | return -ENOMEM; | |
375 | } | |
376 | for (group = 0; group < MAX_LED; group++) { | |
377 | led = &snd_ctl_leds[group]; | |
378 | INIT_LIST_HEAD(&led->controls); | |
379 | device_initialize(&led->dev); | |
380 | led->dev.parent = &snd_ctl_led_dev; | |
381 | led->dev.groups = snd_ctl_led_dev_attr_groups; | |
382 | dev_set_name(&led->dev, led->name); | |
383 | if (device_add(&led->dev)) { | |
384 | put_device(&led->dev); | |
385 | for (; group > 0; group--) { | |
386 | led = &snd_ctl_leds[group]; | |
387 | device_del(&led->dev); | |
388 | } | |
389 | device_del(&snd_ctl_led_dev); | |
390 | return -ENOMEM; | |
391 | } | |
392 | } | |
22d8de62 JK |
393 | snd_ctl_register_layer(&snd_ctl_led_lops); |
394 | return 0; | |
395 | } | |
396 | ||
397 | static void __exit snd_ctl_led_exit(void) | |
398 | { | |
cb17fe00 JK |
399 | struct snd_ctl_led *led; |
400 | unsigned int group; | |
401 | ||
402 | for (group = 0; group < MAX_LED; group++) { | |
403 | led = &snd_ctl_leds[group]; | |
404 | device_del(&led->dev); | |
405 | } | |
406 | device_del(&snd_ctl_led_dev); | |
22d8de62 JK |
407 | snd_ctl_disconnect_layer(&snd_ctl_led_lops); |
408 | snd_ctl_led_clean(NULL); | |
409 | } | |
410 | ||
411 | module_init(snd_ctl_led_init) | |
412 | module_exit(snd_ctl_led_exit) |