]> mj.ucw.cz Git - home-hw.git/blob - rainbow/daemon/burrow-rainbowd.c
Merge branch 'master' of ssh://git.ucw.cz/home/mj/GIT/home-hw
[home-hw.git] / rainbow / daemon / burrow-rainbowd.c
1 /*
2  *      Daemon for Neopixel Rainbow Indicators over USB
3  *
4  *      (c) 2022 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <ucw/lib.h>
8 #include <ucw/clists.h>
9 #include <ucw/log.h>
10 #include <ucw/opt.h>
11 #include <ucw/string.h>
12 #include <ucw/strtonum.h>
13 #include <ucw/unaligned.h>
14
15 #include <math.h>
16 #include <stdarg.h>
17 #include <stdio.h>
18 #include <stdint.h>
19 #include <stdlib.h>
20 #include <string.h>
21 #include <syslog.h>
22 #include <threads.h>
23 #include <time.h>
24 #include <unistd.h>
25
26 #include <libusb.h>
27 #include <mosquitto.h>
28
29 typedef unsigned char byte;
30 typedef uint32_t u32;
31 typedef unsigned int uint;
32
33 #include "../firmware/interface.h"
34
35 /*** LED status ***/
36
37 struct client {
38         cnode n;
39         char *name;
40         int status;             // 0=unknown, 1=running, -1=dead
41 };
42
43 static clist clients;
44 static struct client *null_client;
45
46 static mtx_t led_mutex;
47 static cnd_t led_cond;
48 static bool led_refresh;
49
50 struct led {
51         double r, g, b;
52         struct client *sender;
53 };
54
55 static struct led leds[NPIX_NUM_LEDS];
56 static double led_brightness = 1;
57
58 static struct client *find_client(const char *name)
59 {
60         CLIST_FOR_EACH(struct client *, c, clients)
61                 if (!strcmp(c->name, name))
62                         return c;
63
64         struct client *c = xmalloc_zero(sizeof(*c));
65         clist_add_tail(&clients, &c->n);
66         c->name = xstrdup(name);
67         return c;
68 }
69
70 static void led_init(void) {
71         clist_init(&clients);
72         null_client = find_client("");
73
74         for (uint i=0; i < NPIX_NUM_LEDS; i++)
75                 leds[i].sender = null_client;
76
77         mtx_init(&led_mutex, mtx_plain);
78         cnd_init(&led_cond);
79 }
80
81 static void led_begin_update(void)
82 {
83         mtx_lock(&led_mutex);
84 }
85
86 static void led_end_update(void)
87 {
88         led_refresh = 1;
89         cnd_broadcast(&led_cond);
90         mtx_unlock(&led_mutex);
91 }
92
93 /*** MQTT ***/
94
95 static struct mosquitto *mosq;
96 static bool mqtt_connected;
97
98 static void mqtt_publish(const char *topic, const char *fmt, ...)
99 {
100         va_list args;
101         va_start(args, fmt);
102
103         if (mqtt_connected) {
104                 char m[256];
105                 int l = vsnprintf(m, sizeof(m), fmt, args);
106                 int err = mosquitto_publish(mosq, NULL, topic, l, m, 0, true);
107                 if (err != MOSQ_ERR_SUCCESS)
108                         msg(L_ERROR, "Mosquitto: Publish failed, error=%d", err);
109         }
110
111         va_end(args);
112 }
113
114 static void mqtt_conn_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int status)
115 {
116         if (!status) {
117                 msg(L_DEBUG, "MQTT: Connection established");
118                 mqtt_connected = true;
119                 if (mosquitto_subscribe(mosq, NULL, "burrow/lights/rainbow/#", 1) != MOSQ_ERR_SUCCESS)
120                         die("Mosquitto: subscribe failed");
121                 if (mosquitto_subscribe(mosq, NULL, "status/#", 1) != MOSQ_ERR_SUCCESS)
122                         die("Mosquitto: subscribe failed");
123                 mqtt_publish("status/rainbow", "ok");
124         } else if (mqtt_connected) {
125                 msg(L_DEBUG, "MQTT: Connection lost");
126                 mqtt_connected = false;
127         }
128 }
129
130 static void mqtt_log_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int level, const char *message)
131 {
132         msg(L_DEBUG, "MQTT(%d): %s", level, message);
133 }
134
135 static void msg_status(const char *topic, const char *key, const char *val)
136 {
137         if (!*key) {
138                 msg(L_ERROR, "Unknown status topic %s", topic);
139                 return;
140         }
141
142         int status;
143         if (!*val)
144                 status = 0;
145         else if (!strcmp(val, "ok"))
146                 status = 1;
147         else
148                 status = -1;
149
150         led_begin_update();
151         struct client *c = find_client(key);
152         c->status = status;
153         if (status < 0) {
154                 for (uint i=0; i < NPIX_NUM_LEDS; i++) {
155                         struct led *led = &leds[i];
156                         if (led->sender == c) {
157                                 led->r = led->g = led->b = 0;
158                                 led->sender = null_client;
159                         }
160                 }
161         }
162         led_end_update();
163 }
164
165 static void msg_brightness(const char *topic, const char *val)
166 {
167         double b = atof(val);
168         if (!(b >= 0 && b <= 1)) {
169                 msg(L_ERROR, "Invalid value of %s: %s", topic, val);
170                 return;
171         }
172
173         led_begin_update();
174         led_brightness = b;
175         led_end_update();
176 }
177
178 static void msg_rainbow(const char *topic, const char *key, const char *val)
179 {
180         if (!strcmp(key, "brightness"))
181                 return msg_brightness(topic, val);
182
183         uint index;
184         if (str_to_uint(&index, key, NULL, 10 | STN_WHOLE) || index >= NPIX_NUM_LEDS) {
185                 msg(L_ERROR, "Unknown topic: %s", topic);
186                 return;
187         }
188
189         double r, g, b;
190         char sender[strlen(val) + 1];
191         sender[0] = 0;
192
193         if (!*val) {
194                 r = g = b = 0;
195         } else if (sscanf(val, "%lf%lf%lf %s", &r, &g, &b, sender) < 3 || !(r >= 0 && r <= 1) || !(g >= 0 && g <= 1) || !(b >= 0 && b <= 1)) {
196                 msg(L_ERROR, "Invalid value of %s: %s", topic, val);
197                 return;
198         }
199
200         led_begin_update();
201         struct led *led = &leds[index];
202         struct client *client = find_client(sender);
203         if (client->status < 0) {
204                 msg(L_ERROR, "LED update from a dead client %s", client->name);
205         } else {
206                 led->r = r;
207                 led->g = g;
208                 led->b = b;
209                 led->sender = client;
210         }
211         led_end_update();
212 }
213
214 static void mqtt_msg_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, const struct mosquitto_message *m)
215 {
216         char val[256];
217         if (m->payloadlen >= sizeof(val) - 1) {
218                 msg(L_ERROR, "Invalid value for topic %s", m->topic);
219                 return;
220         }
221         memcpy(val, m->payload, m->payloadlen);
222         val[m->payloadlen] = 0;
223         msg(L_DEBUG, "MQTT < %s %s", m->topic, val);
224
225         static const char px_status[] = "status/";
226         static const char px_rainbow[] = "burrow/lights/rainbow/";
227         if (str_has_prefix(m->topic, px_status))
228                 msg_status(m->topic, m->topic + strlen(px_status), val);
229         else if (str_has_prefix(m->topic, px_rainbow))
230                 msg_rainbow(m->topic, m->topic + strlen(px_rainbow), val);
231 }
232
233 static void mqtt_init(void)
234 {
235         mosquitto_lib_init();
236
237         mosq = mosquitto_new("rainbowd", 1, NULL);
238         if (!mosq)
239                 die("Mosquitto: Initialization failed");
240
241         mosquitto_connect_callback_set(mosq, mqtt_conn_callback);
242         mosquitto_log_callback_set(mosq, mqtt_log_callback);
243         mosquitto_message_callback_set(mosq, mqtt_msg_callback);
244
245         if (mosquitto_will_set(mosq, "status/rainbow", 4, "dead", 0, true) != MOSQ_ERR_SUCCESS)
246                 die("Mosquitto: Unable to set will");
247
248         if (mosquitto_tls_set(mosq, "/etc/burrow-mqtt/ca.crt", NULL, "/etc/burrow-mqtt/client.crt", "/etc/burrow-mqtt/client.key", NULL) != MOSQ_ERR_SUCCESS)
249                 die("Mosquitto: Unable to set TLS parameters");
250
251         if (mosquitto_connect_async(mosq, "burrow-mqtt", 8883, 60) != MOSQ_ERR_SUCCESS)
252                 die("Mosquitto: Unable to connect");
253
254         if (mosquitto_loop_start(mosq))
255                 die("Mosquitto: Cannot start service thread");
256 }
257
258 /*** USB ***/
259
260 static struct libusb_context *usb_ctxt;
261 static struct libusb_device_handle *devh;
262
263 static void usb_error(const char *msg, ...)
264 {
265         va_list args;
266         va_start(args, msg);
267         ucw_vmsg(L_ERROR, msg, args);
268         va_end(args);
269
270         if (devh) {
271                 libusb_close(devh);
272                 devh = NULL;
273         }
274 }
275
276 static void open_device(void)
277 {
278         int err;
279         libusb_device **devlist;
280         ssize_t devn = libusb_get_device_list(usb_ctxt, &devlist);
281         if (devn < 0)
282                 die("Cannot enumerate USB devices: error %d", (int) devn);
283
284         for (ssize_t i=0; i<devn; i++) {
285                 struct libusb_device_descriptor desc;
286                 libusb_device *dev = devlist[i];
287                 if (!libusb_get_device_descriptor(dev, &desc)) {
288                         if (desc.idVendor == NPIX_USB_VENDOR && desc.idProduct == NPIX_USB_PRODUCT) {
289                                 msg(L_INFO, "Found NPIX device at usb%d.%d", libusb_get_bus_number(dev), libusb_get_device_address(dev));
290
291                                 if (err = libusb_open(dev, &devh)) {
292                                         usb_error("Cannot open device: error %d", err);
293                                         goto out;
294                                 }
295                                 libusb_reset_device(devh);
296                                 if (err = libusb_claim_interface(devh, 0)) {
297                                         usb_error("Cannot claim interface: error %d", err);
298                                         goto out;
299                                 }
300
301                                 goto out;
302                         }
303                 }
304         }
305
306 out:
307         libusb_free_device_list(devlist, 1);
308 }
309
310 static void init_usb(void)
311 {
312         int err;
313         if (err = libusb_init(&usb_ctxt))
314                 die("Cannot initialize libusb: error %d", err);
315         // libusb_set_debug(usb_ctxt, 3);
316         open_device();
317 }
318
319 /*** NPIX ***/
320
321 static byte npix_packet[3*NPIX_NUM_LEDS];
322
323 static int npix_build_packet(void)
324 {
325         byte *pkt = npix_packet;
326
327         for (int i=0; i < NPIX_NUM_LEDS; i++) {
328                 struct led *led = &leds[i];
329                 struct client *sender = led->sender;
330                 msg(L_DEBUG, "LED #%d: r=%.3f g=%.3f b=%.3f client=%s status=%d", i, led->r, led->g, led->b, sender->name, sender->status);
331                 *pkt++ = (int)(led->r * led_brightness * 255);
332                 *pkt++ = (int)(led->g * led_brightness * 255);
333                 *pkt++ = (int)(led->b * led_brightness * 255);
334         }
335
336         return pkt - npix_packet;
337 }
338
339 /*** Main ***/
340
341 static int use_daemon;
342 static int use_debug;
343
344 static struct opt_section options = {
345         OPT_ITEMS {
346                 OPT_HELP("A daemon for controlling lights via NPIX512"),
347                 OPT_HELP(""),
348                 OPT_HELP("Options:"),
349                 OPT_BOOL('d', "debug", use_debug, 0, "\tLog debugging messages"),
350                 OPT_BOOL(0, "daemon", use_daemon, 0, "\tDaemonize"),
351                 OPT_HELP_OPTION,
352                 OPT_CONF_OPTIONS,
353                 OPT_END
354         }
355 };
356
357 int main(int argc UNUSED, char **argv)
358 {
359         log_init(argv[0]);
360         opt_parse(&options, argv+1);
361
362         if (use_daemon) {
363                 struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
364                 log_set_default_stream(ls);
365         }
366         if (!use_debug)
367                 log_default_stream()->levels &= ~(1U << L_DEBUG);
368
369         led_init();
370         mqtt_init();
371         init_usb();
372
373         bool need_resend = true;
374         for (;;) {
375                 if (!devh) {
376                         msg(L_INFO, "Waiting for device to appear...");
377                         while (!devh) {
378                                 sleep(5);
379                                 open_device();
380                         }
381                         need_resend = true;
382                 }
383
384                 mtx_lock(&led_mutex);
385                 while (!need_resend && !led_refresh)
386                         cnd_wait(&led_cond, &led_mutex);
387
388                 int len = npix_build_packet();
389                 led_refresh = 0;
390                 need_resend = 0;
391                 mtx_unlock(&led_mutex);
392
393                 msg(L_DEBUG, "Sending NPIX packet");
394
395                 int err, transferred;
396                 if (err = libusb_bulk_transfer(devh, 0x01, npix_packet, len, &transferred, 1000))
397                         usb_error("USB transfer failed: error %d", err);
398                 else if (transferred != len)
399                         usb_error("USB short transfer: %d out of %d bytes", transferred, len);
400         }
401 }