diff options
Diffstat (limited to 'backends/artnet.c')
-rw-r--r-- | backends/artnet.c | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/backends/artnet.c b/backends/artnet.c new file mode 100644 index 0000000..c16e630 --- /dev/null +++ b/backends/artnet.c @@ -0,0 +1,562 @@ +#include <string.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> +#include <unistd.h> +#include <fcntl.h> +#include <ctype.h> +#include <errno.h> + +#include "artnet.h" +#define MAX_FDS 255 +#define BACKEND_NAME "artnet" + +static uint8_t default_net = 0; +static size_t artnet_fds = 0; +static artnet_descriptor* artnet_fd = NULL; + +static int artnet_listener(char* host, char* port){ + int fd = -1, status, yes = 1, flags; + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_DGRAM, + .ai_flags = AI_PASSIVE + }; + struct addrinfo* info; + struct addrinfo* addr_it; + + if(artnet_fds >= MAX_FDS){ + fprintf(stderr, "ArtNet backend descriptor limit reached\n"); + return -1; + } + + status = getaddrinfo(host, port, &hints, &info); + if(status){ + fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status)); + return -1; + } + + for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){ + fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); + if(fd < 0){ + continue; + } + + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n"); + } + + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to set SO_BROADCAST on socket\n"); + } + + yes = 0; + if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno)); + } + + status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } + + break; + } + + freeaddrinfo(info); + + if(!addr_it){ + fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port); + return -1; + } + + //set nonblocking + flags = fcntl(fd, F_GETFL, 0); + if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ + fprintf(stderr, "Failed to set ArtNet descriptor nonblocking\n"); + return -1; + } + + //store fd + artnet_fd = realloc(artnet_fd, (artnet_fds + 1) * sizeof(artnet_descriptor)); + if(!artnet_fd){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + + fprintf(stderr, "ArtNet backend interface %zu bound to %s port %s\n", artnet_fds, host, port); + artnet_fd[artnet_fds].fd = fd; + artnet_fd[artnet_fds].output_instances = 0; + artnet_fd[artnet_fds].output_instance = NULL; + artnet_fd[artnet_fds].last_frame = NULL; + artnet_fds++; + return 0; +} + +static int artnet_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ + struct addrinfo* head; + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_DGRAM + }; + + int error = getaddrinfo(host, port, &hints, &head); + if(error || !head){ + fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); + return 1; + } + + memcpy(addr, head->ai_addr, head->ai_addrlen); + *len = head->ai_addrlen; + + freeaddrinfo(head); + return 0; +} + +static int artnet_separate_hostspec(char* in, char** host, char** port){ + size_t u; + + if(!in || !host || !port){ + return 1; + } + + for(u = 0; in[u] && !isspace(in[u]); u++){ + } + + //guess + *host = in; + + if(in[u]){ + in[u] = 0; + *port = in + u + 1; + } + else{ + //no port given + *port = ARTNET_PORT; + } + return 0; +} + +int init(){ + backend artnet = { + .name = BACKEND_NAME, + .conf = artnet_configure, + .create = artnet_instance, + .conf_instance = artnet_configure_instance, + .channel = artnet_channel, + .handle = artnet_set, + .process = artnet_handle, + .start = artnet_start, + .shutdown = artnet_shutdown + }; + + //register backend + if(mm_backend_register(artnet)){ + fprintf(stderr, "Failed to register ArtNet backend\n"); + return 1; + } + return 0; +} + +static int artnet_configure(char* option, char* value){ + char* host = NULL, *port = NULL; + if(!strcmp(option, "net")){ + //configure default net + default_net = strtoul(value, NULL, 0); + return 0; + } + else if(!strcmp(option, "bind")){ + if(artnet_separate_hostspec(value, &host, &port)){ + fprintf(stderr, "Not a valid ArtNet bind address: %s\n", value); + return 1; + } + + if(artnet_listener(host, port)){ + fprintf(stderr, "Failed to bind ArtNet descriptor: %s\n", value); + return 1; + } + return 0; + } + + fprintf(stderr, "Unknown ArtNet backend option %s\n", option); + return 1; +} + +static instance* artnet_instance(){ + artnet_instance_data* data = NULL; + instance* inst = mm_instance(); + if(!inst){ + return NULL; + } + + data = calloc(1, sizeof(artnet_instance_data)); + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + data->net = default_net; + + inst->impl = data; + return inst; +} + +static int artnet_configure_instance(instance* inst, char* option, char* value){ + char* host = NULL, *port = NULL; + artnet_instance_data* data = (artnet_instance_data*) inst->impl; + + if(!strcmp(option, "net")){ + data->net = strtoul(value, NULL, 0); + return 0; + } + else if(!strcmp(option, "uni") || !strcmp(option, "universe")){ + data->uni = strtoul(value, NULL, 0); + return 0; + } + else if(!strcmp(option, "iface") || !strcmp(option, "interface")){ + data->fd_index = strtoul(value, NULL, 0); + + if(data->fd_index >= artnet_fds){ + fprintf(stderr, "Invalid interface configured for ArtNet instance %s\n", inst->name); + return 1; + } + return 0; + } + else if(!strcmp(option, "dest") || !strcmp(option, "destination")){ + if(artnet_separate_hostspec(value, &host, &port)){ + fprintf(stderr, "Not a valid ArtNet destination for instance %s\n", inst->name); + return 1; + } + + return artnet_parse_addr(host, port, &data->dest_addr, &data->dest_len); + } + + fprintf(stderr, "Unknown ArtNet option %s for instance %s\n", option, inst->name); + return 1; +} + +static channel* artnet_channel(instance* inst, char* spec){ + artnet_instance_data* data = (artnet_instance_data*) inst->impl; + char* spec_next = spec; + unsigned chan_a = strtoul(spec, &spec_next, 10); + unsigned chan_b = 0; + + //primary channel sanity check + if(!chan_a || chan_a > 512){ + fprintf(stderr, "Invalid ArtNet channel specification %s\n", spec); + return NULL; + } + chan_a--; + + //secondary channel setup + if(*spec_next == '+'){ + chan_b = strtoul(spec_next + 1, NULL, 10); + if(!chan_b || chan_b > 512){ + fprintf(stderr, "Invalid wide-channel spec %s\n", spec); + return NULL; + } + chan_b--; + + //if mapped mode differs, bail + if(IS_ACTIVE(data->data.map[chan_b]) && data->data.map[chan_b] != (MAP_FINE | chan_a)){ + fprintf(stderr, "Fine channel already mapped for ArtNet spec %s\n", spec); + return NULL; + } + + data->data.map[chan_b] = MAP_FINE | chan_a; + } + + //check current map mode + if(IS_ACTIVE(data->data.map[chan_a])){ + if((*spec_next == '+' && data->data.map[chan_a] != (MAP_COARSE | chan_b)) + || (*spec_next != '+' && data->data.map[chan_a] != (MAP_SINGLE | chan_a))){ + fprintf(stderr, "Primary ArtNet channel already mapped at differing mode: %s\n", spec); + return NULL; + } + } + data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a); + + return mm_channel(inst, chan_a, 1); +} + +static int artnet_transmit(instance* inst){ + size_t u; + artnet_instance_data* data = (artnet_instance_data*) inst->impl; + //output frame + artnet_pkt frame = { + .magic = {'A', 'r', 't', '-', 'N', 'e', 't', 0x00}, + .opcode = htobe16(OpDmx), + .version = htobe16(ARTNET_VERSION), + .sequence = data->data.seq++, + .port = 0, + .universe = data->uni, + .net = data->net, + .length = htobe16(512), + .data = {0} + }; + memcpy(frame.data, data->data.out, 512); + + if(sendto(artnet_fd[data->fd_index].fd, &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ + fprintf(stderr, "Failed to output ArtNet frame for instance %s: %s\n", inst->name, strerror(errno)); + } + + //update last frame timestamp + for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){ + if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){ + artnet_fd[data->fd_index].last_frame[u] = mm_timestamp(); + } + } + return 0; +} + +static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v){ + size_t u, mark = 0; + artnet_instance_data* data = (artnet_instance_data*) inst->impl; + + if(!data->dest_len){ + fprintf(stderr, "ArtNet instance %s not enabled for output (%zu channel events)\n", inst->name, num); + return 0; + } + + //FIXME maybe introduce minimum frame interval + for(u = 0; u < num; u++){ + if(IS_WIDE(data->data.map[c[u]->ident])){ + uint32_t val = v[u].normalised * ((double) 0xFFFF); + //the primary (coarse) channel is the one registered to the core, so we don't have to check for that + if(data->data.out[c[u]->ident] != ((val >> 8) & 0xFF)){ + mark = 1; + data->data.out[c[u]->ident] = (val >> 8) & 0xFF; + } + + if(data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] != (val & 0xFF)){ + mark = 1; + data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] = val & 0xFF; + } + } + else if(data->data.out[c[u]->ident] != (v[u].normalised * 255.0)){ + mark = 1; + data->data.out[c[u]->ident] = v[u].normalised * 255.0; + } + } + + if(mark){ + return artnet_transmit(inst); + } + + return 0; +} + +static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ + size_t p, max_mark = 0; + uint16_t wide_val = 0; + channel* chan = NULL; + channel_value val; + artnet_instance_data* data = (artnet_instance_data*) inst->impl; + + if(be16toh(frame->length) > 512){ + fprintf(stderr, "Invalid ArtNet frame channel count\n"); + return 1; + } + + //read data, mark update channels + for(p = 0; p < be16toh(frame->length); p++){ + if(IS_ACTIVE(data->data.map[p]) && frame->data[p] != data->data.in[p]){ + data->data.in[p] = frame->data[p]; + data->data.map[p] |= MAP_MARK; + max_mark = p; + } + } + + //generate events + for(p = 0; p <= max_mark; p++){ + if(data->data.map[p] & MAP_MARK){ + data->data.map[p] &= ~MAP_MARK; + if(data->data.map[p] & MAP_FINE){ + chan = mm_channel(inst, MAPPED_CHANNEL(data->data.map[p]), 0); + } + else{ + chan = mm_channel(inst, p, 0); + } + + if(!chan){ + fprintf(stderr, "Active channel %zu on %s not known to core\n", p, inst->name); + return 1; + } + + if(IS_WIDE(data->data.map[p])){ + data->data.map[MAPPED_CHANNEL(data->data.map[p])] &= ~MAP_MARK; + wide_val = data->data.in[p] << ((data->data.map[p] & MAP_COARSE) ? 8 : 0); + wide_val |= data->data.in[MAPPED_CHANNEL(data->data.map[p])] << ((data->data.map[p] & MAP_COARSE) ? 0 : 8); + + val.raw.u64 = wide_val; + val.normalised = (double) wide_val / (double) 0xFFFF; + } + else{ + //single channel + val.raw.u64 = data->data.in[p]; + val.normalised = (double) data->data.in[p] / 255.0; + } + + if(mm_channel_event(chan, val)){ + fprintf(stderr, "Failed to push ArtNet channel event to core\n"); + return 1; + } + } + } + return 0; +} + +static int artnet_handle(size_t num, managed_fd* fds){ + size_t u, c; + uint64_t timestamp = mm_timestamp(); + ssize_t bytes_read; + char recv_buf[ARTNET_RECV_BUF]; + artnet_instance_id inst_id = { + .label = 0 + }; + instance* inst = NULL; + artnet_pkt* frame = (artnet_pkt*) recv_buf; + + //transmit keepalive frames + for(u = 0; u < artnet_fds; u++){ + for(c = 0; c < artnet_fd[u].output_instances; c++){ + if(timestamp - artnet_fd[u].last_frame[c] >= ARTNET_KEEPALIVE_INTERVAL){ + inst = mm_instance_find(BACKEND_NAME, artnet_fd[u].output_instance[c].label); + if(inst){ + artnet_transmit(inst); + } + } + } + } + + if(!num){ + //early exit + return 0; + } + + for(u = 0; u < num; u++){ + do{ + bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); + if(bytes_read > 0 && bytes_read > sizeof(artnet_hdr)){ + if(!memcmp(frame->magic, "Art-Net\0", 8) && be16toh(frame->opcode) == OpDmx){ + //find matching instance + inst_id.fields.fd_index = ((uint64_t) fds[u].impl) & 0xFF; + inst_id.fields.net = frame->net; + inst_id.fields.uni = frame->universe; + inst = mm_instance_find(BACKEND_NAME, inst_id.label); + if(inst && artnet_process_frame(inst, frame)){ + fprintf(stderr, "Failed to process ArtNet frame\n"); + } + } + } + } while(bytes_read > 0); + + if(bytes_read < 0 && errno != EAGAIN){ + fprintf(stderr, "ArtNet failed to receive data: %s\n", strerror(errno)); + } + + if(bytes_read == 0){ + fprintf(stderr, "ArtNet listener closed\n"); + return 1; + } + } + + return 0; +} + +static int artnet_start(){ + size_t n, u, p; + int rv = 1; + instance** inst = NULL; + artnet_instance_data* data = NULL; + artnet_instance_id id = { + .label = 0 + }; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + if(!n){ + free(inst); + return 0; + } + + if(!artnet_fds){ + fprintf(stderr, "Failed to start ArtNet backend: no descriptors bound\n"); + return 1; + } + + for(u = 0; u < n; u++){ + data = (artnet_instance_data*) inst[u]->impl; + //set instance identifier + id.fields.fd_index = data->fd_index; + id.fields.net = data->net; + id.fields.uni = data->uni; + inst[u]->ident = id.label; + + //check for duplicates + for(p = 0; p < u; p++){ + if(inst[u]->ident == inst[p]->ident){ + fprintf(stderr, "Universe specified multiple times, use one instance: %s - %s\n", inst[u]->name, inst[p]->name); + goto bail; + } + } + + //if enabled for output, add to keepalive tracking + if(data->dest_len){ + artnet_fd[data->fd_index].output_instance = realloc(artnet_fd[data->fd_index].output_instance, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(artnet_instance_id)); + artnet_fd[data->fd_index].last_frame = realloc(artnet_fd[data->fd_index].last_frame, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(uint64_t)); + + if(!artnet_fd[data->fd_index].output_instance || !artnet_fd[data->fd_index].last_frame){ + fprintf(stderr, "Failed to allocate memory\n"); + goto bail; + } + artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances] = id; + artnet_fd[data->fd_index].last_frame[artnet_fd[data->fd_index].output_instances] = 0; + + artnet_fd[data->fd_index].output_instances++; + } + } + + fprintf(stderr, "ArtNet backend registering %zu descriptors to core\n", artnet_fds); + for(u = 0; u < artnet_fds; u++){ + if(mm_manage_fd(artnet_fd[u].fd, BACKEND_NAME, 1, (void*) u)){ + goto bail; + } + } + + rv = 0; +bail: + free(inst); + return rv; +} + +static int artnet_shutdown(){ + size_t n, p; + instance** inst = NULL; + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + for(p = 0; p < n; p++){ + free(inst[p]->impl); + } + free(inst); + + for(p = 0; p < artnet_fds; p++){ + close(artnet_fd[p].fd); + free(artnet_fd[p].output_instance); + free(artnet_fd[p].last_frame); + } + free(artnet_fd); + + fprintf(stderr, "ArtNet backend shut down\n"); + return 0; +} |