--- /dev/null
+/*
+ * Daemon for Neopixel Indicators over USB
+ *
+ * (c) 2022 Martin Mares <mj@ucw.cz>
+ */
+
+#include <ucw/lib.h>
+#include <ucw/log.h>
+#include <ucw/opt.h>
+#include <ucw/string.h>
+#include <ucw/strtonum.h>
+#include <ucw/unaligned.h>
+
+#include <math.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <threads.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <libusb.h>
+#include <mosquitto.h>
+
+typedef unsigned char byte;
+typedef uint32_t u32;
+typedef unsigned int uint;
+
+#include "../firmware/interface.h"
+
+static mtx_t led_mutex;
+static cnd_t led_cond;
+static bool led_refresh;
+
+struct led {
+ byte r, g, b;
+};
+
+static struct led leds[NPIX_NUM_LEDS];
+
+/*** MQTT ***/
+
+static struct mosquitto *mosq;
+static bool mqtt_connected;
+
+static void mqtt_publish(const char *topic, const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+
+ if (mqtt_connected) {
+ char m[256];
+ int l = vsnprintf(m, sizeof(m), fmt, args);
+ int err = mosquitto_publish(mosq, NULL, topic, l, m, 0, true);
+ if (err != MOSQ_ERR_SUCCESS)
+ msg(L_ERROR, "Mosquitto: Publish failed, error=%d", err);
+ }
+
+ va_end(args);
+}
+
+static void mqtt_conn_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int status)
+{
+ if (!status) {
+ msg(L_DEBUG, "MQTT: Connection established");
+ mqtt_connected = true;
+ mqtt_publish("status/rainbow", "ok");
+ if (mosquitto_subscribe(mosq, NULL, "burrow/lights/rainbow/#", 1) != MOSQ_ERR_SUCCESS)
+ die("Mosquitto: subscribe failed");
+ } else if (mqtt_connected) {
+ msg(L_DEBUG, "MQTT: Connection lost");
+ mqtt_connected = false;
+ }
+}
+
+static void mqtt_log_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int level, const char *message)
+{
+ msg(L_DEBUG, "MQTT(%d): %s", level, message);
+}
+
+static void mqtt_msg_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, const struct mosquitto_message *m)
+{
+ char val[256];
+ if (m->payloadlen >= sizeof(val) - 1) {
+ msg(L_ERROR, "Invalid value for topic %s", m->topic);
+ return;
+ }
+ memcpy(val, m->payload, m->payloadlen);
+ val[m->payloadlen] = 0;
+ msg(L_DEBUG, "MQTT < %s %s", m->topic, val);
+
+ static const char px[] = "burrow/lights/rainbow/";
+ if (!str_has_prefix(m->topic, px))
+ return;
+
+ uint index;
+ if (str_to_uint(&index, m->topic + strlen(px), NULL, STN_DEC | STN_WHOLE) || index >= NPIX_NUM_LEDS) {
+ msg(L_ERROR, "Unsupported topic: %s", m->topic);
+ return;
+ }
+
+ uint r, g, b;
+ if (sscanf(val, "%u%u%u", &r, &g, &b) != 3 || r >= 256 || g >= 256 || b >= 256) {
+ msg(L_ERROR, "Invalid value of %s: %s", m->topic, val);
+ return;
+ }
+
+ mtx_lock(&led_mutex);
+ leds[index].r = r;
+ leds[index].g = g;
+ leds[index].b = b;
+ led_refresh = 1;
+ cnd_broadcast(&led_cond);
+ mtx_unlock(&led_mutex);
+}
+
+static void mqtt_init(void)
+{
+ mosquitto_lib_init();
+
+ mosq = mosquitto_new("rainbowd", 1, NULL);
+ if (!mosq)
+ die("Mosquitto: Initialization failed");
+
+ mosquitto_connect_callback_set(mosq, mqtt_conn_callback);
+ mosquitto_log_callback_set(mosq, mqtt_log_callback);
+ mosquitto_message_callback_set(mosq, mqtt_msg_callback);
+
+ if (mosquitto_will_set(mosq, "status/rainbow", 4, "dead", 0, true) != MOSQ_ERR_SUCCESS)
+ die("Mosquitto: Unable to set will");
+
+ 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)
+ die("Mosquitto: Unable to set TLS parameters");
+
+ if (mosquitto_connect_async(mosq, "burrow-mqtt", 8883, 60) != MOSQ_ERR_SUCCESS)
+ die("Mosquitto: Unable to connect");
+
+ if (mosquitto_loop_start(mosq))
+ die("Mosquitto: Cannot start service thread");
+}
+
+/*** USB ***/
+
+static struct libusb_context *usb_ctxt;
+static struct libusb_device_handle *devh;
+
+static void usb_error(const char *msg, ...)
+{
+ va_list args;
+ va_start(args, msg);
+ ucw_vmsg(L_ERROR, msg, args);
+ va_end(args);
+
+ if (devh) {
+ libusb_close(devh);
+ devh = NULL;
+ }
+}
+
+static void open_device(void)
+{
+ int err;
+ libusb_device **devlist;
+ ssize_t devn = libusb_get_device_list(usb_ctxt, &devlist);
+ if (devn < 0)
+ die("Cannot enumerate USB devices: error %d", (int) devn);
+
+ for (ssize_t i=0; i<devn; i++) {
+ struct libusb_device_descriptor desc;
+ libusb_device *dev = devlist[i];
+ if (!libusb_get_device_descriptor(dev, &desc)) {
+ if (desc.idVendor == NPIX_USB_VENDOR && desc.idProduct == NPIX_USB_PRODUCT) {
+ msg(L_INFO, "Found NPIX device at usb%d.%d", libusb_get_bus_number(dev), libusb_get_device_address(dev));
+
+ if (err = libusb_open(dev, &devh)) {
+ usb_error("Cannot open device: error %d", err);
+ goto out;
+ }
+ libusb_reset_device(devh);
+ if (err = libusb_claim_interface(devh, 0)) {
+ usb_error("Cannot claim interface: error %d", err);
+ goto out;
+ }
+
+ goto out;
+ }
+ }
+ }
+
+out:
+ libusb_free_device_list(devlist, 1);
+}
+
+static void init_usb(void)
+{
+ int err;
+ if (err = libusb_init(&usb_ctxt))
+ die("Cannot initialize libusb: error %d", err);
+ // libusb_set_debug(usb_ctxt, 3);
+ open_device();
+}
+
+/*** NPIX ***/
+
+static byte npix_packet[3*NPIX_NUM_LEDS];
+
+static int npix_build_packet(void)
+{
+ byte *pkt = npix_packet;
+
+ for (int i=0; i < NPIX_NUM_LEDS; i++) {
+ *pkt++ = leds[i].r;
+ *pkt++ = leds[i].g;
+ *pkt++ = leds[i].b;
+ }
+
+ return sizeof(npix_packet);
+}
+
+/*** Main ***/
+
+static int use_daemon;
+static int use_debug;
+
+static struct opt_section options = {
+ OPT_ITEMS {
+ OPT_HELP("A daemon for controlling lights via NPIX512"),
+ OPT_HELP(""),
+ OPT_HELP("Options:"),
+ OPT_BOOL('d', "debug", use_debug, 0, "\tLog debugging messages"),
+ OPT_BOOL(0, "daemon", use_daemon, 0, "\tDaemonize"),
+ OPT_HELP_OPTION,
+ OPT_CONF_OPTIONS,
+ OPT_END
+ }
+};
+
+int main(int argc UNUSED, char **argv)
+{
+ log_init(argv[0]);
+ opt_parse(&options, argv+1);
+
+ if (use_daemon) {
+ struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
+ log_set_default_stream(ls);
+ }
+ if (!use_debug)
+ log_default_stream()->levels &= ~(1U << L_DEBUG);
+
+ mtx_init(&led_mutex, mtx_plain);
+ cnd_init(&led_cond);
+
+ mqtt_init();
+ init_usb();
+
+ bool need_resend = true;
+ for (;;) {
+ if (!devh) {
+ msg(L_INFO, "Waiting for device to appear...");
+ while (!devh) {
+ sleep(5);
+ open_device();
+ }
+ need_resend = true;
+ }
+
+ mtx_lock(&led_mutex);
+ while (!need_resend && !led_refresh)
+ cnd_wait(&led_cond, &led_mutex);
+
+ int len = npix_build_packet();
+ led_refresh = 0;
+ need_resend = 0;
+ mtx_unlock(&led_mutex);
+
+ msg(L_DEBUG, "Sending NPIX packet");
+
+ int err, transferred;
+ if (err = libusb_bulk_transfer(devh, 0x01, npix_packet, len, &transferred, 1000))
+ usb_error("USB transfer failed: error %d", err);
+ else if (transferred != len)
+ usb_error("USB short transfer: %d out of %d bytes", transferred, len);
+ }
+}