2 * The Ursary Control Panel
4 * (c) 2014-2023 Martin Mares <mj@ucw.cz>
10 #include <ucw/clists.h>
11 #include <ucw/daemon.h>
13 #include <ucw/mainloop.h>
15 #include <ucw/stkstring.h>
16 #include <ucw/string.h>
31 * rotary red button green button
32 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
33 * 0 sink PCH mute switch to PCH
34 * 1 sink BT mute switch to BT
35 * 2 ceil brightness mic non-mute ceiling lights on
36 * 3 desk brightness - desk lights on
37 * 4 Albireo MPD mute MPD play/pause
38 * 5 Albireo MPV mute MPD stop
39 * 6 Albireo Zoom mute MPD prev
40 * 7 eveyrhing else mute MPD next
42 * center rainbow brightness
43 * slider light color temperature
46 #define PCH_SINK "alsa_output.pci-0000_07_00.6.analog-stereo"
47 #define BT_SINK "bluez_sink.CC_98_8B_D0_8C_06.a2dp_sink"
48 #define LOGI_SOURCE "alsa_input.usb-046d_Logitech_Webcam_C925e_EF163C5F-02.analog-stereo"
50 /*** Sink controls ***/
52 static double volume_from_pa(pa_volume_t vol)
54 return (double) vol / PA_VOLUME_NORM;
57 static pa_volume_t volume_to_pa(double vol)
59 return vol * PA_VOLUME_NORM + 0.0001;
62 static void update_ring_from_sink(int ring, const char *sink_name)
64 struct pulse_sink *s = pulse_sink_by_name(sink_name);
67 noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
68 noct_set_button(ring, 0);
74 noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
75 noct_set_button(ring, 1);
79 double vol = CLAMP(volume_from_pa(s->volume), 0, 1);
80 noct_set_ring(ring, RING_MODE_LEFT, CLAMP((int)(0x7f * vol), 12, 0x7f));
81 noct_set_button(ring, 0);
84 static void update_sink_from_rotary(int delta, const char *sink_name)
86 struct pulse_sink *s = pulse_sink_by_name(sink_name);
90 double vol = volume_from_pa(s->volume) + delta * 0.02;
91 pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, 1));
92 if (pavol == s->volume)
95 pa_cvolume_set(&cvol, s->channels, pavol);
97 DBG("## Setting volume of sink %s to %d", s->name, cvol.values[0]);
98 pulse_sink_set_volume(s->idx, &cvol);
101 static void update_sink_mute_from_button(int on, const char *sink_name)
106 struct pulse_sink *s = pulse_sink_by_name(sink_name);
110 DBG("## Setting mute of sink %s to %d", s->name, !s->mute);
111 pulse_sink_set_mute(s->idx, !s->mute);
114 /*** Client controls ***/
122 static struct client_map client_map[] = {
123 { 4, "Music Player Daemon", "albireo", },
124 { 5, "mpv", "albireo", },
125 { 6, "ZOOM VoiceEngine", "albireo", },
129 #define CLIENT_MAP_SIZE ARRAY_SIZE(client_map)
131 struct group_config {
143 static struct group_config group_config[NUM_GROUPS] = {
144 [4] = { .enabled = 1, .range = 1.5 },
145 [5] = { .enabled = 1, .range = 1.5 },
146 [6] = { .enabled = 1, .range = 1.5 },
147 [7] = { .enabled = 1, .range = 1.5 },
150 static struct group_state group_state[NUM_GROUPS];
152 static void calc_groups(void)
154 bzero(group_state, sizeof(group_state));
156 CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
158 s->noct_group_idx = -1;
160 if (s->client_idx < 0 || s->sink_idx < 0)
163 struct pulse_client *c = pulse_client_by_idx(s->client_idx);
167 for (uns i=0; i < CLIENT_MAP_SIZE; i++)
169 struct client_map *cm = &client_map[i];
170 if ((!cm->client || !strcmp(cm->client, c->name)) &&
171 (!cm->host || !strcmp(cm->host, c->host)))
174 struct group_state *gs = &group_state[g];
175 DBG("@@ Client #%d, sink input #%d -> group %d", s->client_idx, s->idx, g);
176 s->noct_group_idx = g;
177 gs->volume = MAX(gs->volume, s->volume);
178 gs->have_muted[!!s->mute] = 1;
185 static void update_groups(void)
189 for (uns i=0; i < NUM_GROUPS; i++)
191 struct group_config *gc = &group_config[i];
192 struct group_state *gs = &group_state[i];
196 DBG("@@ Group #%d: mute=%d/%d volume=%.3f/%.3f", i, gs->have_muted[0], gs->have_muted[1], volume_from_pa(gs->volume), gc->range);
198 if (!gs->have_muted[0] && !gs->have_muted[1])
200 noct_set_ring(i, RING_MODE_LEFT, 0);
201 noct_set_button(i, 0);
203 else if (!gs->have_muted[0])
205 noct_set_ring(i, RING_MODE_SINGLE_ON, 0x7f);
206 noct_set_button(i, 1);
210 double vol = CLAMP(volume_from_pa(gs->volume), 0, gc->range);
211 int val = 0x7f * vol / gc->range;
212 val = CLAMP(val, 12, 0x7f);
213 noct_set_ring(i, RING_MODE_LEFT, val);
214 noct_set_button(i, 0);
219 static void update_group_from_rotary(int i, int delta)
223 struct group_config *gc = &group_config[i];
224 struct group_state *gs = &group_state[i];
229 double vol = volume_from_pa(gs->volume) + delta*0.02;
230 pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, gc->range));
232 CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
234 if (s->noct_group_idx == i && s->volume != pavol)
236 DBG("@@ Client #%d, sink input #%d: setting volume=%u", s->client_idx, s->idx, pavol);
238 pa_cvolume_set(&cvol, s->channels, pavol);
239 pulse_sink_input_set_volume(s->idx, &cvol);
244 static void update_group_from_button(int i, int on)
250 struct group_config *gc = &group_config[i];
251 struct group_state *gs = &group_state[i];
256 if (!gs->have_muted[0] && !gs->have_muted[1])
258 uns mute = !gs->have_muted[1];
260 CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
262 if (s->noct_group_idx == i)
264 DBG("@@ Client #%d, sink input #%d: setting mute=%u", s->client_idx, s->idx, mute);
265 pulse_sink_input_set_mute(s->idx, mute);
270 static int find_touched_client(void)
274 for (uns i=0; i < NUM_GROUPS; i++)
275 if (group_config[i].enabled && noct_rotary_touched[i])
284 /*** Default sink controls ***/
286 static const char *get_client_sink(int i)
288 const char *sink = NULL;
290 CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
291 if (s->noct_group_idx == i)
293 struct pulse_sink *sk = (s->sink_idx >= 0) ? pulse_sink_by_idx(s->sink_idx) : NULL;
294 const char *ss = sk ? sk->name : NULL;
297 else if (strcmp(sink, ss))
303 static void update_default_sink(void)
305 int i = find_touched_client();
308 sink = get_client_sink(i);
310 sink = pulse_default_sink_name ? : "?";
312 if (!strcmp(sink, PCH_SINK))
314 noct_set_button(8, 1);
315 noct_set_button(9, 0);
317 else if (!strcmp(sink, BT_SINK))
319 noct_set_button(8, 0);
320 noct_set_button(9, 1);
324 noct_set_button(8, 0);
325 noct_set_button(9, 0);
329 static void update_default_sink_from_button(int button, int on)
334 int i = find_touched_client();
338 sink = get_client_sink(i);
340 sink = pulse_default_sink_name ? : "?";
343 const char *switch_to = NULL;
345 switch_to = PCH_SINK;
346 else if (button == 9)
354 struct pulse_sink *sk = pulse_sink_by_name(switch_to);
358 CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
359 if (s->noct_group_idx == i)
361 DBG("Moving input #%d to sink #%d", s->idx, sk->idx);
362 pulse_sink_input_move(s->idx, sk->idx);
367 DBG("Switching default sink to %s", switch_to);
368 pulse_server_set_default_sink(switch_to);
372 /*** Source mute controls ***/
374 static void update_source_buttons(void)
376 struct pulse_source *source = pulse_source_by_name(LOGI_SOURCE);
380 #if 0 // Disabled for now
381 // if (source->suspended || source->mute)
383 noct_set_button(2, 0);
385 noct_set_button(2, 1);
389 static void update_source_mute_from_button(int on, const char *source_name)
394 struct pulse_source *s = pulse_source_by_name(source_name);
399 DBG("## Setting mute of source %s to %d", s->name, !s->mute);
400 pulse_source_set_mute(s->idx, !s->mute);
404 /*** MPD controls ***/
406 static bool mpd_flash_state;
408 static void mpd_flash_timeout(struct main_timer *t)
410 mpd_flash_state ^= 1;
411 noct_set_button(12, mpd_flash_state);
412 timer_add_rel(t, 500);
415 static struct main_timer mpd_flash_timer = {
416 .handler = mpd_flash_timeout,
419 static void update_mpd(void)
421 const char *state = mpd_get_player_state();
422 if (!strcmp(state, "play"))
424 noct_set_button(12, 1);
425 timer_del(&mpd_flash_timer);
427 else if (!strcmp(state, "pause"))
429 if (!timer_is_active(&mpd_flash_timer))
432 mpd_flash_timeout(&mpd_flash_timer);
437 noct_set_button(12, 0);
438 timer_del(&mpd_flash_timer);
442 static void mpd_button_timeout(struct main_timer *t)
449 static void update_mpd_from_button(int button UNUSED, int on)
451 static struct main_timer mpd_button_timer = {
452 .handler = mpd_button_timeout,
455 const char *state = mpd_get_player_state();
459 if (timer_is_active(&mpd_button_timer))
461 timer_del(&mpd_button_timer);
462 if (!strcmp(state, "play"))
467 else if (!strcmp(state, "pause"))
476 if (!strcmp(state, "stop"))
483 DBG("MPD starting button timer");
484 timer_add_rel(&mpd_button_timer, 1000);
490 static bool lights_on[2];
491 static double lights_brightness[2];
492 static double lights_temperature[2];
493 static timestamp_t lights_last_update[2];
494 static int lights_ir_channel; // 2 if temperature
496 static void update_lights(void)
498 for (uint ch=0; ch < 2; ch++)
502 noct_set_ring(3-ch, RING_MODE_LEFT, lights_brightness[ch] * 127);
503 noct_set_button(11-ch, 1);
507 noct_set_ring(3-ch, RING_MODE_LEFT, 0);
508 noct_set_button(11-ch, 0);
513 static void send_lights(int ch)
515 DBG("Lights[%d]: on=%d bri=%.3f temp=%.3f", ch, lights_on[ch], lights_brightness[ch], lights_temperature[ch]);
516 double b = lights_on[ch] ? lights_brightness[ch] : 0;
517 double t = lights_on[ch] ? lights_temperature[ch] : 0;
518 char topic[100], val[100];
519 snprintf(topic, sizeof(topic), "burrow/lights/catarium/%s", (ch ? "top" : "bottom"));
520 snprintf(val, sizeof(val), "%.3f %.3f", b, t);
521 mqtt_publish(topic, val);
522 lights_last_update[ch] = main_get_now();
526 static void update_lights_from_rotary(int ch, int delta)
529 lights_brightness[ch] = CLAMP(lights_brightness[ch] + 0.015*delta*abs(delta), 0., 1.);
533 static void update_lights_from_slider(int value)
535 lights_temperature[0] = value / 127.;
536 lights_temperature[1] = value / 127.;
541 static void lights_button_timeout(struct main_timer *t)
543 int ch = (uintptr_t) t->data;
544 DBG("Lights[%d]: Full throttle!", ch);
547 lights_brightness[ch] = 1;
551 static void update_lights_from_button(int ch, int on)
553 static struct main_timer lights_button_timer[2] = {{
554 .handler = lights_button_timeout,
555 .data = (void *)(uintptr_t) 0,
557 .handler = lights_button_timeout,
558 .data = (void *)(uintptr_t) 1,
562 timer_add_rel(&lights_button_timer[ch], 500);
563 else if (timer_is_active(&lights_button_timer[ch]))
565 timer_del(&lights_button_timer[ch]);
566 lights_on[ch] = !lights_on[ch];
571 static void update_lights_from_ir(int ch, int state)
573 lights_on[ch] = state;
575 lights_ir_channel = ch;
578 static void update_lights_ir_num(int num)
580 if (lights_ir_channel < 2)
582 lights_brightness[lights_ir_channel] = num / 9.;
583 send_lights(lights_ir_channel);
587 lights_temperature[0] = num / 9.;
588 lights_temperature[1] = num / 9.;
594 static void update_lights_ir_full(void)
596 lights_brightness[0] = lights_brightness[1] = 1;
603 static double rainbow_brightness;
604 static timestamp_t rainbow_last_update;
606 static void update_rainbow(void)
608 noct_set_ring(8, RING_MODE_LEFT, rainbow_brightness * 127);
611 static void send_rainbow(void)
614 snprintf(val, sizeof(val), "%.3f", rainbow_brightness);
615 mqtt_publish("burrow/lights/rainbow/brightness", val);
616 rainbow_last_update = main_get_now();
620 static void update_rainbow_from_rotary(int delta)
622 rainbow_brightness = CLAMP(rainbow_brightness + 0.015*delta*abs(delta), 0., 1.);
626 /*** Main update routines ***/
628 static struct main_timer update_timer;
629 static timestamp_t last_touch_time;
638 static enum update_state update_state;
640 static bool want_sleep_p(void)
642 CLIST_FOR_EACH(struct pulse_sink *, s, pulse_sink_list)
648 static void do_update(struct main_timer *t)
650 DBG("## UPDATE in state %u", update_state);
654 if (!noct_is_ready())
656 DBG("## UPDATE: Nocturn is not ready");
657 update_state = US_OFFLINE;
660 else if (update_state == US_OFFLINE)
662 DBG("## UPDATE: Going online");
663 update_state = US_ONLINE;
667 if (!pulse_is_ready())
669 DBG("## UPDATE: Pulse is not online");
670 if (update_state != US_PULSE_DEAD)
672 update_state = US_PULSE_DEAD;
674 for (int i=0; i<8; i++)
675 noct_set_button(i, 1);
679 else if (update_state == US_PULSE_DEAD)
681 DBG("## UPDATE: Waking up from the dead");
682 update_state = US_ONLINE;
691 bool want_sleep = want_sleep_p();
693 last_touch_time = main_get_now();
694 timestamp_t since_touch = last_touch_time ? main_get_now() - last_touch_time : 0;
695 timestamp_t sleep_in = 30000;
696 if (since_touch >= sleep_in)
698 DBG("UPDATE: Sleeping");
699 if (update_state == US_ONLINE)
701 update_state = US_SLEEPING;
703 noct_set_ring(8, RING_MODE_LEFT, 127);
709 if (update_state == US_SLEEPING)
711 DBG("UPDATE: Waking up");
712 update_state = US_ONLINE;
717 timestamp_t t = sleep_in - since_touch + 10;
718 DBG("UPDATE: Scheduling sleep in %d ms", (int) t);
719 timer_add_rel(&update_timer, t);
724 update_ring_from_sink(0, PCH_SINK);
725 update_ring_from_sink(1, BT_SINK);
727 update_default_sink();
728 update_source_buttons();
734 void schedule_update(void)
736 timer_add_rel(&update_timer, 10);
739 static bool prepare_notify(void)
741 if (!pulse_is_ready())
743 DBG("## NOTIFY: Pulse is not online");
747 last_touch_time = main_get_now();
748 if (update_state == US_SLEEPING)
750 DBG("## NOTIFY: Scheduling wakeup");
757 void notify_rotary(int rotary, int delta)
759 if (!prepare_notify())
765 update_sink_from_rotary(delta, PCH_SINK);
768 update_sink_from_rotary(delta, BT_SINK);
771 update_lights_from_rotary(1, delta);
774 update_lights_from_rotary(0, delta);
777 update_rainbow_from_rotary(delta);
780 update_lights_from_slider(delta);
783 update_group_from_rotary(rotary, delta);
787 void notify_button(int button, int on)
789 if (!prepare_notify())
795 update_sink_mute_from_button(on, PCH_SINK);
798 update_sink_mute_from_button(on, BT_SINK);
801 update_source_mute_from_button(on, LOGI_SOURCE);
805 update_default_sink_from_button(button, on);
808 update_lights_from_button(1, on);
811 update_lights_from_button(0, on);
814 update_mpd_from_button(button, on);
829 update_group_from_button(button, on);
833 void notify_touch(int rotary UNUSED, int on UNUSED)
835 if (!prepare_notify())
838 // Rotary touches switch meaning of LEDs, this is handled inside display updates
839 if (rotary >= 4 && rotary < 8)
843 static void notify_ir(const char *key)
845 DBG("Received IR key %s", key);
848 if (!strcmp(key, "preset+"))
849 update_lights_from_ir(1, 1);
850 else if (!strcmp(key, "preset-"))
851 update_lights_from_ir(1, 0);
852 else if (!strcmp(key, "tuning-up"))
853 update_lights_from_ir(0, 1);
854 else if (!strcmp(key, "tuning-down"))
855 update_lights_from_ir(0, 0);
856 else if (strlen(key) == 1 && key[0] >= '0' && key[0] <= '9')
857 update_lights_ir_num(key[0] - '0');
858 else if (!strcmp(key, "10/0"))
859 update_lights_ir_num(0);
860 else if (!strcmp(key, "band"))
861 lights_ir_channel = 2;
862 else if (!strcmp(key, "fm-mode"))
863 update_lights_ir_full();
866 else if (!strcmp(key, "play"))
868 else if (!strcmp(key, "stop"))
870 else if (!strcmp(key, "pause"))
872 else if (!strcmp(key, "prev-song"))
874 else if (!strcmp(key, "next-song"))
876 else if (!strcmp(key, "rewind"))
877 update_sink_from_rotary(-2, PCH_SINK);
878 else if (!strcmp(key, "ffwd"))
879 update_sink_from_rotary(2, PCH_SINK);
882 void notify_mqtt(const char *topic, const char *val)
884 const char blc[] = "burrow/lights/catarium/";
885 if (str_has_prefix(topic, blc))
887 topic += strlen(blc);
889 if (!strcmp(topic, "top"))
891 else if (!strcmp(topic, "bottom"))
897 if (sscanf(val, "%lf %lf", &b, &t) != 2)
900 timestamp_t now = main_get_now();
901 if (!lights_last_update[ch] || lights_last_update[ch] + 1000 < now)
903 DBG("Received foreign light settings");
909 lights_brightness[ch] = b;
910 lights_temperature[ch] = t;
916 if (!strcmp(topic, "burrow/lights/rainbow/brightness"))
919 if (sscanf(val, "%lf", &b) == 1 && b >= 0 && b <= 1)
921 timestamp_t now = main_get_now();
922 if (!rainbow_last_update || rainbow_last_update + 1000 < now)
924 DBG("Received foreign rainbow settings");
925 rainbow_brightness = b;
931 if (!strcmp(topic, "burrow/control/catarium-ir"))
935 /*** Main entry point ***/
940 static struct opt_section options = {
942 OPT_HELP("Control console for the Ursary"),
944 OPT_HELP("Options:"),
946 OPT_BOOL('d', "debug", debug, 0, "\tEnable debugging mode (no fork etc.)"),
947 OPT_BOOL(0, "no-fork", no_fork, 0, "\tDo not fork\n"),
952 static void sigterm_handler(struct main_signal *ms UNUSED)
957 static void daemon_body(struct daemon_params *dp)
960 update_timer.handler = do_update;
968 static struct main_signal term_sig = {
970 .handler = sigterm_handler,
972 signal_add(&term_sig);
974 msg(L_INFO, "Ursary daemon starting");
976 msg(L_INFO, "Ursary daemon shut down");
980 int main(int argc UNUSED, char **argv)
982 opt_parse(&options, argv+1);
986 struct daemon_params dp = {
987 .flags = ((debug || no_fork) ? DAEMON_FLAG_SIMULATE : 0),
988 .pid_file = "/run/ursaryd.pid",
989 .run_as_user = "ursary",
996 struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
997 ls->levels = ~(1U << L_DEBUG);
998 log_set_default_stream(ls);
1001 daemon_run(&dp, daemon_body);