From dd91621ccee033550312683293b5bf40c3599053 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 23 Feb 2020 18:20:12 +0100 Subject: Implement OpenPixelControl output --- backends/Makefile | 11 +- backends/openpixelcontrol.c | 353 +++++++++++++++++++++++++++++++++++++++++++ backends/openpixelcontrol.h | 48 ++++++ backends/openpixelcontrol.md | 49 ++++++ 4 files changed, 459 insertions(+), 2 deletions(-) create mode 100644 backends/openpixelcontrol.c create mode 100644 backends/openpixelcontrol.h create mode 100644 backends/openpixelcontrol.md (limited to 'backends') 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 + +#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. -- cgit v1.2.3