]> mj.ucw.cz Git - home-hw.git/blob - rainbow/daemon/burrow-rainbowd.c
Rainbow: All settings are floats, added global brightness
[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/log.h>
9 #include <ucw/opt.h>
10 #include <ucw/string.h>
11 #include <ucw/strtonum.h>
12 #include <ucw/unaligned.h>
13
14 #include <math.h>
15 #include <stdarg.h>
16 #include <stdio.h>
17 #include <stdint.h>
18 #include <stdlib.h>
19 #include <string.h>
20 #include <syslog.h>
21 #include <threads.h>
22 #include <time.h>
23 #include <unistd.h>
24
25 #include <libusb.h>
26 #include <mosquitto.h>
27
28 typedef unsigned char byte;
29 typedef uint32_t u32;
30 typedef unsigned int uint;
31
32 #include "../firmware/interface.h"
33
34 static mtx_t led_mutex;
35 static cnd_t led_cond;
36 static bool led_refresh;
37
38 struct led {
39     double r, g, b;
40 };
41
42 static struct led leds[NPIX_NUM_LEDS];
43 static double led_brightness = 1;
44
45 /*** MQTT ***/
46
47 static struct mosquitto *mosq;
48 static bool mqtt_connected;
49
50 static void mqtt_publish(const char *topic, const char *fmt, ...)
51 {
52         va_list args;
53         va_start(args, fmt);
54
55         if (mqtt_connected) {
56                 char m[256];
57                 int l = vsnprintf(m, sizeof(m), fmt, args);
58                 int err = mosquitto_publish(mosq, NULL, topic, l, m, 0, true);
59                 if (err != MOSQ_ERR_SUCCESS)
60                         msg(L_ERROR, "Mosquitto: Publish failed, error=%d", err);
61         }
62
63         va_end(args);
64 }
65
66 static void mqtt_conn_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int status)
67 {
68         if (!status) {
69                 msg(L_DEBUG, "MQTT: Connection established");
70                 mqtt_connected = true;
71                 mqtt_publish("status/rainbow", "ok");
72                 if (mosquitto_subscribe(mosq, NULL, "burrow/lights/rainbow/#", 1) != MOSQ_ERR_SUCCESS)
73                         die("Mosquitto: subscribe failed");
74         } else if (mqtt_connected) {
75                 msg(L_DEBUG, "MQTT: Connection lost");
76                 mqtt_connected = false;
77         }
78 }
79
80 static void mqtt_log_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int level, const char *message)
81 {
82         msg(L_DEBUG, "MQTT(%d): %s", level, message);
83 }
84
85 static void mqtt_msg_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, const struct mosquitto_message *m)
86 {
87         char val[256];
88         if (m->payloadlen >= sizeof(val) - 1) {
89                 msg(L_ERROR, "Invalid value for topic %s", m->topic);
90                 return;
91         }
92         memcpy(val, m->payload, m->payloadlen);
93         val[m->payloadlen] = 0;
94         msg(L_DEBUG, "MQTT < %s %s", m->topic, val);
95
96         static const char px[] = "burrow/lights/rainbow/";
97         if (!str_has_prefix(m->topic, px))
98                 return;
99
100         const char *top = m->topic + strlen(px);
101         if (!strcmp(top, "brightness")) {
102                 double b = atof(val);
103                 if (!(b >= 0 && b <= 1)) {
104                         msg(L_ERROR, "Invalid value of %s: %s", m->topic, val);
105                         return;
106                 }
107                 mtx_lock(&led_mutex);
108                 led_brightness = b;
109                 led_refresh = 1;
110                 cnd_broadcast(&led_cond);
111                 mtx_unlock(&led_mutex);
112                 return;
113         }
114
115         uint index;
116         if (str_to_uint(&index, m->topic + strlen(px), NULL, 10 | STN_WHOLE) || index >= NPIX_NUM_LEDS) {
117                 msg(L_ERROR, "Unsupported topic: %s", m->topic);
118                 return;
119         }
120
121         double r, g, b;
122         if (sscanf(val, "%lf%lf%lf", &r, &g, &b) != 3 || !(r >= 0 && r <= 1) || !(g >= 0 && g <= 1) || !(b >= 0 && b <= 1)) {
123                 msg(L_ERROR, "Invalid value of %s: %s", m->topic, val);
124                 return;
125         }
126
127         mtx_lock(&led_mutex);
128         leds[index].r = r;
129         leds[index].g = g;
130         leds[index].b = b;
131         led_refresh = 1;
132         cnd_broadcast(&led_cond);
133         mtx_unlock(&led_mutex);
134 }
135
136 static void mqtt_init(void)
137 {
138         mosquitto_lib_init();
139
140         mosq = mosquitto_new("rainbowd", 1, NULL);
141         if (!mosq)
142                 die("Mosquitto: Initialization failed");
143
144         mosquitto_connect_callback_set(mosq, mqtt_conn_callback);
145         mosquitto_log_callback_set(mosq, mqtt_log_callback);
146         mosquitto_message_callback_set(mosq, mqtt_msg_callback);
147
148         if (mosquitto_will_set(mosq, "status/rainbow", 4, "dead", 0, true) != MOSQ_ERR_SUCCESS)
149                 die("Mosquitto: Unable to set will");
150
151         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)
152                 die("Mosquitto: Unable to set TLS parameters");
153
154         if (mosquitto_connect_async(mosq, "burrow-mqtt", 8883, 60) != MOSQ_ERR_SUCCESS)
155                 die("Mosquitto: Unable to connect");
156
157         if (mosquitto_loop_start(mosq))
158                 die("Mosquitto: Cannot start service thread");
159 }
160
161 /*** USB ***/
162
163 static struct libusb_context *usb_ctxt;
164 static struct libusb_device_handle *devh;
165
166 static void usb_error(const char *msg, ...)
167 {
168         va_list args;
169         va_start(args, msg);
170         ucw_vmsg(L_ERROR, msg, args);
171         va_end(args);
172
173         if (devh) {
174                 libusb_close(devh);
175                 devh = NULL;
176         }
177 }
178
179 static void open_device(void)
180 {
181         int err;
182         libusb_device **devlist;
183         ssize_t devn = libusb_get_device_list(usb_ctxt, &devlist);
184         if (devn < 0)
185                 die("Cannot enumerate USB devices: error %d", (int) devn);
186
187         for (ssize_t i=0; i<devn; i++) {
188                 struct libusb_device_descriptor desc;
189                 libusb_device *dev = devlist[i];
190                 if (!libusb_get_device_descriptor(dev, &desc)) {
191                         if (desc.idVendor == NPIX_USB_VENDOR && desc.idProduct == NPIX_USB_PRODUCT) {
192                                 msg(L_INFO, "Found NPIX device at usb%d.%d", libusb_get_bus_number(dev), libusb_get_device_address(dev));
193
194                                 if (err = libusb_open(dev, &devh)) {
195                                         usb_error("Cannot open device: error %d", err);
196                                         goto out;
197                                 }
198                                 libusb_reset_device(devh);
199                                 if (err = libusb_claim_interface(devh, 0)) {
200                                         usb_error("Cannot claim interface: error %d", err);
201                                         goto out;
202                                 }
203
204                                 goto out;
205                         }
206                 }
207         }
208
209 out:
210         libusb_free_device_list(devlist, 1);
211 }
212
213 static void init_usb(void)
214 {
215         int err;
216         if (err = libusb_init(&usb_ctxt))
217                 die("Cannot initialize libusb: error %d", err);
218         // libusb_set_debug(usb_ctxt, 3);
219         open_device();
220 }
221
222 /*** NPIX ***/
223
224 static byte npix_packet[3*NPIX_NUM_LEDS];
225
226 static int npix_build_packet(void)
227 {
228         byte *pkt = npix_packet;
229
230         for (int i=0; i < NPIX_NUM_LEDS; i++) {
231                 *pkt++ = (int)(leds[i].r * led_brightness * 255);
232                 *pkt++ = (int)(leds[i].g * led_brightness * 255);
233                 *pkt++ = (int)(leds[i].b * led_brightness * 255);
234         }
235
236         return pkt - npix_packet;
237 }
238
239 /*** Main ***/
240
241 static int use_daemon;
242 static int use_debug;
243
244 static struct opt_section options = {
245         OPT_ITEMS {
246                 OPT_HELP("A daemon for controlling lights via NPIX512"),
247                 OPT_HELP(""),
248                 OPT_HELP("Options:"),
249                 OPT_BOOL('d', "debug", use_debug, 0, "\tLog debugging messages"),
250                 OPT_BOOL(0, "daemon", use_daemon, 0, "\tDaemonize"),
251                 OPT_HELP_OPTION,
252                 OPT_CONF_OPTIONS,
253                 OPT_END
254         }
255 };
256
257 int main(int argc UNUSED, char **argv)
258 {
259         log_init(argv[0]);
260         opt_parse(&options, argv+1);
261
262         if (use_daemon) {
263                 struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
264                 log_set_default_stream(ls);
265         }
266         if (!use_debug)
267                 log_default_stream()->levels &= ~(1U << L_DEBUG);
268
269         mtx_init(&led_mutex, mtx_plain);
270         cnd_init(&led_cond);
271
272         mqtt_init();
273         init_usb();
274
275         bool need_resend = true;
276         for (;;) {
277                 if (!devh) {
278                         msg(L_INFO, "Waiting for device to appear...");
279                         while (!devh) {
280                                 sleep(5);
281                                 open_device();
282                         }
283                         need_resend = true;
284                 }
285
286                 mtx_lock(&led_mutex);
287                 while (!need_resend && !led_refresh)
288                         cnd_wait(&led_cond, &led_mutex);
289
290                 int len = npix_build_packet();
291                 led_refresh = 0;
292                 need_resend = 0;
293                 mtx_unlock(&led_mutex);
294
295                 msg(L_DEBUG, "Sending NPIX packet");
296
297                 int err, transferred;
298                 if (err = libusb_bulk_transfer(devh, 0x01, npix_packet, len, &transferred, 1000))
299                         usb_error("USB transfer failed: error %d", err);
300                 else if (transferred != len)
301                         usb_error("USB short transfer: %d out of %d bytes", transferred, len);
302         }
303 }