X-Git-Url: http://mj.ucw.cz/gitweb/?a=blobdiff_plain;f=ursaryd.c;h=1d0e5ff4ed56ea8881a1506f44f1a4908d5f048b;hb=HEAD;hp=bb057e16a1f105278bbb048d4fe24487d55bfe99;hpb=298340146eefb4a9a62c2dd64b042be0e526e70a;p=ursary.git diff --git a/ursaryd.c b/ursaryd.c index bb057e1..1d0e5ff 100644 --- a/ursaryd.c +++ b/ursaryd.c @@ -1,555 +1,744 @@ -#define LOCAL_DEBUG +/* + * The Ursary Control Panel + * + * (c) 2014-2023 Martin Mares + */ + +#undef LOCAL_DEBUG #include #include +#include +#include #include +#include #include +#include +#include +#include #include #include #include - -#include +#include #include "ursaryd.h" +#include "usb.h" -/*** Interface to PulseAudio ***/ +/* + * Map of all controls + * + * rotary red button green button + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * 0 sink PCH mute switch to PCH + * 1 sink BT mute switch to BT + * 2 ceil brightness mic non-mute ceiling lights on + * 3 desk brightness - desk lights on + * 4 Albireo MPD mute MPD play/pause + * 5 Albireo MPV mute MPD stop + * 6 Albireo Zoom mute MPD prev + * 7 eveyrhing else mute MPD next + * + * center rainbow brightness + * slider light color temperature + */ -static pa_context *pulse_ctx; -static struct main_timer pulse_connect_timer; +#define PCH_SINK "alsa_output.pci-0000_07_00.6.analog-stereo" +#define BT_SINK "bluez_sink.CC_98_8B_D0_8C_06.a2dp_sink" +#define LOGI_SOURCE "alsa_input.usb-046d_Logitech_Webcam_C925e_EF163C5F-02.analog-stereo" -static void pulse_dump(void); - -enum pulse_state { - PS_OFFLINE, - PS_SUBSCRIBE, - PS_GET_CLIENTS, - PS_GET_SINKS, - PS_GET_SINK_INPUTS, - PS_ONLINE, -}; - -static enum pulse_state pulse_state; -#define PULSE_STATE(s) do { pulse_state = s; DBG("Pulse: " #s); } while (0) - -// Tracking of currently running asynchronous operations -struct pulse_op { - cnode n; - pa_operation *o; - bool is_init; -}; +/*** Sink controls ***/ -static clist pulse_op_list; - -static struct pulse_op *pulse_op_new(void) +static double volume_from_pa(pa_volume_t vol) { - struct pulse_op *op = xmalloc_zero(sizeof(*op)); - clist_add_tail(&pulse_op_list, &op->n); - return op; + return (double) vol / PA_VOLUME_NORM; } -static void pulse_op_done(struct pulse_op *op) +static pa_volume_t volume_to_pa(double vol) { - if (op->o) - pa_operation_unref(op->o); - clist_remove(&op->n); - xfree(op); + return vol * PA_VOLUME_NORM + 0.0001; } -static void pulse_op_cancel_all(void) +static void update_ring_from_sink(int ring, const char *sink_name) { - struct pulse_op *op; - while (op = (struct pulse_op *) clist_head(&pulse_op_list)) + struct pulse_sink *s = pulse_sink_by_name(sink_name); + if (!s) { - DBG("Pulse: Cancelling pending operation"); - pa_operation_cancel(op->o); - pulse_op_done(op); + noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f); + noct_set_button(ring, 0); + return; + } + + if (s->mute) + { + noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f); + noct_set_button(ring, 1); + return; } -} -#define PULSE_ASYNC_RUN(name, ...) do { struct pulse_op *_op = pulse_op_new(); _op->o = name(__VA_ARGS__, _op); } while (0) -#define PULSE_ASYNC_INIT_RUN(name, ...) do { struct pulse_op *_op = pulse_op_new(); _op->is_init = 1; _op->o = name(__VA_ARGS__, _op); } while (0) + double vol = CLAMP(volume_from_pa(s->volume), 0, 1); + noct_set_ring(ring, RING_MODE_LEFT, CLAMP((int)(0x7f * vol), 12, 0x7f)); + noct_set_button(ring, 0); +} -static void pulse_success_cb(pa_context *ctx UNUSED, int success, void *userdata) +static void update_sink_from_rotary(int delta, const char *sink_name) { - if (!success) - msg(L_ERROR, "Pulse: Failure reported"); - pulse_op_done(userdata); + struct pulse_sink *s = pulse_sink_by_name(sink_name); + if (!s) + return; + + double vol = volume_from_pa(s->volume) + delta * 0.02; + pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, 1)); + if (pavol == s->volume) + return; + pa_cvolume cvol; + pa_cvolume_set(&cvol, s->channels, pavol); + + DBG("## Setting volume of sink %s to %d", s->name, cvol.values[0]); + pulse_sink_set_volume(s->idx, &cvol); } -static void pulse_dump_proplist(pa_proplist *pl UNUSED) +static void update_sink_mute_from_button(int on, const char *sink_name) { -#if 0 - void *iterator = NULL; - const char *key; + if (!on) + return; - while (key = pa_proplist_iterate(pl, &iterator)) - { - const char *val = pa_proplist_gets(pl, key); - DBG(" %s = %s", key, val); - } -#endif + struct pulse_sink *s = pulse_sink_by_name(sink_name); + if (!s) + return; + + DBG("## Setting mute of sink %s to %d", s->name, !s->mute); + pulse_sink_set_mute(s->idx, !s->mute); } -struct pulse_sink_input { - int idx; - char *name; - int client_idx; - int sink_idx; - uns channels; - uns volume; - uns mute; - int noct_client_idx; // Used by the high-level logic below +/*** Client controls ***/ + +struct client_map { + int group; + const char *client; + const char *host; +}; + +static struct client_map client_map[] = { + { 4, "Music Player Daemon", "albireo", }, + { 5, "mpv", "albireo", }, + { 6, "ZOOM VoiceEngine", "albireo", }, + { 7, NULL, NULL, }, }; -#define HASH_NODE struct pulse_sink_input -#define HASH_PREFIX(x) pulse_sink_input_##x -#define HASH_KEY_ATOMIC idx -#define HASH_WANT_CLEANUP -#define HASH_WANT_LOOKUP -#define HASH_WANT_REMOVE -#define HASH_ZERO_FILL -#include +#define CLIENT_MAP_SIZE ARRAY_SIZE(client_map) -#define SET_STRING(_field, _val) do { if (!_field || strcmp(_field, _val)) { xfree(_field); _field = xstrdup(_val); } } while (0) +struct group_config { + bool enabled; + double range; +}; -static void pulse_sink_input_cb(pa_context *ctx UNUSED, const pa_sink_input_info *i, int eol, void *userdata) +struct group_state { + double volume; + bool have_muted[2]; +}; + +#define NUM_GROUPS 9 + +static struct group_config group_config[NUM_GROUPS] = { + [4] = { .enabled = 1, .range = 1.5 }, + [5] = { .enabled = 1, .range = 1.5 }, + [6] = { .enabled = 1, .range = 1.5 }, + [7] = { .enabled = 1, .range = 1.5 }, +}; + +static struct group_state group_state[NUM_GROUPS]; + +static void calc_groups(void) { - struct pulse_op *op = userdata; + bzero(group_state, sizeof(group_state)); - if (eol) + CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list) { - if (op->is_init) + s->noct_group_idx = -1; + + if (s->client_idx < 0 || s->sink_idx < 0) + continue; + + struct pulse_client *c = pulse_client_by_idx(s->client_idx); + if (!c) + continue; + + for (uns i=0; i < CLIENT_MAP_SIZE; i++) { - PULSE_STATE(PS_ONLINE); - schedule_update(); + struct client_map *cm = &client_map[i]; + if ((!cm->client || !strcmp(cm->client, c->name)) && + (!cm->host || !strcmp(cm->host, c->host))) + { + int g = cm->group; + struct group_state *gs = &group_state[g]; + DBG("@@ Client #%d, sink input #%d -> group %d", s->client_idx, s->idx, g); + s->noct_group_idx = g; + gs->volume = MAX(gs->volume, s->volume); + gs->have_muted[!!s->mute] = 1; + break; + } } - pulse_op_done(op); - return; } +} + +static void update_groups(void) +{ + calc_groups(); + + for (uns i=0; i < NUM_GROUPS; i++) + { + struct group_config *gc = &group_config[i]; + struct group_state *gs = &group_state[i]; + if (!gc->enabled) + continue; - DBG("Pulse: SINK INPUT #%u: %s client=%d sink=%d chans=%d has_vol=%d vol_rw=%d volume=%u mute=%d", - i->index, i->name, i->client, i->sink, i->channel_map.channels, i->has_volume, i->volume_writable, i->volume.values[0], i->mute); - pulse_dump_proplist(i->proplist); + 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); - struct pulse_sink_input *s = pulse_sink_input_lookup(i->index); - SET_STRING(s->name, i->name); - s->client_idx = i->client; - s->sink_idx = i->sink; - s->channels = i->channel_map.channels; - s->volume = pa_cvolume_avg(&i->volume); - s->mute = i->mute; - schedule_update(); + if (!gs->have_muted[0] && !gs->have_muted[1]) + { + noct_set_ring(i, RING_MODE_LEFT, 0); + noct_set_button(i, 0); + } + else if (!gs->have_muted[0]) + { + noct_set_ring(i, RING_MODE_SINGLE_ON, 0x7f); + noct_set_button(i, 1); + } + else + { + double vol = CLAMP(volume_from_pa(gs->volume), 0, gc->range); + int val = 0x7f * vol / gc->range; + val = CLAMP(val, 12, 0x7f); + noct_set_ring(i, RING_MODE_LEFT, val); + noct_set_button(i, 0); + } + } } -static void pulse_sink_input_gone(int idx) +static void update_group_from_rotary(int i, int delta) { - DBG("Pulse: REMOVE SINK INPUT #%d", idx); - struct pulse_sink_input *s = pulse_sink_input_lookup(idx); - pulse_sink_input_remove(s); - schedule_update(); -} + if (i >= NUM_GROUPS) + return; + struct group_config *gc = &group_config[i]; + struct group_state *gs = &group_state[i]; + if (!gc->enabled) + return; -struct pulse_sink { - int idx; - char *name; - uns channels; - uns volume; - uns base_volume; - int mute; -}; + calc_groups(); + double vol = volume_from_pa(gs->volume) + delta*0.02; + pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, gc->range)); -#define HASH_NODE struct pulse_sink -#define HASH_PREFIX(x) pulse_sink_##x -#define HASH_KEY_ATOMIC idx -#define HASH_WANT_CLEANUP -#define HASH_WANT_LOOKUP -#define HASH_WANT_REMOVE -#define HASH_ZERO_FILL -#include + CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list) + { + if (s->noct_group_idx == i && s->volume != pavol) + { + DBG("@@ Client #%d, sink input #%d: setting volume=%u", s->client_idx, s->idx, pavol); + pa_cvolume cvol; + pa_cvolume_set(&cvol, s->channels, pavol); + pulse_sink_input_set_volume(s->idx, &cvol); + } + } +} -static void pulse_sink_cb(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) +static void update_group_from_button(int i, int on) { - struct pulse_op *op = userdata; + if (!on) + return; + if (i >= NUM_GROUPS) + return; + struct group_config *gc = &group_config[i]; + struct group_state *gs = &group_state[i]; + if (!gc->enabled) + return; - if (eol) + calc_groups(); + if (!gs->have_muted[0] && !gs->have_muted[1]) + return; + uns mute = !gs->have_muted[1]; + + CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list) { - if (op->is_init) + if (s->noct_group_idx == i) { - PULSE_STATE(PS_GET_SINK_INPUTS); - PULSE_ASYNC_INIT_RUN(pa_context_get_sink_input_info_list, ctx, pulse_sink_input_cb); + DBG("@@ Client #%d, sink input #%d: setting mute=%u", s->client_idx, s->idx, mute); + pulse_sink_input_set_mute(s->idx, mute); } - pulse_op_done(op); - return; } +} - DBG("Pulse: SINK #%u: %s (%s) flags=%08x channels=%u volume=%u mute=%d base_vol=%u state=%u", - i->index, i->name, i->description, i->flags, i->channel_map.channels, i->volume.values[0], i->mute, i->base_volume, i->state); - pulse_dump_proplist(i->proplist); +static int find_touched_client(void) +{ + int touched = -1; - struct pulse_sink *s = pulse_sink_lookup(i->index); - SET_STRING(s->name, i->name); - s->channels = i->channel_map.channels; - s->volume = pa_cvolume_avg(&i->volume); - s->base_volume = i->base_volume; - s->mute = i->mute; - schedule_update(); + for (uns i=0; i < NUM_GROUPS; i++) + if (group_config[i].enabled && noct_rotary_touched[i]) + { + if (touched >= 0) + return -1; + touched = i; + } + return touched; } -static void pulse_sink_gone(int idx) +/*** Default sink controls ***/ + +static const char *get_client_sink(int i) { - DBG("Pulse: REMOVE SINK #%d", idx); - struct pulse_sink *s = pulse_sink_lookup(idx); - pulse_sink_remove(s); - schedule_update(); + const char *sink = NULL; + + CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list) + if (s->noct_group_idx == i) + { + struct pulse_sink *sk = (s->sink_idx >= 0) ? pulse_sink_by_idx(s->sink_idx) : NULL; + const char *ss = sk ? sk->name : NULL; + if (!sink) + sink = ss; + else if (strcmp(sink, ss)) + sink = "?"; + } + return sink ? : "?"; } -static struct pulse_sink *pulse_sink_by_name(const char *name) +static void update_default_sink(void) { - HASH_FOR_ALL(pulse_sink, s) + int i = find_touched_client(); + const char *sink; + if (i >= 0) + sink = get_client_sink(i); + else + sink = pulse_default_sink_name ? : "?"; + + if (!strcmp(sink, PCH_SINK)) { - if (!strcmp(s->name, name)) - return s; + noct_set_button(8, 1); + noct_set_button(9, 0); + } + else if (!strcmp(sink, BT_SINK)) + { + noct_set_button(8, 0); + noct_set_button(9, 1); + } + else + { + noct_set_button(8, 0); + noct_set_button(9, 0); } - HASH_END_FOR; - return NULL; } -struct pulse_client { - int idx; - char *name; - char *host; -}; +static void update_default_sink_from_button(int button, int on) +{ + if (!on) + return; -#define HASH_NODE struct pulse_client -#define HASH_PREFIX(x) pulse_client_##x -#define HASH_KEY_ATOMIC idx -#define HASH_WANT_CLEANUP -#define HASH_WANT_LOOKUP -#define HASH_WANT_REMOVE -#define HASH_ZERO_FILL -#include + int i = find_touched_client(); +#if 0 + const char *sink; + if (i >= 0) + sink = get_client_sink(i); + else + sink = pulse_default_sink_name ? : "?"; +#endif -static void pulse_client_cb(pa_context *ctx, const pa_client_info *i, int eol, void *userdata) -{ - struct pulse_op *op = userdata; + const char *switch_to = NULL; + if (button == 8) + switch_to = PCH_SINK; + else if (button == 9) + switch_to = BT_SINK; + + if (!switch_to) + return; - if (eol) + if (i >= 0) { - if (op->is_init) - { - PULSE_STATE(PS_GET_SINKS); - PULSE_ASYNC_INIT_RUN(pa_context_get_sink_info_list, ctx, pulse_sink_cb); - } - pulse_op_done(op); - return; + struct pulse_sink *sk = pulse_sink_by_name(switch_to); + if (!sk) + return; + + CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list) + if (s->noct_group_idx == i) + { + DBG("Moving input #%d to sink #%d", s->idx, sk->idx); + pulse_sink_input_move(s->idx, sk->idx); + } + } + else + { + DBG("Switching default sink to %s", switch_to); + pulse_server_set_default_sink(switch_to); } - - char *host = stk_strdup(pa_proplist_gets(i->proplist, "application.process.host") ? : "?"); - DBG("Pulse: CLIENT #%u: %s mod=%u drv=%s host=%s", - i->index, i->name, i->owner_module, i->driver, host); - pulse_dump_proplist(i->proplist); - - struct pulse_client *c = pulse_client_lookup(i->index); - SET_STRING(c->name, i->name); - SET_STRING(c->host, host); - schedule_update(); } -static void pulse_client_gone(int idx) -{ - DBG("Pulse: REMOVE CLIENT #%d", idx); - struct pulse_client *c = pulse_client_lookup(idx); - pulse_client_remove(c); - schedule_update(); -} +/*** Source mute controls ***/ -static void pulse_shutdown(void) +static void update_source_buttons(void) { - DBG("Pulse: Shutting down"); - pulse_client_cleanup(); - pulse_sink_cleanup(); - pulse_sink_input_cleanup(); + struct pulse_source *source = pulse_source_by_name(LOGI_SOURCE); + if (!source) + return; + +#if 0 // Disabled for now + // if (source->suspended || source->mute) + if (source->mute) + noct_set_button(2, 0); + else + noct_set_button(2, 1); +#endif } -static void pulse_subscribe_done_cb(pa_context *ctx, int success, void *userdata) +static void update_source_mute_from_button(int on, const char *source_name) { - pulse_op_done(userdata); + if (!on) + return; - if (!success) - msg(L_ERROR, "pa_context_subscribe failed: success=%d", success); + struct pulse_source *s = pulse_source_by_name(source_name); + if (!s) + return; - PULSE_STATE(PS_GET_CLIENTS); - PULSE_ASYNC_INIT_RUN(pa_context_get_client_info_list, ctx, pulse_client_cb); +#if 0 + DBG("## Setting mute of source %s to %d", s->name, !s->mute); + pulse_source_set_mute(s->idx, !s->mute); +#endif } -static void pulse_event_cb(pa_context *ctx, pa_subscription_event_type_t type, uint32_t idx, void *userdata UNUSED) -{ - DBG("Pulse: SUBSCRIBE EVENT type=%08x idx=%u", type, idx); +/*** MPD controls ***/ - uns object = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; - uns action = type & PA_SUBSCRIPTION_EVENT_TYPE_MASK; - switch (object) - { - case PA_SUBSCRIPTION_EVENT_CLIENT: - if (action == PA_SUBSCRIPTION_EVENT_NEW || action == PA_SUBSCRIPTION_EVENT_CHANGE) - PULSE_ASYNC_RUN(pa_context_get_client_info, ctx, idx, pulse_client_cb); - else if (action == PA_SUBSCRIPTION_EVENT_REMOVE) - pulse_client_gone(idx); - break; - case PA_SUBSCRIPTION_EVENT_SINK: - if (action == PA_SUBSCRIPTION_EVENT_NEW || action == PA_SUBSCRIPTION_EVENT_CHANGE) - PULSE_ASYNC_RUN(pa_context_get_sink_info_by_index, ctx, idx, pulse_sink_cb); - else if (action == PA_SUBSCRIPTION_EVENT_REMOVE) - pulse_sink_gone(idx); - break; - case PA_SUBSCRIPTION_EVENT_SINK_INPUT: - if (action == PA_SUBSCRIPTION_EVENT_NEW || action == PA_SUBSCRIPTION_EVENT_CHANGE) - PULSE_ASYNC_RUN(pa_context_get_sink_input_info, ctx, idx, pulse_sink_input_cb); - else if (action == PA_SUBSCRIPTION_EVENT_REMOVE) - pulse_sink_input_gone(idx); - break; - } +static bool mpd_flash_state; + +static void mpd_flash_timeout(struct main_timer *t) +{ + mpd_flash_state ^= 1; + noct_set_button(12, mpd_flash_state); + timer_add_rel(t, 500); } -static void pulse_state_cb(pa_context *ctx, void *userdata UNUSED) +static struct main_timer mpd_flash_timer = { + .handler = mpd_flash_timeout, +}; + +static void update_mpd(void) { - int state = pa_context_get_state(ctx); - DBG("Pulse: State callback, new state = %d", state); - if (state == PA_CONTEXT_READY) + const char *state = mpd_get_player_state(); + if (!strcmp(state, "play")) { - if (pulse_state == PS_OFFLINE) + noct_set_button(12, 1); + timer_del(&mpd_flash_timer); + } + else if (!strcmp(state, "pause")) + { + if (!timer_is_active(&mpd_flash_timer)) { - PULSE_STATE(PS_SUBSCRIBE); - pa_context_set_subscribe_callback(ctx, pulse_event_cb, NULL); - PULSE_ASYNC_INIT_RUN(pa_context_subscribe, ctx, PA_SUBSCRIPTION_MASK_ALL, pulse_subscribe_done_cb); + mpd_flash_state = 1; + mpd_flash_timeout(&mpd_flash_timer); } } else { - if (pulse_state != PS_OFFLINE) - { - PULSE_STATE(PS_OFFLINE); - pulse_op_cancel_all(); - pulse_shutdown(); - schedule_update(); - } - if (state == PA_CONTEXT_FAILED && !timer_is_active(&pulse_connect_timer)) - timer_add_rel(&pulse_connect_timer, 2000); + noct_set_button(12, 0); + timer_del(&mpd_flash_timer); } } -static void pulse_dump(void) +static void mpd_button_timeout(struct main_timer *t) +{ + DBG("MPD stop"); + timer_del(t); + mpd_stop(); +} + +static void update_mpd_from_button(int button UNUSED, int on) { - HASH_FOR_ALL(pulse_client, c) + static struct main_timer mpd_button_timer = { + .handler = mpd_button_timeout, + }; + + const char *state = mpd_get_player_state(); + + if (!on) { - DBG("## Client #%d: %s host=%s", c->idx, c->name, c->host); + if (timer_is_active(&mpd_button_timer)) + { + timer_del(&mpd_button_timer); + if (!strcmp(state, "play")) + { + DBG("MPD pause"); + mpd_pause(1); + } + else if (!strcmp(state, "pause")) + { + DBG("MPD resume"); + mpd_pause(0); + } + } + return; } - HASH_END_FOR; - HASH_FOR_ALL(pulse_sink, s) + if (!strcmp(state, "stop")) { - DBG("## Sink #%d: %s channels=%u volume=%u base_vol=%u mute=%u", - s->idx, s->name, s->channels, s->volume, s->base_volume, s->mute); + DBG("MPD play"); + mpd_play(); } - HASH_END_FOR; - - HASH_FOR_ALL(pulse_sink_input, s) + else { - DBG("## Sink input #%d: %s client=%d sink=%d channels=%u volume=%u mute=%u", - s->idx, s->name, s->client_idx, s->sink_idx, s->channels, s->volume, s->mute); + DBG("MPD starting button timer"); + timer_add_rel(&mpd_button_timer, 1000); } - HASH_END_FOR; } -static void pulse_connect(struct main_timer *t) -{ - DBG("Pulse: Connecting"); - timer_del(t); - - clist_init(&pulse_op_list); - pulse_client_init(); - pulse_sink_init(); - pulse_sink_input_init(); +/*** Lights ***/ - if (pulse_ctx) - pa_context_unref(pulse_ctx); - pulse_ctx = pa_context_new(&pmain_api, "ursaryd"); +static bool lights_on[2]; +static double lights_brightness[2]; +static double lights_temperature[2]; +static timestamp_t lights_last_update[2]; - pa_context_set_state_callback(pulse_ctx, pulse_state_cb, NULL); - pa_context_connect(pulse_ctx, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL); +static void update_lights(void) +{ + for (uint ch=0; ch < 2; ch++) + { + if (lights_on[ch]) + { + noct_set_ring(3-ch, RING_MODE_LEFT, lights_brightness[ch] * 127); + noct_set_button(11-ch, 1); + } + else + { + noct_set_ring(3-ch, RING_MODE_LEFT, 0); + noct_set_button(11-ch, 0); + } + } } -static void pulse_init(void) +static void send_lights(int ch) { - pmain_init(); + DBG("Lights[%d]: on=%d bri=%.3f temp=%.3f", ch, lights_on[ch], lights_brightness[ch], lights_temperature[ch]); + double b = lights_on[ch] ? lights_brightness[ch] : 0; + double t = lights_on[ch] ? lights_temperature[ch] : 0; + char topic[100], val[100]; + snprintf(topic, sizeof(topic), "burrow/lights/catarium/%s", (ch ? "top" : "bottom")); + snprintf(val, sizeof(val), "%.3f %.3f", b, t); + mqtt_publish(topic, val); + lights_last_update[ch] = main_get_now(); + update_lights(); +} - pulse_connect_timer.handler = pulse_connect; - timer_add_rel(&pulse_connect_timer, 0); +static void update_lights_from_rotary(int ch, int delta) +{ + if (lights_on[ch]) + lights_brightness[ch] = CLAMP(lights_brightness[ch] + 0.015*delta*abs(delta), 0., 1.); + send_lights(ch); } -/*** High-level logic ***/ +static void update_lights_from_slider(int value) +{ + lights_temperature[0] = value / 127.; + lights_temperature[1] = value / 127.; + send_lights(0); + send_lights(1); +} -static struct main_timer update_timer; +static void lights_button_timeout(struct main_timer *t) +{ + int ch = (uintptr_t) t->data; + DBG("Lights[%d]: Full throttle!", ch); + timer_del(t); + lights_on[ch] = 1; + lights_brightness[ch] = 1; + send_lights(ch); +} -static void update_ring_from_sink(int ring, const char *sink_name) +static void update_lights_from_button(int ch, int on) { - struct pulse_sink *s = pulse_sink_by_name(sink_name); - if (!s) + static struct main_timer lights_button_timer[2] = {{ + .handler = lights_button_timeout, + .data = (void *)(uintptr_t) 0, + },{ + .handler = lights_button_timeout, + .data = (void *)(uintptr_t) 1, + }}; + + if (on) + timer_add_rel(&lights_button_timer[ch], 500); + else if (timer_is_active(&lights_button_timer[ch])) { - noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f); - noct_set_button(ring, 0); - return; + timer_del(&lights_button_timer[ch]); + lights_on[ch] = !lights_on[ch]; + send_lights(ch); } +} - if (s->mute) +static void update_lights_from_ir(int ch, int dir) +{ + if (lights_on[ch]) + lights_brightness[ch] = CLAMP(lights_brightness[ch] + 0.07*dir, 0., 1.); + else if (dir > 0) { - noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f); - noct_set_button(ring, 1); - return; + lights_on[ch] = 1; + lights_brightness[ch] = 1; + } + else + { + lights_on[ch] = 1; + lights_brightness[ch] = 0.05; } + send_lights(ch); +} - double vol = pa_sw_volume_to_linear(s->volume); - vol = CLAMP(vol, 0, 1); - int val = 0x7f * vol; - val = CLAMP(val, 12, 0x7f); - noct_set_ring(ring, RING_MODE_LEFT, val); - noct_set_button(ring, 0); +static void update_lights_on_off_ir(int ch) +{ + lights_on[ch] ^= 1; + send_lights(ch); } -struct client_map { - int rotary; - const char *client; - const char *host; - double range; -}; +static void update_lights_temp_ir(void) +{ + if (!lights_on[0] && !lights_on[1]) + return; -static struct client_map client_map[] = { - { 4, "Music Player Daemon", "albireo", 1 }, - { 5, "MPlayer", NULL, 1 }, - { 6, NULL, "ogion", 1 }, -}; + double t = (lights_temperature[0] + lights_temperature[1]) / 2; + if (t >= 0.66) + t = 0; + else if (t < 0.33) + t = 0.5; + else + t = 1; + lights_temperature[0] = lights_temperature[1] = t; -#define NUM_CLIENTS ARRAY_SIZE(client_map) + send_lights(0); + send_lights(1); +} -struct client_state { - double volume; - bool have_muted[2]; -}; +/*** Rainbow ***/ -static struct client_state client_state[NUM_CLIENTS]; +static double rainbow_brightness; +static timestamp_t rainbow_last_update; -static int find_client_by_rotary(int rotary) +static void update_rainbow(void) { - uns i; - for (i=0; i < NUM_CLIENTS; i++) - if (client_map[i].rotary == rotary) - return i; - return -1; + noct_set_ring(8, RING_MODE_LEFT, rainbow_brightness * 127); } -static void calc_clients(void) +static void send_rainbow(void) { - bzero(client_state, sizeof(client_state)); + char val[100]; + snprintf(val, sizeof(val), "%.3f", rainbow_brightness); + mqtt_publish("burrow/lights/rainbow/brightness", val); + rainbow_last_update = main_get_now(); + update_rainbow(); +} - HASH_FOR_ALL(pulse_sink_input, s) - { - s->noct_client_idx = -1; +static void update_rainbow_from_rotary(int delta) +{ + rainbow_brightness = CLAMP(rainbow_brightness + 0.015*delta*abs(delta), 0., 1.); + send_rainbow(); +} - if (s->client_idx < 0 || s->sink_idx < 0) - continue; +/*** Main update routines ***/ - struct pulse_client *c = pulse_client_lookup(s->client_idx); - if (!c) - continue; +static struct main_timer update_timer; +static timestamp_t last_touch_time; - for (uns i=0; i < NUM_CLIENTS; i++) - { - struct client_map *cm = &client_map[i]; - struct client_state *cs = &client_state[i]; - if ((!cm->client || !strcmp(cm->client, c->name)) && - (!cm->host || !strcmp(cm->host, c->host))) - { - // DBG("@@ Client #%d, sink input #%d -> rotary %d", s->client_idx, s->idx, cm->rotary); - s->noct_client_idx = i; - cs->volume = MAX(cs->volume, s->volume); - cs->have_muted[!!s->mute] = 1; - break; - } - } - } - HASH_END_FOR; -} +enum update_state { + US_OFFLINE, + US_ONLINE, + US_SLEEPING, + US_PULSE_DEAD, +}; -static void update_clients(void) -{ - calc_clients(); +static enum update_state update_state; - for (uns i=0; i < NUM_CLIENTS; i++) - { - struct client_map *cm = &client_map[i]; - struct client_state *cs = &client_state[i]; - if (!cs->have_muted[0] && !cs->have_muted[1]) - { - noct_set_ring(cm->rotary, RING_MODE_LEFT, 0); - noct_set_button(cm->rotary, 0); - } - else if (!cs->have_muted[0]) - { - noct_set_ring(cm->rotary, RING_MODE_SINGLE_ON, 0x7f); - noct_set_button(cm->rotary, 1); - } - else - { - double vol = pa_sw_volume_to_linear(cs->volume); - vol = CLAMP(vol, 0, cm->range); - int val = 0x7f * vol / cm->range; - val = CLAMP(val, 12, 0x7f); - noct_set_ring(cm->rotary, RING_MODE_LEFT, val); - noct_set_button(cm->rotary, 0); - } - } +static bool want_sleep_p(void) +{ + CLIST_FOR_EACH(struct pulse_sink *, s, pulse_sink_list) + if (!s->suspended) + return 0; + return 1; } static void do_update(struct main_timer *t) { + DBG("## UPDATE in state %u", update_state); timer_del(t); + + // Nocturn dead? if (!noct_is_ready()) { DBG("## UPDATE: Nocturn is not ready"); + update_state = US_OFFLINE; return; } + else if (update_state == US_OFFLINE) + { + DBG("## UPDATE: Going online"); + update_state = US_ONLINE; + } - static bool dead; - if (pulse_state != PS_ONLINE) + // Pulse dead? + if (!pulse_is_ready()) { DBG("## UPDATE: Pulse is not online"); - for (int i=0; i<=8; i++) - noct_set_ring(i, RING_MODE_LEFT, 0); - for (int i=0; i<8; i++) + if (update_state != US_PULSE_DEAD) { - noct_set_button(i, 1); - noct_set_button(i+8, 0); + update_state = US_PULSE_DEAD; + noct_clear(); + for (int i=0; i<8; i++) + noct_set_button(i, 1); } - dead = 1; return; } - if (dead) + else if (update_state == US_PULSE_DEAD) { DBG("## UPDATE: Waking up from the dead"); - for (int i=0; i<=8; i++) - noct_set_ring(i, RING_MODE_LEFT, 0); - for (int i=0; i<16; i++) - noct_set_button(i, 0); - dead = 0; + update_state = US_ONLINE; + noct_clear(); } - DBG("## UPDATE"); +#ifdef LOCAL_DEBUG pulse_dump(); +#endif + + // Sleeping? + bool want_sleep = want_sleep_p(); + if (!want_sleep) + last_touch_time = main_get_now(); + timestamp_t since_touch = last_touch_time ? main_get_now() - last_touch_time : 0; + timestamp_t sleep_in = 30000; + if (since_touch >= sleep_in) + { + DBG("UPDATE: Sleeping"); + if (update_state == US_ONLINE) + { + update_state = US_SLEEPING; + noct_clear(); + noct_set_ring(8, RING_MODE_LEFT, 127); + } + return; + } + else + { + if (update_state == US_SLEEPING) + { + DBG("UPDATE: Waking up"); + update_state = US_ONLINE; + noct_clear(); + } + if (want_sleep) + { + timestamp_t t = sleep_in - since_touch + 10; + DBG("UPDATE: Scheduling sleep in %d ms", (int) t); + timer_add_rel(&update_timer, t); + } + } - update_ring_from_sink(0, "ursarium"); - update_ring_from_sink(1, "catarium"); - update_clients(); + // Everything normal + update_ring_from_sink(0, PCH_SINK); + update_ring_from_sink(1, BT_SINK); + update_groups(); + update_default_sink(); + update_source_buttons(); + update_mpd(); + update_lights(); + update_rainbow(); } void schedule_update(void) @@ -557,150 +746,266 @@ void schedule_update(void) timer_add_rel(&update_timer, 10); } -static void update_sink_from_rotary(int delta, const char *sink_name) +static bool prepare_notify(void) { - struct pulse_sink *s = pulse_sink_by_name(sink_name); - if (!s) - return; - - double vol = pa_sw_volume_to_linear(s->volume); - vol += delta * 0.02; - vol = CLAMP(vol, 0, 1); - pa_volume_t pavol = pa_sw_volume_from_linear(vol); - if (pavol == s->volume) - return; - pa_cvolume cvol; - pa_cvolume_set(&cvol, s->channels, pavol); - - DBG("## Setting volume of sink %s to %d", s->name, cvol.values[0]); - PULSE_ASYNC_RUN(pa_context_set_sink_volume_by_index, pulse_ctx, s->idx, &cvol, pulse_success_cb); -} - -static void update_client_from_rotary(int rotary, int delta) -{ - int i = find_client_by_rotary(rotary); - if (i < 0) - return; - struct client_map *cm = &client_map[i]; - struct client_state *cs = &client_state[i]; - - calc_clients(); - double vol = pa_sw_volume_to_linear(cs->volume); - vol += delta * 0.02; - vol = CLAMP(vol, 0, cm->range); - pa_volume_t pavol = pa_sw_volume_from_linear(vol); + if (!pulse_is_ready()) + { + DBG("## NOTIFY: Pulse is not online"); + return 0; + } - HASH_FOR_ALL(pulse_sink_input, s) + last_touch_time = main_get_now(); + if (update_state == US_SLEEPING) { - if (s->noct_client_idx == i && s->volume != pavol) - { - DBG("@@ Client #%d, sink input #%d: setting volume=%u", s->client_idx, s->idx, pavol); - pa_cvolume cvol; - pa_cvolume_set(&cvol, s->channels, pavol); - PULSE_ASYNC_RUN(pa_context_set_sink_input_volume, pulse_ctx, s->idx, &cvol, pulse_success_cb); - } + DBG("## NOTIFY: Scheduling wakeup"); + schedule_update(); } - HASH_END_FOR; + + return 1; } void notify_rotary(int rotary, int delta) { - if (pulse_state != PS_ONLINE) - { - DBG("## NOTIFY: Pulse is not online"); - return; - } + if (!prepare_notify()) + return; switch (rotary) { case 0: - update_sink_from_rotary(delta, "ursarium"); + update_sink_from_rotary(delta, PCH_SINK); break; case 1: - update_sink_from_rotary(delta, "catarium"); + update_sink_from_rotary(delta, BT_SINK); + break; + case 2: + update_lights_from_rotary(1, delta); + break; + case 3: + update_lights_from_rotary(0, delta); break; case 8: - update_sink_from_rotary(delta, "ursarium"); - update_sink_from_rotary(delta, "catarium"); + update_rainbow_from_rotary(delta); + break; + case 9: + update_lights_from_slider(delta); break; default: - update_client_from_rotary(rotary, delta); + update_group_from_rotary(rotary, delta); } } -static void update_sink_mute_from_button(int on, const char *sink_name) +void notify_button(int button, int on) { - if (!on) - return; - - struct pulse_sink *s = pulse_sink_by_name(sink_name); - if (!s) + if (!prepare_notify()) return; - DBG("## Setting mute of sink %s to %d", s->name, !s->mute); - PULSE_ASYNC_RUN(pa_context_set_sink_mute_by_index, pulse_ctx, s->idx, !s->mute, pulse_success_cb); + switch (button) + { + case 0: + update_sink_mute_from_button(on, PCH_SINK); + break; + case 1: + update_sink_mute_from_button(on, BT_SINK); + break; + case 2: + update_source_mute_from_button(on, LOGI_SOURCE); + break; + case 8: + case 9: + update_default_sink_from_button(button, on); + break; + case 10: + update_lights_from_button(1, on); + break; + case 11: + update_lights_from_button(0, on); + break; + case 12: + update_mpd_from_button(button, on); + break; + case 13: + if (on) + mpd_stop(); + break; + case 14: + if (on) + mpd_prev(); + break; + case 15: + if (on) + mpd_next(); + break; + default: + update_group_from_button(button, on); + } } -static void update_client_from_button(int button, int on) +void notify_touch(int rotary UNUSED, int on UNUSED) { - if (button >= 8 || !on) + if (!prepare_notify()) return; - int i = find_client_by_rotary(button); - if (i < 0) - return; - struct client_state *cs = &client_state[i]; + // Rotary touches switch meaning of LEDs, this is handled inside display updates + if (rotary >= 4 && rotary < 8) + schedule_update(); +} + +static void notify_ir(const char *key) +{ + DBG("Received IR key %s", key); + + // Lights + if (!strcmp(key, "preset+")) + update_lights_from_ir(1, 1); + else if (!strcmp(key, "preset-")) + update_lights_from_ir(1, -1); + else if (!strcmp(key, "tuning-up")) + update_lights_from_ir(0, 1); + else if (!strcmp(key, "tuning-down")) + update_lights_from_ir(0, -1); + else if (!strcmp(key, "band")) + update_lights_on_off_ir(1); + else if (!strcmp(key, "fm-mode")) + update_lights_on_off_ir(0); + else if (!strcmp(key, "dimmer")) + update_lights_temp_ir(); + + // Player + else if (!strcmp(key, "play")) + mpd_play(); + else if (!strcmp(key, "stop")) + mpd_stop(); + else if (!strcmp(key, "pause")) + mpd_pause(1); + else if (!strcmp(key, "prev-song")) + mpd_prev(); + else if (!strcmp(key, "next-song")) + mpd_next(); + else if (!strcmp(key, "rewind")) + update_sink_from_rotary(-2, PCH_SINK); + else if (!strcmp(key, "ffwd")) + update_sink_from_rotary(2, PCH_SINK); +} + +void notify_mqtt(const char *topic, const char *val) +{ + const char blc[] = "burrow/lights/catarium/"; + if (str_has_prefix(topic, blc)) + { + topic += strlen(blc); + int ch; + if (!strcmp(topic, "top")) + ch = 1; + else if (!strcmp(topic, "bottom")) + ch = 0; + else + return; - calc_clients(); - if (!cs->have_muted[0] && !cs->have_muted[1]) - return; - uns mute = !cs->have_muted[1]; + double b, t; + if (sscanf(val, "%lf %lf", &b, &t) != 2) + return; - HASH_FOR_ALL(pulse_sink_input, s) - { - if (s->noct_client_idx == i) + timestamp_t now = main_get_now(); + if (!lights_last_update[ch] || lights_last_update[ch] + 1000 < now) { - DBG("@@ Client #%d, sink input #%d: setting mute=%u", s->client_idx, s->idx, mute); - PULSE_ASYNC_RUN(pa_context_set_sink_input_mute, pulse_ctx, s->idx, mute, pulse_success_cb); + DBG("Received foreign light settings"); + if (!b) + lights_on[ch] = 0; + else + { + lights_on[ch] = 1; + lights_brightness[ch] = b; + lights_temperature[ch] = t; + } + update_lights(); } } - HASH_END_FOR; -} -void notify_button(int button, int on) -{ - if (pulse_state != PS_ONLINE) + if (!strcmp(topic, "burrow/lights/rainbow/brightness")) { - DBG("## NOTIFY: Pulse is not online"); - return; + double b; + if (sscanf(val, "%lf", &b) == 1 && b >= 0 && b <= 1) + { + timestamp_t now = main_get_now(); + if (!rainbow_last_update || rainbow_last_update + 1000 < now) + { + DBG("Received foreign rainbow settings"); + rainbow_brightness = b; + update_rainbow(); + } + } } - switch (button) - { - case 0: - update_sink_mute_from_button(on, "ursarium"); - break; - case 1: - update_sink_mute_from_button(on, "catarium"); - break; - default: - update_client_from_button(button, on); - } + if (!strcmp(topic, "burrow/control/catarium-ir")) + notify_ir(val); } -int main(int argc UNUSED, char **argv) +/*** Main entry point ***/ + +static int debug; +static int no_fork; + +static struct opt_section options = { + OPT_ITEMS { + OPT_HELP("Control console for the Ursary"), + OPT_HELP(""), + OPT_HELP("Options:"), + OPT_HELP_OPTION, + OPT_BOOL('d', "debug", debug, 0, "\tEnable debugging mode (no fork etc.)"), + OPT_BOOL(0, "no-fork", no_fork, 0, "\tDo not fork\n"), + OPT_END + } +}; + +static void sigterm_handler(struct main_signal *ms UNUSED) +{ + main_shut_down(); +} + +static void daemon_body(struct daemon_params *dp) { - log_init(argv[0]); main_init(); update_timer.handler = do_update; + usb_init(); noct_init(); - - msg(L_INFO, "Initializing PulseAudio"); pulse_init(); + mpd_init(); + mqtt_init(); + + static struct main_signal term_sig = { + .signum = SIGTERM, + .handler = sigterm_handler, + }; + signal_add(&term_sig); - msg(L_INFO, "Entering main loop"); + msg(L_INFO, "Ursary daemon starting"); main_loop(); + msg(L_INFO, "Ursary daemon shut down"); + daemon_exit(dp); +} + +int main(int argc UNUSED, char **argv) +{ + opt_parse(&options, argv+1); + unsetenv("DISPLAY"); + unsetenv("HOME"); + + struct daemon_params dp = { + .flags = ((debug || no_fork) ? DAEMON_FLAG_SIMULATE : 0), + .pid_file = "/run/ursaryd.pid", + .run_as_user = "ursary", + }; + daemon_init(&dp); + + log_init(argv[0]); + if (!debug) + { + struct log_stream *ls = log_new_syslog("daemon", LOG_PID); + ls->levels = ~(1U << L_DEBUG); + log_set_default_stream(ls); + } + daemon_run(&dp, daemon_body); return 0; }