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