aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorcbdev <cb@cbcdn.com>2020-02-23 18:20:12 +0100
committercbdev <cb@cbcdn.com>2020-02-23 18:20:12 +0100
commitdd91621ccee033550312683293b5bf40c3599053 (patch)
treeb56199bf974a4ff2e65397e4eb0f3a40c65d5740
parentb529f4d3a9efc59c6bec46e07ebf1a114b7a0831 (diff)
downloadmidimonster-dd91621ccee033550312683293b5bf40c3599053.tar.gz
midimonster-dd91621ccee033550312683293b5bf40c3599053.tar.bz2
midimonster-dd91621ccee033550312683293b5bf40c3599053.zip
Implement OpenPixelControl output
-rw-r--r--backends/Makefile11
-rw-r--r--backends/openpixelcontrol.c353
-rw-r--r--backends/openpixelcontrol.h48
-rw-r--r--backends/openpixelcontrol.md49
4 files changed, 459 insertions, 2 deletions
diff --git a/backends/Makefile b/backends/Makefile
index 656e6b6..191a495 100644
--- a/backends/Makefile
+++ b/backends/Makefile
@@ -1,7 +1,7 @@
.PHONY: all clean full
LINUX_BACKENDS = midi.so evdev.so
-WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll
-BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so
+WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll
+BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so
OPTIONAL_BACKENDS = ola.so
BACKEND_LIB = libmmbackend.o
@@ -36,6 +36,10 @@ sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
sacn.dll: LDLIBS += -lws2_32
+openpixelcontrol.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
+openpixelcontrol.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
+openpixelcontrol.dll: LDLIBS += -lws2_32
+
maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
maweb.so: LDLIBS = -lssl
maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
@@ -59,6 +63,9 @@ lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 ||
lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"")
lua.dll: LDLIBS += -L../libs -llua53
+python.so: CFLAGS += $(shell pkg-config --cflags python3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"")
+python.so: CFLAGS += $(shell pkg-config --libs python3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"")
+
%.so :: %.c %.h $(BACKEND_LIB)
$(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS)
diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c
new file mode 100644
index 0000000..062c015
--- /dev/null
+++ b/backends/openpixelcontrol.c
@@ -0,0 +1,353 @@
+#define BACKEND_NAME "openpixelcontrol"
+
+#include <string.h>
+
+#include "libmmbackend.h"
+#include "openpixelcontrol.h"
+
+/*
+ * TODO handle destination close/unregister/reopen
+ */
+
+MM_PLUGIN_API int init(){
+ backend openpixel = {
+ .name = BACKEND_NAME,
+ .conf = openpixel_configure,
+ .create = openpixel_instance,
+ .conf_instance = openpixel_configure_instance,
+ .channel = openpixel_channel,
+ .handle = openpixel_set,
+ .process = openpixel_handle,
+ .start = openpixel_start,
+ .shutdown = openpixel_shutdown
+ };
+
+ //register backend
+ if(mm_backend_register(openpixel)){
+ LOG("Failed to register backend");
+ return 1;
+ }
+ return 0;
+}
+
+static int openpixel_configure(char* option, char* value){
+ //no global configuration
+ LOG("No backend configuration possible");
+ return 1;
+}
+
+static int openpixel_configure_instance(instance* inst, char* option, char* value){
+ char* host = NULL, *port = NULL;
+ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl;
+
+ //FIXME this should store the destination/listen address and establish on _start
+ if(!strcmp(option, "destination")){
+ mmbackend_parse_hostspec(value, &host, &port, NULL);
+ if(!host || !port){
+ LOGPF("Invalid destination address specified for instance %s", inst->name);
+ return 1;
+ }
+
+ data->dest_fd = mmbackend_socket(host, port, SOCK_STREAM, 0, 0);
+ if(data->dest_fd >= 0){
+ return 0;
+ }
+ return 1;
+ }
+ if(!strcmp(option, "listen")){
+ mmbackend_parse_hostspec(value, &host, &port, NULL);
+ if(!host || !port){
+ LOGPF("Invalid listen address specified for instance %s", inst->name);
+ return 1;
+ }
+
+ data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0);
+ if(data->listen_fd >= 0 && listen(data->listen_fd, SOMAXCONN)){
+ return 0;
+ }
+ return 1;
+ }
+ else if(!strcmp(option, "mode")){
+ if(!strcmp(value, "16bit")){
+ data->mode = rgb16;
+ return 0;
+ }
+ else if(!strcmp(value, "8bit")){
+ data->mode = rgb8;
+ return 0;
+ }
+ LOGPF("Unknown instance mode %s\n", value);
+ return 1;
+ }
+
+ LOGPF("Unknown instance option %s for instance %s", option, inst->name);
+ return 1;
+}
+
+static int openpixel_instance(instance* inst){
+ openpixel_instance_data* data = calloc(1, sizeof(openpixel_instance_data));
+ inst->impl = data;
+ if(!inst->impl){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ data->dest_fd = -1;
+ data->listen_fd = -1;
+ return 0;
+}
+
+static ssize_t openpixel_buffer_find(openpixel_instance_data* data, uint8_t strip, uint8_t input){
+ ssize_t n = 0;
+
+ for(n = 0; n < data->buffers; n++){
+ if(data->buffer[n].strip == strip
+ && (data->buffer[n].flags & OPENPIXEL_INPUT) >= input){
+ return n;
+ }
+ }
+ return -1;
+}
+
+static int openpixel_buffer_extend(openpixel_instance_data* data, uint8_t strip, uint8_t input, uint8_t length){
+ ssize_t buffer = openpixel_buffer_find(data, strip, input);
+ length = (length % 3) ? ((length / 3) + 1) * 3 : length;
+ size_t bytes_required = (data->mode == rgb8) ? length : length * 2;
+ if(buffer < 0){
+ //allocate new buffer
+ data->buffer = realloc(data->buffer, (data->buffers + 1) * sizeof(openpixel_buffer));
+ if(!data->buffer){
+ data->buffers = 0;
+ LOG("Failed to allocate memory");
+ return -1;
+ }
+
+ buffer = data->buffers;
+ data->buffers++;
+
+ data->buffer[buffer].strip = strip;
+ data->buffer[buffer].flags = input ? OPENPIXEL_INPUT : 0;
+ data->buffer[buffer].bytes = 0;
+ data->buffer[buffer].data.u8 = NULL;
+ }
+
+ if(data->buffer[buffer].bytes < bytes_required){
+ //resize buffer
+ data->buffer[buffer].data.u8 = realloc(data->buffer[buffer].data.u8, bytes_required);
+ if(!data->buffer[buffer].data.u8){
+ data->buffer[buffer].bytes = 0;
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ //FIXME might want to memset() only newly allocated channels
+ memset(data->buffer[buffer].data.u8, 0, bytes_required);
+ data->buffer[buffer].bytes = bytes_required;
+ }
+ return 0;
+}
+
+static channel* openpixel_channel(instance* inst, char* spec, uint8_t flags){
+ uint32_t strip = 0, channel = 0;
+ char* token = spec;
+ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl;
+
+ //read strip index if supplied
+ if(!strncmp(spec, "strip", 5)){
+ strip = strtoul(spec + 5, &token, 10);
+ //skip the dot
+ token++;
+ }
+
+ //read (and calculate) channel index
+ if(!strncmp(token, "channel", 7)){
+ channel = strtoul(token + 7, NULL, 10);
+ }
+ else if(!strncmp(token, "red", 3)){
+ channel = strtoul(token + 3, NULL, 10) * 3 - 2;
+ }
+ else if(!strncmp(token, "green", 5)){
+ channel = strtoul(token + 5, NULL, 10) * 3 - 1;
+ }
+ else if(!strncmp(token, "blue", 4)){
+ channel = strtoul(token + 4, NULL, 10) * 3;
+ }
+
+ if(!channel){
+ LOGPF("Invalid channel specification %s", spec);
+ return NULL;
+ }
+
+ //check channel direction
+ if(flags & mmchannel_input){
+ //strip 0 (bcast) can not be mapped as input
+ if(!strip){
+ LOGPF("Broadcast channel %s.%s can not be mapped as an input", inst->name, spec);
+ return NULL;
+ }
+ if(data->listen_fd < 0){
+ LOGPF("Channel %s mapped as input, but instance %s is not accepting input", spec, inst->name);
+ return NULL;
+ }
+
+ if(openpixel_buffer_extend(data, strip, 1, channel)){
+ return NULL;
+ }
+ }
+
+ if(flags & mmchannel_output){
+ if(data->dest_fd < 0){
+ LOGPF("Channel %s mapped as output, but instance %s is not sending output", spec, inst->name);
+ return NULL;
+ }
+
+ if(openpixel_buffer_extend(data, strip, 0, channel)){
+ return NULL;
+ }
+ }
+
+ return mm_channel(inst, ((uint64_t) strip) << 32 | channel, 1);
+}
+
+static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* v){
+ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl;
+ size_t u, p;
+ ssize_t buffer;
+ uint32_t strip, channel;
+ openpixel_header hdr;
+
+ for(u = 0; u < num; u++){
+ //read strip/channel
+ strip = c[u]->ident >> 32;
+ channel = c[u]->ident & 0xFFFFFFFF;
+ channel--;
+
+ //find the buffer
+ buffer = openpixel_buffer_find(data, strip, 0);
+ if(buffer < 0){
+ LOGPF("No buffer for channel %s.%d.%d\n", inst->name, strip, channel);
+ continue;
+ }
+
+ //mark buffer for output
+ data->buffer[buffer].flags |= OPENPIXEL_MARK;
+
+ //update data
+ switch(data->mode){
+ case rgb8:
+ data->buffer[buffer].data.u8[channel] = ((uint8_t)(v[u].normalised * 255.0));
+ break;
+ case rgb16:
+ data->buffer[buffer].data.u16[channel] = ((uint16_t)(v[u].normalised * 65535.0));
+ break;
+ }
+
+ if(strip == 0){
+ //update values in all other output strips, dont mark
+ for(p = 0; p < data->buffers; p++){
+ if(!(data->buffer[p].flags & OPENPIXEL_INPUT)){
+ //check whether the buffer is large enough
+ if(data->mode == rgb8 && data->buffer[p].bytes >= channel){
+ data->buffer[p].data.u8[channel] = ((uint8_t)(v[u].normalised * 255.0));
+ }
+ else if(data->mode == rgb16 && data->buffer[p].bytes >= channel * 2){
+ data->buffer[p].data.u16[channel] = ((uint16_t)(v[u].normalised * 65535.0));
+ }
+ }
+ }
+ }
+ }
+
+ //send updated strips
+ for(u = 0; u < data->buffers; u++){
+ if(!(data->buffer[u].flags & OPENPIXEL_INPUT) && (data->buffer[u].flags & OPENPIXEL_MARK)){
+ //remove mark
+ data->buffer[u].flags &= ~OPENPIXEL_MARK;
+
+ //prepare header
+ hdr.strip = data->buffer[u].strip;
+ hdr.mode = data->mode;
+ hdr.length = htobe16(data->buffer[u].bytes);
+
+ //output data
+ if(mmbackend_send(data->dest_fd, (uint8_t*) &hdr, sizeof(hdr))
+ || mmbackend_send(data->dest_fd, data->buffer[u].data.u8, data->buffer[u].bytes)){
+ return 1;
+ }
+ }
+ }
+ return 0;
+}
+
+static int openpixel_handle(size_t num, managed_fd* fds){
+ //TODO handle bcast
+ return 0;
+}
+
+static int openpixel_start(size_t n, instance** inst){
+ int rv = -1;
+ size_t u, nfds = 0;
+ openpixel_instance_data* data = NULL;
+
+ for(u = 0; u < n; u++){
+ data = (openpixel_instance_data*) inst[u]->impl;
+
+ //register fds
+ if(data->dest_fd >= 0){
+ if(mm_manage_fd(data->dest_fd, BACKEND_NAME, 1, inst[u])){
+ LOGPF("Failed to register destination descriptor for instance %s with core", inst[u]->name);
+ goto bail;
+ }
+ nfds++;
+ }
+ if(data->listen_fd >= 0){
+ if(mm_manage_fd(data->listen_fd, BACKEND_NAME, 1, inst[u])){
+ LOGPF("Failed to register host descriptor for instance %s with core", inst[u]->name);
+ goto bail;
+ }
+ nfds++;
+ }
+ }
+
+ LOGPF("Registered %" PRIsize_t " descriptors to core", nfds);
+ rv = 0;
+bail:
+ return rv;
+}
+
+static int openpixel_shutdown(size_t n, instance** inst){
+ size_t u, p;
+ openpixel_instance_data* data = NULL;
+
+ for(u = 0; u < n; u++){
+ data = (openpixel_instance_data*) inst[u]->impl;
+
+ //shutdown all clients
+ for(p = 0; p < data->clients; p++){
+ if(data->client_fd[p] >= 0){
+ close(data->client_fd[p]);
+ }
+ }
+ free(data->client_fd);
+ free(data->bytes_left);
+
+ //close all configured fds
+ if(data->listen_fd >= 0){
+ close(data->listen_fd);
+ }
+ if(data->dest_fd >= 0){
+ close(data->dest_fd);
+ }
+
+ //free all buffers
+ for(p = 0; p < data->buffers; p++){
+ free(data->buffer[p].data.u8);
+ }
+ free(data->buffer);
+
+ free(data);
+ inst[u]->impl = NULL;
+ }
+
+ LOG("Backend shut down");
+ return 0;
+}
diff --git a/backends/openpixelcontrol.h b/backends/openpixelcontrol.h
new file mode 100644
index 0000000..f1061ea
--- /dev/null
+++ b/backends/openpixelcontrol.h
@@ -0,0 +1,48 @@
+#include "midimonster.h"
+
+MM_PLUGIN_API int init();
+static int openpixel_configure(char* option, char* value);
+static int openpixel_configure_instance(instance* inst, char* option, char* value);
+static int openpixel_instance(instance* inst);
+static channel* openpixel_channel(instance* inst, char* spec, uint8_t flags);
+static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int openpixel_handle(size_t num, managed_fd* fds);
+static int openpixel_start(size_t n, instance** inst);
+static int openpixel_shutdown(size_t n, instance** inst);
+
+#define OPENPIXEL_INPUT 1
+#define OPENPIXEL_MARK 2
+
+typedef struct /*_data_buffer*/ {
+ uint8_t strip;
+ uint8_t flags;
+ uint16_t bytes;
+ union {
+ uint16_t* u16;
+ uint8_t* u8;
+ } data;
+} openpixel_buffer;
+
+#pragma pack(push, 1)
+typedef struct /*_openpixel_hdr*/ {
+ uint8_t strip;
+ uint8_t mode;
+ uint16_t length;
+} openpixel_header;
+#pragma pack(pop)
+
+typedef struct {
+ enum {
+ rgb8 = 0,
+ rgb16 = 2
+ } mode;
+
+ size_t buffers;
+ openpixel_buffer* buffer;
+
+ int dest_fd;
+ int listen_fd;
+ size_t clients;
+ int* client_fd;
+ size_t* bytes_left;
+} openpixel_instance_data;
diff --git a/backends/openpixelcontrol.md b/backends/openpixelcontrol.md
new file mode 100644
index 0000000..ce60278
--- /dev/null
+++ b/backends/openpixelcontrol.md
@@ -0,0 +1,49 @@
+### The `openpixelcontrol` backend
+
+This backend provides read-write access to the TCP-based OpenPixelControl protocol,
+used for controlling intelligent RGB led strips.
+
+This backend can both control a remote OpenPixelControl server as well as receive data
+from OpenPixelControl clients.
+
+#### Global configuration
+
+This backend does not take any global configuration.
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|-----------------------|
+| `destination` | `10.11.12.1 9001` | none | Destination for output data. Setting this option enables the instance for output |
+| `listen` | `10.11.12.2 9002` | none | Local address to wait for client connections on. Setting this enables the instance for input |
+| `mode` | `16bit` | `8bit` | RGB channel resolution |
+
+#### Channel specification
+
+Each instance can control up to 255 strips of RGB LED lights. The OpenPixelControl specification
+confusingly calls these strips "channels".
+
+Strip `0` acts as a "broadcast" strip, setting values on all other strips at once.
+Consequently, components on strip 0 can only be mapped as output channels to a destination
+(setting components on all strips there), not as input channels. When such messages are received from
+a client, the corresponding mapped component channels on all strips will receive events.
+
+Every single component of any LED on any string can be mapped as an individual MIDIMonster channel.
+The components are laid out as sequences of Red - Green - Blue value triplets.
+
+Channels can be specified by their sequential index (one-based).
+
+Example mapping (data from Strip 2 LED 66's green component is mapped to the blue component of LED 2 on strip 1):
+```
+strip1.channel6 < strip2.channel200
+```
+
+Additionally, channels may be referred to by their color component and LED index:
+```
+strip1.blue2 < strip2.green66
+```
+
+#### Known bugs / problems
+
+If the connection is lost, it is currently not reestablished and may cause exit the MIDIMonster entirely.
+Thisi behaviour may be changed in future releases.