From e1fcd4d11cfdbad54470b2cce98d8b749464ec00 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 27 Feb 2020 00:33:22 +0100 Subject: Implement OpenPixelControl server mode (8bit) --- README.md | 6 +- backends/libmmbackend.c | 4 + backends/openpixelcontrol.c | 220 +++++++++++++++++++++++++++++++++++++++++-- backends/openpixelcontrol.h | 15 ++- backends/openpixelcontrol.md | 9 +- 5 files changed, 241 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 46331f3..4d0b052 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ MIDIMonster Logo -[![Build Status](https://travis-ci.com/cbdevnet/midimonster.svg?branch=master)](https://travis-ci.com/cbdevnet/midimonster) [![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168) +[![Build Status](https://travis-ci.com/cbdevnet/midimonster.svg?branch=master)](https://travis-ci.com/cbdevnet/midimonster) +[![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168) +[![IRC Channel](https://static.midimonster.net/hackint-badge.svg)](https://webirc.hackint.org/#irc://irc.hackint.org/#midimonster) Named for its scary math, the MIDIMonster is a universal control and translation tool for multi-channel absolute-value-based control and/or bus protocols. @@ -15,6 +17,7 @@ Currently, the MIDIMonster supports the following protocols: | ArtNet | Linux, Windows, OSX | Version 4 | [`artnet`](backends/artnet.md)| | Streaming ACN (sACN / E1.31) | Linux, Windows, OSX | | [`sacn`](backends/sacn.md) | | OpenSoundControl (OSC) | Linux, Windows, OSX | | [`osc`](backends/osc.md) | +| OpenPixelControl | Linux, Windows, OSX | 8 Bit & 16 Bit modes | [`openpixelcontrol`](backends/openpixelcontrol.md) | | evdev input devices | Linux | Virtual output supported | [`evdev`](backends/evdev.md) | | Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | | MA Lighting Web Remote | Linux, Windows, OSX | GrandMA2 and dot2 (incl. OnPC) | [`maweb`](backends/maweb.md) | @@ -142,6 +145,7 @@ special information. These documentation files are located in the `backends/` di * [`loopback` backend documentation](backends/loopback.md) * [`ola` backend documentation](backends/ola.md) * [`osc` backend documentation](backends/osc.md) +* [`openpixelcontrol` backend documentation](backends/openpixelcontrol.md) * [`lua` backend documentation](backends/lua.md) * [`maweb` backend documentation](backends/maweb.md) diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index ffa403b..b9513ac 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -153,7 +153,11 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin int mmbackend_send(int fd, uint8_t* data, size_t length){ ssize_t total = 0, sent; while(total < length){ + #ifndef LIBMMBACKEND_TCP_TORTURE sent = send(fd, data + total, length - total, 0); + #else + sent = send(fd, data + total, 1, 0); + #endif if(sent < 0){ LOGPF("Failed to send: %s", strerror(errno)); return 1; diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 062c015..3a94a12 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -52,6 +52,7 @@ static int openpixel_configure_instance(instance* inst, char* option, char* valu if(data->dest_fd >= 0){ return 0; } + LOGPF("Failed to connect to server for instance %s", inst->name); return 1; } if(!strcmp(option, "listen")){ @@ -62,9 +63,10 @@ static int openpixel_configure_instance(instance* inst, char* option, char* valu } data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0); - if(data->listen_fd >= 0 && listen(data->listen_fd, SOMAXCONN)){ + if(data->listen_fd >= 0 && !listen(data->listen_fd, SOMAXCONN)){ return 0; } + LOGPF("Failed to bind server descriptor for instance %s", inst->name); return 1; } else if(!strcmp(option, "mode")){ @@ -103,15 +105,22 @@ static ssize_t openpixel_buffer_find(openpixel_instance_data* data, uint8_t stri for(n = 0; n < data->buffers; n++){ if(data->buffer[n].strip == strip && (data->buffer[n].flags & OPENPIXEL_INPUT) >= input){ + DBGPF("Using allocated %s buffer for requested strip %d, size %d", input ? "input" : "output", strip, data->buffer[n].bytes); return n; } } + DBGPF("Instance has no %s buffer for requested strip %d", input ? "input" : "output", strip); return -1; } -static int openpixel_buffer_extend(openpixel_instance_data* data, uint8_t strip, uint8_t input, uint8_t length){ +static int openpixel_buffer_extend(openpixel_instance_data* data, uint8_t strip, uint8_t input, uint16_t length){ ssize_t buffer = openpixel_buffer_find(data, strip, input); + + //length is in component-channels, round it to the nearest rgb-triplet + //this guarantees that any allocated buffer has at least three bytes, which is important to parts of the receive handler length = (length % 3) ? ((length / 3) + 1) * 3 : length; + + //calculate required buffer length size_t bytes_required = (data->mode == rgb8) ? length : length * 2; if(buffer < 0){ //allocate new buffer @@ -242,7 +251,7 @@ static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* } if(strip == 0){ - //update values in all other output strips, dont mark + //update values in all other output strips, don't mark for(p = 0; p < data->buffers; p++){ if(!(data->buffer[p].flags & OPENPIXEL_INPUT)){ //check whether the buffer is large enough @@ -278,8 +287,204 @@ static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* return 0; } +static int openpixel_client_new(instance* inst, int fd){ + if(fd < 0){ + return 1; + } + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + size_t u; + + //mark nonblocking + #ifdef _WIN32 + unsigned long flags = 1; + if(ioctlsocket(fd, FIONBIO, &flags)){ + #else + int flags = fcntl(fd, F_GETFL, 0); + if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ + #endif + LOGPF("Failed to set client descriptor on %s nonblocking", inst->name); + close(fd); + return 0; + } + + //find a client block + for(u = 0; u < data->clients; u++){ + if(data->client[u].fd <= 0){ + break; + } + } + + //if no free slot, make one + if(u == data->clients){ + data->client = realloc(data->client, (data->clients + 1) * sizeof(openpixel_client)); + if(!data->client){ + data->clients = 0; + LOG("Failed to allocate memory"); + return 1; + } + data->clients++; + } + + data->client[u].fd = fd; + data->client[u].buffer = -1; + data->client[u].offset = 0; + + return mm_manage_fd(fd, BACKEND_NAME, 1, inst); +} + +static int openpixel_client_handle(instance* inst, int fd){ + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + uint8_t buffer[8192]; + size_t c = 0, offset = 0, u; + ssize_t bytes_left = 0; + channel* chan = NULL; + channel_value val; + + for(c = 0; c < data->clients; c++){ + if(data->client[c].fd == fd){ + break; + } + } + + if(c == data->clients){ + LOGPF("Unknown client descriptor signaled on %s", inst->name); + return 1; + } + + //FIXME might want to read until EAGAIN + ssize_t bytes = recv(fd, buffer, sizeof(buffer), 0); + if(bytes <= 0){ + if(bytes < 0){ + LOGPF("Failed to receive from client: %s", strerror(errno)); + } + + //close the connection + close(fd); + data->client[c].fd = -1; + //unmanage the fd + mm_manage_fd(fd, BACKEND_NAME, 0, NULL); + return 0; + } + + for(bytes_left = bytes - offset; bytes_left > 0; bytes_left = bytes - offset){ + if(data->client[c].buffer == -1){ + //read a header + DBGPF("Reading %" PRIsize_t " bytes to header at offset %" PRIsize_t ", header size %" PRIsize_t ", %" PRIsize_t " bytes left", min(sizeof(openpixel_header) - data->client[c].offset, bytes_left), data->client[c].offset, sizeof(openpixel_header), bytes_left); + memcpy(((uint8_t*) (&data->client[c].hdr)) + data->client[c].offset, buffer + offset, min(sizeof(openpixel_header) - data->client[c].offset, bytes_left)); + + //if done, resolve buffer + if(sizeof(openpixel_header) - data->client[c].offset < bytes_left){ + data->client[c].buffer = openpixel_buffer_find(data, data->client[c].hdr.strip, 1); + //if no buffer or mode mismatch, ignore data + if(data->client[c].buffer < 0 + || data->mode != data->client[c].hdr.mode){ + data->client[c].buffer = -2; //mark for ignore + } + data->client[c].left = be16toh(data->client[c].hdr.length); + data->client[c].offset = 0; + } + //if not, update client offset + else{ + data->client[c].offset += bytes_left; + } + + //update scan offset + offset += min(sizeof(openpixel_header) - data->client[c].offset, bytes_left); + } + else{ + //read data + if(data->client[c].buffer == -2){ + //ignore data + offset += min(data->client[c].left, bytes_left); + data->client[c].offset += min(data->client[c].left, bytes_left); + data->client[c].left -= min(data->client[c].left, bytes_left); + } + else{ + if(data->mode == rgb8){ + for(u = 0; u < bytes_left; u++){ + //if over buffer length, ignore + if(u + data->client[c].offset >= data->buffer[data->client[c].buffer].bytes){ + data->client[c].buffer = -2; + break; + } + + //FIXME if at start of trailing non-multiple of 3, ignore + + //update changed channels + if(data->buffer[data->client[c].buffer].data.u8[u + data->client[c].offset] != buffer[offset + u]){ + data->buffer[data->client[c].buffer].data.u8[u + data->client[c].offset] = buffer[offset + u]; + chan = mm_channel(inst, ((uint64_t) data->client[c].hdr.strip << 32) | (u + data->client[c].offset + 1), 0); + if(chan){ + //push event + val.raw.u64 = buffer[offset + u]; + val.normalised = (double) buffer[offset + u] / 255.0; + if(mm_channel_event(chan, val)){ + LOG("Failed to push channel event to core"); + //FIXME err out here + } + } + } + } + + //update offsets + offset += u; + data->client[c].offset += u; + data->client[c].left -= u; + } + else{ + //TODO byte-order conversion may be on recv boundary + //if over buffer length, ignore + //skip non-multiple-of 6 trailing data + } + } + + //end of data, return to reading headers + if(data->client[c].left == 0){ + data->client[c].buffer = -1; + data->client[c].offset = 0; + data->client[c].left = 0; + } + } + } + + return 0; +} + static int openpixel_handle(size_t num, managed_fd* fds){ - //TODO handle bcast + size_t u; + instance* inst = NULL; + openpixel_instance_data* data = NULL; + uint8_t buffer[8192]; + ssize_t bytes; + + for(u = 0; u < num; u++){ + inst = (instance*) fds[u].impl; + data = (openpixel_instance_data*) inst->impl; + + if(fds[u].fd == data->dest_fd){ + //destination fd ready to read + //since the protocol does not define any responses, the connection was probably closed + bytes = recv(data->dest_fd, buffer, sizeof(buffer), 0); + if(bytes <= 0){ + LOGPF("Output descriptor closed on instance %s", inst->name); + //unmanage the fd to give the core some rest + mm_manage_fd(data->dest_fd, BACKEND_NAME, 0, NULL); + } + else{ + LOGPF("Unhandled response data on %s (%" PRIsize_t" bytes)", inst->name, bytes); + } + } + else if(fds[u].fd == data->listen_fd){ + //listen fd ready to read, accept a new client + if(openpixel_client_new(inst, accept(data->listen_fd, NULL, NULL))){ + return 1; + } + } + else{ + //handle client input + openpixel_client_handle(inst, fds[u].fd); + } + } return 0; } @@ -323,12 +528,11 @@ static int openpixel_shutdown(size_t n, instance** inst){ //shutdown all clients for(p = 0; p < data->clients; p++){ - if(data->client_fd[p] >= 0){ - close(data->client_fd[p]); + if(data->client[p].fd>= 0){ + close(data->client[p].fd); } } - free(data->client_fd); - free(data->bytes_left); + free(data->client); //close all configured fds if(data->listen_fd >= 0){ diff --git a/backends/openpixelcontrol.h b/backends/openpixelcontrol.h index f1061ea..658bbf0 100644 --- a/backends/openpixelcontrol.h +++ b/backends/openpixelcontrol.h @@ -31,6 +31,18 @@ typedef struct /*_openpixel_hdr*/ { } openpixel_header; #pragma pack(pop) +typedef struct /*_openpixel_client*/ { + int fd; + ssize_t buffer; + openpixel_header hdr; + size_t offset; + size_t left; + union { + uint8_t u8[2]; + uint16_t u16; + } boundary; +} openpixel_client; + typedef struct { enum { rgb8 = 0, @@ -43,6 +55,5 @@ typedef struct { int dest_fd; int listen_fd; size_t clients; - int* client_fd; - size_t* bytes_left; + openpixel_client* client; } openpixel_instance_data; diff --git a/backends/openpixelcontrol.md b/backends/openpixelcontrol.md index ce60278..6dd38bc 100644 --- a/backends/openpixelcontrol.md +++ b/backends/openpixelcontrol.md @@ -45,5 +45,10 @@ 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. +If the connection is lost, it is currently not reestablished and may cause the MIDIMonster to exit entirely. +This behaviour may be changed in future releases. + +While acting as an OpenPixelControl server, the backend allows multiple clients to connect. +This may lead to confusing data output when multiple clients are trying to control the same strip. + +16 bit server mode is not implemented yet. This will be fixed in a future release. -- cgit v1.2.3