From 1ed17293bedaf5bf5182d863cd406d7eb66b4501 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 24 Mar 2019 19:22:30 +0100 Subject: Factor out socket operations (Fixes #13) --- backends/libmmbackend.h | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 backends/libmmbackend.h (limited to 'backends/libmmbackend.h') diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h new file mode 100644 index 0000000..38bfca0 --- /dev/null +++ b/backends/libmmbackend.h @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Parse spec as host specification in the form + * host port + * into its constituent parts. + * Returns offsets into the original string and modifies it. + * Returns NULL in *port if none given. + * Returns NULL in both *port and *host if spec was an empty string. + */ +void mmbackend_parse_hostspec(char* spec, char** host, char** port); + +/* Parse a given host / port combination into a sockaddr_storage + * suitable for usage with connect / sendto + * Returns 0 on success + */ +int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len); + +/* Create a socket of given type and mode for a bind / connect host. + * Returns -1 on failure, a valid file descriptor for the socket on success. + */ +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener); -- cgit v1.2.3 From 20a6882a063404858588596bd3f12bdd9e53460a Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 3 Aug 2019 18:42:39 +0200 Subject: Windows build compatiblity --- .gitignore | 3 +++ Makefile | 23 +++++++++++++++++-- backend.c | 31 ++++++++++++++----------- backend.h | 9 +++++++- backends/Makefile | 26 +++++++++++++++++++-- backends/artnet.c | 16 ++++++++----- backends/artnet.h | 2 ++ backends/libmmbackend.c | 12 ++++++++-- backends/libmmbackend.h | 6 +++++ backends/lua.c | 4 ---- backends/osc.c | 18 +++++++++------ backends/osc.h | 2 ++ backends/sacn.c | 28 ++++++++++++++--------- backends/sacn.h | 1 - config.c | 56 +++++++++++++++++++++++++++++++++++++++++++++ midimonster.c | 39 ++++++++++++++++++++++++++------ midimonster.h | 38 ++++++++++++++++++++++++------- monster.cfg | 12 +++------- plugin.c | 60 +++++++++++++++++++++++++++++++++++++++++++++---- portability.h | 17 ++++++++++++++ 20 files changed, 326 insertions(+), 77 deletions(-) (limited to 'backends/libmmbackend.h') diff --git a/.gitignore b/.gitignore index 3afd872..4396a38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ midimonster +midimonster.exe +libmmapi.a *.swp *.o *.so +*.dll diff --git a/Makefile b/Makefile index 5ee9cd9..b82d6d8 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,14 @@ -.PHONY: all clean run sanitize backends full backends-full +.PHONY: all clean run sanitize backends windows full backends-full OBJS = config.o backend.o plugin.o PLUGINDIR = "\"./backends/\"" +PLUGINDIR_W32 = "\"backends\\\\\"" SYSTEM := $(shell uname -s) -CFLAGS ?= -g -Wall +CFLAGS ?= -g -Wall -Wpedantic +# Hide all non-API symbols for export +CFLAGS += -fvisibility=hidden + #CFLAGS += -DDEBUG midimonster: LDLIBS = -ldl midimonster: CFLAGS += -DPLUGINS=$(PLUGINDIR) @@ -21,6 +25,8 @@ all: midimonster backends full: midimonster backends-full +windows: midimonster.exe + backends: $(MAKE) -C backends @@ -31,8 +37,21 @@ backends-full: midimonster: midimonster.c portability.h $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ +midimonster.exe: export CC = x86_64-w64-mingw32-gcc +#midimonster.exe: CFLAGS += -Wno-format +midimonster.exe: CFLAGS += -DPLUGINS=$(PLUGINDIR_W32) -Wno-format +midimonster.exe: LDLIBS = -lws2_32 +midimonster.exe: LDFLAGS += -Wl,--out-implib,libmmapi.a +midimonster.exe: midimonster.c portability.h $(OBJS) + $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ + # The windows build for backends requires the import library generated with the build, + # so the backends can't be a prerequisite for the executable... + $(MAKE) -C backends windows + clean: $(RM) midimonster + $(RM) midimonster.exe + $(RM) libmmapi.a $(RM) $(OBJS) $(MAKE) -C backends clean diff --git a/backend.c b/backend.c index 5df5d73..4fa7704 100644 --- a/backend.c +++ b/backend.c @@ -1,4 +1,9 @@ #include +#ifndef _WIN32 +#define MM_API __attribute__((visibility ("default"))) +#else +#define MM_API __attribute__((dllexport)) +#endif #include "midimonster.h" #include "backend.h" @@ -26,7 +31,7 @@ int backends_handle(size_t nfds, managed_fd* fds){ } } - DBGPF("Notifying backend %s of %zu waiting FDs\n", backends[u].name, n); + DBGPF("Notifying backend %s of %lu waiting FDs\n", backends[u].name, n); rv |= backends[u].process(n, fds); if(rv){ fprintf(stderr, "Backend %s failed to handle input\n", backends[u].name); @@ -59,28 +64,28 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ } } - DBGPF("Calling handler for instance %s with %zu events\n", instances[u]->name, n); + DBGPF("Calling handler for instance %s with %lu events\n", instances[u]->name, n); rv |= instances[u]->backend->handle(instances[u], n, c, v); } return 0; } -channel* mm_channel(instance* i, uint64_t ident, uint8_t create){ +channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create){ size_t u; for(u = 0; u < nchannels; u++){ - if(channels[u]->instance == i && channels[u]->ident == ident){ - DBGPF("Requested channel %zu on instance %s already exists, reusing\n", ident, i->name); + if(channels[u]->instance == inst && channels[u]->ident == ident){ + DBGPF("Requested channel %lu on instance %s already exists, reusing\n", ident, inst->name); return channels[u]; } } if(!create){ - DBGPF("Requested unknown channel %zu on instance %s\n", ident, i->name); + DBGPF("Requested unknown channel %lu on instance %s\n", ident, inst->name); return NULL; } - DBGPF("Creating previously unknown channel %zu on instance %s\n", ident, i->name); + DBGPF("Creating previously unknown channel %lu on instance %s\n", ident, inst->name); channel** new_chan = realloc(channels, (nchannels + 1) * sizeof(channel*)); if(!new_chan){ fprintf(stderr, "Failed to allocate memory\n"); @@ -95,12 +100,12 @@ channel* mm_channel(instance* i, uint64_t ident, uint8_t create){ return NULL; } - channels[nchannels]->instance = i; + channels[nchannels]->instance = inst; channels[nchannels]->ident = ident; return channels[nchannels++]; } -instance* mm_instance(){ +instance* MM_API mm_instance(){ instance** new_inst = realloc(instances, (ninstances + 1) * sizeof(instance*)); if(!new_inst){ //TODO free @@ -118,7 +123,7 @@ instance* mm_instance(){ return instances[ninstances++]; } -instance* mm_instance_find(char* name, uint64_t ident){ +instance* MM_API mm_instance_find(char* name, uint64_t ident){ size_t u; backend* b = backend_match(name); if(!b){ @@ -134,7 +139,7 @@ instance* mm_instance_find(char* name, uint64_t ident){ return NULL; } -int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ +int MM_API mm_backend_instances(char* name, size_t* ninst, instance*** inst){ backend* b = backend_match(name); size_t n = 0, u; //count number of affected instances @@ -177,7 +182,7 @@ void instances_free(){ void channels_free(){ size_t u; for(u = 0; u < nchannels; u++){ - DBGPF("Destroying channel %zu on instance %s\n", channels[u]->ident, channels[u]->instance->name); + DBGPF("Destroying channel %lu on instance %s\n", channels[u]->ident, channels[u]->instance->name); if(channels[u]->impl){ channels[u]->instance->backend->channel_free(channels[u]); } @@ -232,7 +237,7 @@ struct timeval backend_timeout(){ return tv; } -int mm_backend_register(backend b){ +int MM_API mm_backend_register(backend b){ if(!backend_match(b.name)){ backends = realloc(backends, (nbackends + 1) * sizeof(backend)); if(!backends){ diff --git a/backend.h b/backend.h index daf96bc..7529154 100644 --- a/backend.h +++ b/backend.h @@ -1,8 +1,8 @@ #include +/* Internal API */ int backends_handle(size_t nfds, managed_fd* fds); int backends_notify(size_t nev, channel** c, channel_value* v); - backend* backend_match(char* name); instance* instance_match(char* name); struct timeval backend_timeout(); @@ -10,3 +10,10 @@ int backends_start(); int backends_stop(); void instances_free(); void channels_free(); + +/* Backend API */ +channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create); +instance* MM_API mm_instance(); +instance* MM_API mm_instance_find(char* name, uint64_t ident); +int MM_API mm_backend_instances(char* name, size_t* ninst, instance*** inst); +int MM_API mm_backend_register(backend b); diff --git a/backends/Makefile b/backends/Makefile index 22cb95b..2374df0 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,12 +1,13 @@ .PHONY: all clean full OPTIONAL_BACKENDS = ola.so +WINDOWS_BACKENDS = loopback.dll artnet.dll osc.dll sacn.dll LINUX_BACKENDS = midi.so evdev.so BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) -CFLAGS += -g -fPIC -I../ +CFLAGS += -g -fPIC -I../ -Wall -Wpedantic CPPFLAGS += -g -fPIC -I../ LDFLAGS += -shared @@ -20,8 +21,17 @@ LDFLAGS += -undefined dynamic_lookup endif artnet.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +artnet.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +artnet.dll: LDLIBS += -lws2_32 + osc.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +osc.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +osc.dll: LDLIBS += -lws2_32 + sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +sacn.dll: LDLIBS += -lws2_32 + midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) @@ -33,12 +43,24 @@ lua.so: LDLIBS += $(shell pkg-config --libs lua5.3) %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) +%.dll :: %.c %.h $(BACKEND_LIB) + $(CC) $(CFLAGS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) $(LDLIBS) + %.so :: %.cpp %.h $(CXX) $(CPPFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) all: $(BACKEND_LIB) $(BACKENDS) +../libmmapi.a: + $(MAKE) -C ../ midimonster.exe + +windows: export CC = x86_64-w64-mingw32-gcc +windows: LDLIBS += -lmmapi +windows: LDFLAGS += -L../ +windows: CFLAGS += -Wno-format -Wno-pointer-sign +windows: ../libmmapi.a $(BACKEND_LIB) $(WINDOWS_BACKENDS) + full: $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) clean: - $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) + $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) $(WINDOWS_BACKENDS) diff --git a/backends/artnet.c b/backends/artnet.c index 8b404a6..a6df4ab 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -3,8 +3,8 @@ #include #include "libmmbackend.h" - #include "artnet.h" + #define MAX_FDS 255 #define BACKEND_NAME "artnet" @@ -32,7 +32,7 @@ static int artnet_listener(char* host, char* port){ return -1; } - fprintf(stderr, "ArtNet backend interface %zu bound to %s port %s\n", artnet_fds, host, port); + fprintf(stderr, "ArtNet backend interface %lu 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; @@ -212,7 +212,7 @@ static int artnet_transmit(instance* inst){ }; 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){ + if(sendto(artnet_fd[data->fd_index].fd, (uint8_t*) &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)); } @@ -230,7 +230,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) 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); + fprintf(stderr, "ArtNet instance %s not enabled for output (%lu channel events)\n", inst->name, num); return 0; } @@ -295,7 +295,7 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ } if(!chan){ - fprintf(stderr, "Active channel %zu on %s not known to core\n", p, inst->name); + fprintf(stderr, "Active channel %lu on %s not known to core\n", p, inst->name); return 1; } @@ -367,7 +367,11 @@ static int artnet_handle(size_t num, managed_fd* fds){ } } while(bytes_read > 0); + #ifdef _WIN32 + if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){ + #else if(bytes_read < 0 && errno != EAGAIN){ + #endif fprintf(stderr, "ArtNet failed to receive data: %s\n", strerror(errno)); } @@ -438,7 +442,7 @@ static int artnet_start(){ } } - fprintf(stderr, "ArtNet backend registering %zu descriptors to core\n", artnet_fds); + fprintf(stderr, "ArtNet backend registering %lu 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; diff --git a/backends/artnet.h b/backends/artnet.h index 90aedd5..f5aa745 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -1,4 +1,6 @@ +#ifndef _WIN32 #include +#endif #include "midimonster.h" int init(); diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 6320611..b27ebc5 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -53,7 +53,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad } int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ - int fd = -1, status, yes = 1, flags; + int fd = -1, status, yes = 1; struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = socktype, @@ -106,12 +106,20 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ } //set nonblocking - flags = fcntl(fd, F_GETFL, 0); + #ifdef _WIN32 + u_long mode = 1; + if(ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR){ + closesocket(fd); + return 1; + } + #else + int flags = fcntl(fd, F_GETFL, 0); if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ fprintf(stderr, "Failed to set socket nonblocking\n"); close(fd); return -1; } + #endif return fd; } diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 38bfca0..77cad6a 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -1,14 +1,20 @@ #include #include #include +#ifdef _WIN32 +#include +//#define close closesocket +#else #include #include +#endif #include #include #include #include #include #include +#include "../portability.h" /* Parse spec as host specification in the form * host port diff --git a/backends/lua.c b/backends/lua.c index 4a910a2..ec02575 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -161,8 +161,6 @@ static int lua_callback_output(lua_State* interpreter){ static int lua_callback_interval(lua_State* interpreter){ size_t n = 0; - instance* inst = NULL; - lua_instance_data* data = NULL; uint64_t interval = 0; int reference = LUA_NOREF; @@ -174,8 +172,6 @@ static int lua_callback_interval(lua_State* interpreter){ //get instance pointer from registry lua_pushstring(interpreter, LUA_REGISTRY_KEY); lua_gettable(interpreter, LUA_REGISTRYINDEX); - inst = (instance*) lua_touserdata(interpreter, -1); - data = (lua_instance_data*) inst->impl; //fetch and round the interval interval = luaL_checkinteger(interpreter, 2); diff --git a/backends/osc.c b/backends/osc.c index 18c8bad..03e431f 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -1,8 +1,8 @@ #include #include #include -#include "libmmbackend.h" +#include "libmmbackend.h" #include "osc.h" /* @@ -480,14 +480,14 @@ static int osc_register_pattern(osc_instance_data* data, char* pattern_path, cha //parse min/max values token = strtok(NULL, " "); if(!token){ - fprintf(stderr, "Missing minimum specification for parameter %zu of OSC pattern %s\n", u, pattern_path); + fprintf(stderr, "Missing minimum specification for parameter %lu of OSC pattern %s\n", u, pattern_path); return 1; } data->pattern[pattern].min[u] = osc_parse_value_spec(format[u], token); token = strtok(NULL, " "); if(!token){ - fprintf(stderr, "Missing maximum specification for parameter %zu of OSC pattern %s\n", u, pattern_path); + fprintf(stderr, "Missing maximum specification for parameter %lu of OSC pattern %s\n", u, pattern_path); return 1; } data->pattern[pattern].max[u] = osc_parse_value_spec(format[u], token); @@ -689,7 +689,7 @@ static int osc_output_channel(instance* inst, size_t channel){ //write data if(offset + osc_data_length(data->channel[channel].type[p]) >= sizeof(xmit_buf)){ - fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %zu\n", inst->name, data->channel[channel].path, p); + fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %lu\n", inst->name, data->channel[channel].path, p); return 1; } @@ -720,7 +720,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ osc_instance_data* data = (osc_instance_data*) inst->impl; if(!data->dest_len){ - fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%zu channels)\n", inst->name, num); + fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%lu channels)\n", inst->name, num); return 0; } @@ -778,7 +778,7 @@ static int osc_process_packet(instance* inst, char* local_path, char* format, ui channel* chan = NULL; if(payload_len % 4){ - fprintf(stderr, "Invalid OSC packet, data length %zu\n", payload_len); + fprintf(stderr, "Invalid OSC packet, data length %lu\n", payload_len); return 0; } @@ -877,7 +877,11 @@ static int osc_handle(size_t num, managed_fd* fds){ } } while(bytes_read > 0); + #ifdef _WIN32 + if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){ + #else if(bytes_read < 0 && errno != EAGAIN){ + #endif fprintf(stderr, "OSC failed to receive data for instance %s: %s\n", inst->name, strerror(errno)); } @@ -924,7 +928,7 @@ static int osc_start(){ } } - fprintf(stderr, "OSC backend registered %zu descriptors to core\n", fds); + fprintf(stderr, "OSC backend registered %lu descriptors to core\n", fds); free(inst); return 0; diff --git a/backends/osc.h b/backends/osc.h index 4e9dec5..b2aaea7 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -1,6 +1,8 @@ #include "midimonster.h" #include +#ifndef _WIN32 #include +#endif #define OSC_RECV_BUF 8192 #define OSC_XMIT_BUF 8192 diff --git a/backends/sacn.c b/backends/sacn.c index 75bb76f..6f7d1a5 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -1,16 +1,18 @@ #include #include -#include -#include #include #include #include #include +#ifndef _WIN32 +#include #include +#include +#endif #include "libmmbackend.h" - #include "sacn.h" + //upper limit imposed by using the fd index as 16-bit part of the instance id #define MAX_FDS 4096 #define BACKEND_NAME "sacn" @@ -71,7 +73,7 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){ return -1; } - fprintf(stderr, "sACN backend interface %zu bound to %s port %s\n", global_cfg.fds, host, port); + fprintf(stderr, "sACN backend interface %lu bound to %s port %s\n", global_cfg.fds, host, port); global_cfg.fd[global_cfg.fds].fd = fd; global_cfg.fd[global_cfg.fds].flags = fd_flags; global_cfg.fd[global_cfg.fds].universes = 0; @@ -271,7 +273,7 @@ static int sacn_transmit(instance* inst){ memcpy(pdu.data.source_name, global_cfg.source_name, sizeof(pdu.data.source_name)); memcpy((((uint8_t*)pdu.data.data) + 1), data->data.out, 512); - if(sendto(global_cfg.fd[data->fd_index].fd, &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ + if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ fprintf(stderr, "Failed to output sACN frame for instance %s: %s\n", inst->name, strerror(errno)); } @@ -293,7 +295,7 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ } if(!data->xmit_prio){ - fprintf(stderr, "sACN instance %s not enabled for output (%zu channel events)\n", inst->name, num); + fprintf(stderr, "sACN instance %s not enabled for output (%lu channel events)\n", inst->name, num); return 0; } @@ -378,7 +380,7 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame } if(!chan){ - fprintf(stderr, "Active channel %zu on %s not known to core", u, inst->name); + fprintf(stderr, "Active channel %lu on %s not known to core", u, inst->name); return 1; } @@ -445,8 +447,8 @@ static void sacn_discovery(size_t fd){ pdu.data.page = page; memcpy(pdu.data.data, global_cfg.fd[fd].universe + page * 512, universes * sizeof(uint16_t)); - if(sendto(global_cfg.fd[fd].fd, &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ - fprintf(stderr, "Failed to output sACN universe discovery frame for interface %zu: %s\n", fd, strerror(errno)); + if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ + fprintf(stderr, "Failed to output sACN universe discovery frame for interface %lu: %s\n", fd, strerror(errno)); } } } @@ -512,7 +514,11 @@ static int sacn_handle(size_t num, managed_fd* fds){ } } while(bytes_read > 0); + #ifdef _WIN32 + if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){ + #else if(bytes_read < 0 && errno != EAGAIN){ + #endif fprintf(stderr, "sACN failed to receive data: %s\n", strerror(errno)); } @@ -577,7 +583,7 @@ static int sacn_start(){ if(!data->unicast_input){ mcast_req.imr_multiaddr.s_addr = htobe32(((uint32_t) 0xefff0000) | ((uint32_t) data->uni)); - if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mcast_req, sizeof(mcast_req))){ + if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ fprintf(stderr, "Failed to join Multicast group for sACN universe %u on instance %s: %s\n", data->uni, inst[u]->name, strerror(errno)); } } @@ -604,7 +610,7 @@ static int sacn_start(){ } } - fprintf(stderr, "sACN backend registering %zu descriptors to core\n", global_cfg.fds); + fprintf(stderr, "sACN backend registering %lu descriptors to core\n", global_cfg.fds); for(u = 0; u < global_cfg.fds; u++){ //allocate memory for storing last frame transmission timestamp global_cfg.fd[u].last_frame = calloc(global_cfg.fd[u].universes, sizeof(uint64_t)); diff --git a/backends/sacn.h b/backends/sacn.h index e7106f7..7af2a36 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -1,4 +1,3 @@ -#include #include "midimonster.h" int init(); diff --git a/config.c b/config.c index 6d5fd16..93fb56d 100644 --- a/config.c +++ b/config.c @@ -20,6 +20,62 @@ typedef enum { static backend* current_backend = NULL; static instance* current_instance = NULL; +#ifdef _WIN32 +#define GETLINE_BUFFER 4096 + +static ssize_t getline(char** line, size_t* alloc, FILE* stream){ + size_t bytes_read = 0; + char c; + //sanity checks + if(!line || !alloc || !stream){ + return -1; + } + + //allocate buffer if none provided + if(!*line || !*alloc){ + *alloc = GETLINE_BUFFER; + *line = calloc(GETLINE_BUFFER, sizeof(char)); + if(!*line){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + } + + if(feof(stream)){ + return -1; + } + + for(c = fgetc(stream); 1; c = fgetc(stream)){ + //end of buffer, resize + if(bytes_read == (*alloc) - 1){ + *alloc += GETLINE_BUFFER; + *line = realloc(*line, (*alloc) * sizeof(char)); + if(!*line){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + } + + //store character + (*line)[bytes_read] = c; + + //end of line + if(feof(stream) || c == '\n'){ + //terminate string + (*line)[bytes_read + 1] = 0; + return bytes_read; + } + + //input broken + if(ferror(stream) || c < 0){ + return -1; + } + + bytes_read++; + } +} +#endif + static char* config_trim_line(char* in){ ssize_t n; //trim front diff --git a/midimonster.c b/midimonster.c index fb664a4..df27ca3 100644 --- a/midimonster.c +++ b/midimonster.c @@ -1,9 +1,14 @@ #include #include -#include #include #include #include +#ifndef _WIN32 +#include +#define MM_API __attribute__((visibility("default"))) +#else +#define MM_API __attribute__((dllexport)) +#endif #include "midimonster.h" #include "config.h" #include "backend.h" @@ -35,11 +40,14 @@ static void signal_handler(int signum){ shutdown_requested = 1; } -uint64_t mm_timestamp(){ +uint64_t MM_API mm_timestamp(){ return global_timestamp; } static void update_timestamp(){ + #ifdef _WIN32 + global_timestamp = GetTickCount(); + #else struct timespec current; if(clock_gettime(CLOCK_MONOTONIC_COARSE, ¤t)){ fprintf(stderr, "Failed to update global timestamp, time-based processing for some backends may be impaired: %s\n", strerror(errno)); @@ -47,6 +55,7 @@ static void update_timestamp(){ } global_timestamp = current.tv_sec * 1000 + current.tv_nsec / 1000000; + #endif } int mm_map_channel(channel* from, channel* to){ @@ -99,7 +108,7 @@ void map_free(){ map = NULL; } -int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ +int MM_API mm_manage_fd(int new_fd, char* back, int manage, void* impl){ backend* b = backend_match(back); size_t u; @@ -163,7 +172,7 @@ void fds_free(){ fd = NULL; } -int mm_channel_event(channel* c, channel_value v){ +int MM_API mm_channel_event(channel* c, channel_value v){ size_t u, p; //find mapped channels @@ -229,7 +238,7 @@ static fd_set fds_collect(int* max_fd){ *max_fd = -1; } - DBGPF("Building selector set from %zu FDs registered to core\n", fds); + DBGPF("Building selector set from %lu FDs registered to core\n", fds); FD_ZERO(&rv_fds); for(u = 0; u < fds; u++){ if(fd[u].fd >= 0){ @@ -243,6 +252,17 @@ static fd_set fds_collect(int* max_fd){ return rv_fds; } +int platform_initialize(){ +#ifdef _WIN32 + WSADATA wsa; + WORD version = MAKEWORD(2, 2); + if(WSAStartup(version, &wsa)){ + return 1; + } +#endif + return 0; +} + int main(int argc, char** argv){ fd_set all_fds, read_fds; event_collection* secondary = NULL; @@ -255,6 +275,11 @@ int main(int argc, char** argv){ cfg_file = argv[1]; } + if(platform_initialize()){ + fprintf(stderr, "Failed to perform platform-specific initialization\n"); + return EXIT_FAILURE; + } + FD_ZERO(&all_fds); //initialize backends if(plugins_load(PLUGINS)){ @@ -316,14 +341,14 @@ int main(int argc, char** argv){ update_timestamp(); //run backend processing, collect events - DBGPF("%zu backend FDs signaled\n", n); + DBGPF("%lu backend FDs signaled\n", n); if(backends_handle(n, signaled_fds)){ goto bail; } while(primary->n){ //swap primary and secondary event collectors - DBGPF("Swapping event collectors, %zu events in primary\n", primary->n); + DBGPF("Swapping event collectors, %lu events in primary\n", primary->n); for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ if(primary != event_pool + u){ secondary = primary; diff --git a/midimonster.h b/midimonster.h index 7f70f5b..eb118c6 100644 --- a/midimonster.h +++ b/midimonster.h @@ -4,6 +4,21 @@ #include #include +#ifndef MM_API + #ifdef _WIN32 + #define MM_API __attribute__((dllimport)) + #else + #define MM_API + #endif +#endif + +/* GCC ignores the visibility attributes on some API functions, so override visibility */ +#if !defined(_WIN32) && defined(__GNUC__) && !defined(__clang__) + #undef MM_API + #define MM_API + #pragma GCC visibility push(default) +#endif + /* Straight-forward min / max macros */ #define max(a,b) (((a) > (b)) ? (a) : (b)) #define min(a,b) (((a) < (b)) ? (a) : (b)) @@ -187,7 +202,7 @@ typedef struct /*_mm_channel_mapping*/ { /* * Register a new backend. */ -int mm_backend_register(backend b); +int MM_API mm_backend_register(backend b); /* * Provides a pointer to a newly (zero-)allocated instance. @@ -201,7 +216,8 @@ int mm_backend_register(backend b); * mmbackend_shutdown procedure of the backend, eg. by querying * all instances for the backend. */ -instance* mm_instance(); +instance* MM_API mm_instance(); + /* * Finds an instance matching the specified backend and identifier. * Since setting an identifier for an instance is optional, @@ -209,7 +225,8 @@ instance* mm_instance(); * Instance identifiers may for example be set in the backends * mmbackend_start call. */ -instance* mm_instance_find(char* backend, uint64_t ident); +instance* MM_API mm_instance_find(char* backend, uint64_t ident); + /* * Provides a pointer to a channel structure, pre-filled with * the provided instance reference and identifier. @@ -224,30 +241,35 @@ instance* mm_instance_find(char* backend, uint64_t ident); * this function, the backend will receive a call to its channel_free * function. */ -channel* mm_channel(instance* i, uint64_t ident, uint8_t create); +channel* MM_API mm_channel(instance* i, uint64_t ident, uint8_t create); //TODO channel* mm_channel_find() + /* * Register (manage = 1) or unregister (manage = 0) a file descriptor * to be selected on. The backend will be notified when the descriptor * becomes ready to read via its registered mmbackend_process_fd call. */ -int mm_manage_fd(int fd, char* backend, int manage, void* impl); +int MM_API mm_manage_fd(int fd, char* backend, int manage, void* impl); + /* * Notifies the core of a channel event. Called by backends to * inject events gathered from their backing implementation. */ -int mm_channel_event(channel* c, channel_value v); +int MM_API mm_channel_event(channel* c, channel_value v); + /* * Query all active instances for a given backend. * *i will need to be freed by the caller. */ -int mm_backend_instances(char* backend, size_t* n, instance*** i); +int MM_API mm_backend_instances(char* backend, size_t* n, instance*** i); + /* * Query an internal timestamp, which is updated every core iteration. * This timestamp should not be used as a performance counter, but can be * used for timeouting. Resolution is milliseconds. */ -uint64_t mm_timestamp(); +uint64_t MM_API mm_timestamp(); + /* * Create a channel-to-channel mapping. This API should not * be used by backends. It is only exported for core modules. diff --git a/monster.cfg b/monster.cfg index 2e6f76f..2413f6d 100644 --- a/monster.cfg +++ b/monster.cfg @@ -1,18 +1,12 @@ [backend artnet] bind = 0.0.0.0 -[backend evdev] +[loopback loop] [artnet art] universe = 0 dest = 255.255.255.255 -[evdev mouse] -input = TPPS - -[loopback loop] - [map] -mouse.EV_REL.REL_X > loop.chan0 -art.{3..4}{4..3} > loop.chan{4..3}{3..4} -art.{1..10} > loop.data{1..10} + +art.1+2 > loop.b diff --git a/plugin.c b/plugin.c index fc642ac..a452559 100644 --- a/plugin.c +++ b/plugin.c @@ -1,11 +1,20 @@ #include #include -#include #include #include #include #include #include +#include "portability.h" +#ifdef _WIN32 +#define dlclose FreeLibrary +#define dlsym GetProcAddress +#define dlerror() "Failed" +#define dlopen(lib,ig) LoadLibrary(lib) +#else +#include +#endif + #include "plugin.h" static size_t plugins = 0; @@ -14,19 +23,29 @@ static void** plugin_handle = NULL; static int plugin_attach(char* path, char* file){ plugin_init init = NULL; void* handle = NULL; + char* lib = NULL; + char* error = NULL; - char* lib = calloc(strlen(path) + strlen(file) + 1, sizeof(char)); + lib = calloc(strlen(path) + strlen(file) + 1, sizeof(char)); if(!lib){ fprintf(stderr, "Failed to allocate memory\n"); return 1; } - snprintf(lib, strlen(path) + strlen(file) + 1, "%s%s", path, file); handle = dlopen(lib, RTLD_NOW); if(!handle){ - fprintf(stderr, "Failed to load plugin %s: %s\n", lib, dlerror()); + #ifdef _WIN32 + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + #else + error = dlerror(); + #endif + fprintf(stderr, "Failed to load plugin %s: %s\n", lib, error); free(lib); + #ifdef _WIN32 + LocalFree(error); + #endif return 0; } @@ -62,6 +81,38 @@ static int plugin_attach(char* path, char* file){ int plugins_load(char* path){ int rv = -1; +#ifdef _WIN32 + char* search_expression = calloc(strlen(path) + strlen("*.dll") + 1, sizeof(char)); + if(!search_expression){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + snprintf(search_expression, strlen(path) + strlen("*.dll"), "%s*.dll", path); + + WIN32_FIND_DATA result; + HANDLE hSearch = FindFirstFile(search_expression, &result); + + if(hSearch == INVALID_HANDLE_VALUE){ + LPVOID lpMsgBuf = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL); + fprintf(stderr, "Failed to search for backend plugin files in %s: %s\n", path, lpMsgBuf); + LocalFree(lpMsgBuf); + return -1; + } + + do { + if(plugin_attach(path, result.cFileName)){ + goto load_done; + } + } while(FindNextFile(hSearch, &result)); + + rv = 0; +load_done: + free(search_expression); + FindClose(hSearch); + return rv; +#else struct dirent* entry; struct stat file_stat; DIR* directory = opendir(path); @@ -100,6 +151,7 @@ load_done: return -1; } return rv; +#endif } int plugins_close(){ diff --git a/portability.h b/portability.h index 25aee01..903ecd8 100644 --- a/portability.h +++ b/portability.h @@ -19,3 +19,20 @@ #define be64toh(x) OSSwapBigToHostInt64(x) #define le64toh(x) OSSwapLittleToHostInt64(x) #endif + +#ifdef _WIN32 + #define WIN32_LEAN_AND_MEAN + #include + #include + + #define htobe16(x) htons(x) + #define be16toh(x) ntohs(x) + + #define htobe32(x) htonl(x) + #define be32toh(x) ntohl(x) + + #define htobe64(x) _byteswap_uint64(x) + #define htole64(x) (x) + #define be64toh(x) _byteswap_uint64(x) + #define le64toh(x) (x) +#endif -- cgit v1.2.3 From cf93d280af47aea1bf8bdafa30eabb2c2de005b8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 10 Aug 2019 15:30:48 +0200 Subject: Implement stream client connections in libmmbackend (Fixes #19) --- backends/artnet.c | 2 +- backends/libmmbackend.c | 62 +++++++++++++++++++++++++++++++++++-------------- backends/libmmbackend.h | 22 ++++++++++++++---- backends/lua.md | 2 +- backends/osc.c | 2 +- backends/sacn.c | 2 +- 6 files changed, 67 insertions(+), 25 deletions(-) (limited to 'backends/libmmbackend.h') diff --git a/backends/artnet.c b/backends/artnet.c index a6df4ab..7f3f08c 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -19,7 +19,7 @@ static int artnet_listener(char* host, char* port){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(fd < 0){ return -1; } diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index b27ebc5..2fd3b8b 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -52,7 +52,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad return 0; } -int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast){ int fd = -1, status, yes = 1; struct addrinfo hints = { .ai_family = AF_UNSPEC, @@ -80,20 +80,31 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ fprintf(stderr, "Failed to enable SO_REUSEADDR on socket\n"); } - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to enable SO_BROADCAST on socket\n"); - } + if(mcast){ + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to enable SO_BROADCAST on socket\n"); + } - yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to disable IP_MULTICAST_LOOP on socket: %s\n", strerror(errno)); + yes = 0; + if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to disable IP_MULTICAST_LOOP on socket: %s\n", strerror(errno)); + } } - status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); - if(status < 0){ - close(fd); - continue; + if(listener){ + status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } + } + else{ + status = connect(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } } break; @@ -107,11 +118,11 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ //set nonblocking #ifdef _WIN32 - u_long mode = 1; - if(ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR){ - closesocket(fd); - return 1; - } + u_long mode = 1; + if(ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR){ + closesocket(fd); + return 1; + } #else int flags = fcntl(fd, F_GETFL, 0); if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ @@ -123,3 +134,20 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ return fd; } + +int mmbackend_send(int fd, uint8_t* data, size_t length){ + ssize_t total = 0, sent; + while(total < length){ + sent = send(fd, data + total, length - total, 0); + if(sent < 0){ + fprintf(stderr, "Failed to send: %s\n", strerror(errno)); + return 1; + } + total += sent; + } + return 0; +} + +int mmbackend_send_str(int fd, char* data){ + return mmbackend_send(fd, (uint8_t*) data, strlen(data)); +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 77cad6a..31c4b96 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -16,7 +16,8 @@ #include #include "../portability.h" -/* Parse spec as host specification in the form +/* + * Parse spec as host specification in the form * host port * into its constituent parts. * Returns offsets into the original string and modifies it. @@ -25,13 +26,26 @@ */ void mmbackend_parse_hostspec(char* spec, char** host, char** port); -/* Parse a given host / port combination into a sockaddr_storage +/* + * Parse a given host / port combination into a sockaddr_storage * suitable for usage with connect / sendto * Returns 0 on success */ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len); -/* Create a socket of given type and mode for a bind / connect host. +/* + * Create a socket of given type and mode for a bind / connect host. * Returns -1 on failure, a valid file descriptor for the socket on success. */ -int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener); +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast); + +/* + * Send arbitrary data over multiple writes if necessary + * Returns 1 on failure, 0 on success. + */ +int mmbackend_send(int fd, uint8_t* data, size_t length); + +/* + * Wraps mmbackend_send for cstrings + */ +int mmbackend_send_str(int fd, char* data); diff --git a/backends/lua.md b/backends/lua.md index 1c67477..6ad5c2a 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -37,7 +37,7 @@ Input values range between 0.0 and 1.0, output values are clamped to the same ra #### Global configuration -The backend does not take any global configuration. +The `lua` backend does not take any global configuration. #### Instance configuration diff --git a/backends/osc.c b/backends/osc.c index 03e431f..77bbde4 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -525,7 +525,7 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ return 1; } - data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(data->fd < 0){ fprintf(stderr, "Failed to bind for instance %s\n", inst->name); return 1; diff --git a/backends/sacn.c b/backends/sacn.c index 6f7d1a5..2f418e5 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -60,7 +60,7 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(fd < 0){ return -1; } -- cgit v1.2.3 From bb6111986bf7a997055287b916d0822957c5d13c Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 11 Aug 2019 20:29:17 +0200 Subject: Initial maweb backend --- README.md | 1 + TODO | 6 + backends/Makefile | 2 + backends/libmmbackend.c | 217 +++++++++++++++ backends/libmmbackend.h | 75 ++++++ backends/maweb.c | 695 ++++++++++++++++++++++++++++++++++++++++++++++++ backends/maweb.h | 69 +++++ backends/maweb.md | 142 ++++++++++ midimonster.c | 3 + monster.cfg | 29 +- 10 files changed, 1238 insertions(+), 1 deletion(-) create mode 100644 backends/maweb.c create mode 100644 backends/maweb.h create mode 100644 backends/maweb.md (limited to 'backends/libmmbackend.h') diff --git a/README.md b/README.md index 8a0d7f9..3e9bb88 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ support for the protocols to translate. * `liblua5.3-dev` (for the lua backend) * `libola-dev` (for the optional OLA backend) * `pkg-config` (as some projects and systems like to spread their files around) +* `libssl-dev` (for the MA Web Remote backend) * A C compiler * GNUmake diff --git a/TODO b/TODO index 5f4ce91..d04773b 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,12 @@ MIDI NRPN +keepalive channels per backend? +mm_backend_start might get some arguments so they don't have to fetch them all the time +mm_channel_resolver might get additional info about the mapping direction Note source in channel value struct Optimize core channel search (store backend offset) Printing backend / Verbose mode mm_managed_fd.impl is not freed currently + +rtpmidi mode=peer + mode=initiator diff --git a/backends/Makefile b/backends/Makefile index 2635ddc..582655c 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -33,8 +33,10 @@ sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: LDLIBS += -lws2_32 maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +maweb.so: LDLIBS = -lssl maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) maweb.dll: LDLIBS += -lws2_32 +maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 2fd3b8b..c98cfe3 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -151,3 +151,220 @@ int mmbackend_send(int fd, uint8_t* data, size_t length){ int mmbackend_send_str(int fd, char* data){ return mmbackend_send(fd, (uint8_t*) data, strlen(data)); } + +json_type json_identify(char* json, size_t length){ + size_t n; + + //skip leading blanks + for(n = 0; json[n] && n < length && isspace(json[n]); n++){ + } + + if(n == length){ + return JSON_INVALID; + } + + switch(json[n]){ + case '{': + return JSON_OBJECT; + case '[': + return JSON_ARRAY; + case '"': + return JSON_STRING; + case '-': + case '+': + return JSON_NUMBER; + default: + //true false null number + if(!strncmp(json + n, "true", 4) + || !strncmp(json + n, "false", 5)){ + return JSON_BOOL; + } + else if(!strncmp(json + n, "null", 4)){ + return JSON_NULL; + } + //a bit simplistic but it should do + if(isdigit(json[n])){ + return JSON_NUMBER; + } + } + return JSON_INVALID; +} + +size_t json_validate(char* json, size_t length){ + switch(json_identify(json, length)){ + case JSON_STRING: + return json_validate_string(json, length); + case JSON_ARRAY: + return json_validate_array(json, length); + case JSON_OBJECT: + return json_validate_object(json, length); + case JSON_INVALID: + return 0; + default: + return json_validate_value(json, length); + } +} + +size_t json_validate_string(char* json, size_t length){ + size_t string_length = 0, offset; + + for(offset = 0; json[offset] && offset < length && json[offset] != '"'; offset++){ + } + + if(offset == length){ + return 0; + } + + //find terminating quotation mark not preceded by escape + for(string_length = 1; offset + string_length < length + && isprint(json[offset + string_length]) + && (json[offset + string_length] != '"' || json[offset + string_length - 1] == '\\'); string_length++){ + } + + //complete string found + if(json[offset + string_length] == '"' && json[offset + string_length - 1] != '\\'){ + return offset + string_length + 1; + } + + return 0; +} + +size_t json_validate_array(char* json, size_t length){ + //TODO + return 0; +} + +size_t json_validate_object(char* json, size_t length){ + //TODO + return 0; +} + +size_t json_validate_value(char* json, size_t length){ + //TODO + return 0; +} + +size_t json_obj_offset(char* json, char* key){ + size_t offset = 0; + uint8_t match = 0; + + //skip whitespace + for(offset = 0; json[offset] && isspace(json[offset]); offset++){ + } + + if(json[offset] != '{'){ + return 0; + } + offset++; + + while(json_identify(json + offset, strlen(json + offset)) == JSON_STRING){ + //skip to key begin + for(; json[offset] && json[offset] != '"'; offset++){ + } + + if(!strncmp(json + offset + 1, key, strlen(key)) && json[offset + 1 + strlen(key)] == '"'){ + //key found + match = 1; + } + + offset += json_validate_string(json + offset, strlen(json + offset)); + + //skip to value separator + for(; json[offset] && json[offset] != ':'; offset++){ + } + + //skip whitespace + for(offset++; json[offset] && isspace(json[offset]); offset++){ + } + + if(match){ + return offset; + } + + //add length of value + offset += json_validate(json + offset, strlen(json + offset)); + + //find comma or closing brace + for(; json[offset] && json[offset] != ',' && json[offset] != '}'; offset++){ + } + + if(json[offset] == ','){ + offset++; + } + } + + return 0; +} + +json_type json_obj(char* json, char* key){ + size_t offset = json_obj_offset(json, key); + if(offset){ + return json_identify(json + offset, strlen(json + offset)); + } + return JSON_INVALID; +} + +uint8_t json_obj_bool(char* json, char* key, uint8_t fallback){ + size_t offset = json_obj_offset(json, key); + if(offset){ + if(!strncmp(json + offset, "true", 4)){ + return 1; + } + if(!strncmp(json + offset, "false", 5)){ + return 0; + } + } + return fallback; +} + +int64_t json_obj_int(char* json, char* key, int64_t fallback){ + char* next_token = NULL; + int64_t result; + size_t offset = json_obj_offset(json, key); + if(offset){ + result = strtol(json + offset, &next_token, 10); + if(next_token != json + offset){ + return result; + } + } + return fallback; +} + +double json_obj_double(char* json, char* key, double fallback){ + char* next_token = NULL; + int64_t result; + size_t offset = json_obj_offset(json, key); + if(offset){ + result = strtod(json + offset, &next_token); + if(next_token != json + offset){ + return result; + } + } + return fallback; +} + +char* json_obj_str(char* json, char* key, size_t* length){ + size_t offset = json_obj_offset(json, key), raw_length; + if(offset){ + raw_length = json_validate_string(json + offset, strlen(json + offset)); + if(length){ + *length = raw_length - 2; + } + return json + offset + 1; + } + return NULL; +} + +char* json_obj_strdup(char* json, char* key){ + size_t offset = json_obj_offset(json, key), raw_length; + char* rv = NULL; + if(offset){ + raw_length = json_validate_string(json + offset, strlen(json + offset)); + rv = calloc(raw_length - 1, sizeof(char)); + if(rv){ + memcpy(rv, json + offset + 1, raw_length - 2); + } + return rv; + } + return NULL; +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 31c4b96..aa0ac0c 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -16,6 +16,10 @@ #include #include "../portability.h" +/*** BACKEND IMPLEMENTATION LIBRARY ***/ + +/** Networking functions **/ + /* * Parse spec as host specification in the form * host port @@ -49,3 +53,74 @@ int mmbackend_send(int fd, uint8_t* data, size_t length); * Wraps mmbackend_send for cstrings */ int mmbackend_send_str(int fd, char* data); + + +/** JSON parsing **/ + +typedef enum /*_json_types*/ { + JSON_INVALID = 0, + JSON_STRING, + JSON_ARRAY, + JSON_OBJECT, + JSON_NUMBER, + JSON_BOOL, + JSON_NULL +} json_type; + +/* + * Try to identify the type of JSON data next in the buffer + * Will access at most the next `length` bytes + */ +json_type json_identify(char* json, size_t length); + +/* + * Validate that a buffer contains a valid JSON document/data within `length` bytes + * Returns the length of a detected JSON document, 0 otherwise (ie. parse failures) + */ +size_t json_validate(char* json, size_t length); + +size_t json_validate_string(char* json, size_t length); + +size_t json_validate_array(char* json, size_t length); + +size_t json_validate_object(char* json, size_t length); + +size_t json_validate_value(char* json, size_t length); + +/* + * Calculate offset for value of `key` + * Assumes a zero-terminated, validated JSON object as input + * Returns offset on success, 0 on failure + */ +size_t json_obj_offset(char* json, char* key); + +/* + * Check for for a key within a JSON object + * Assumes a zero-terminated, validated JSON object as input + * Returns type of value + */ +json_type json_obj(char* json, char* key); + +//json_type json_array(char* json, size_t index) + +/* + * Fetch boolean value for an object key + * Assumes a zero-terminated, validated JSON object as input + */ +uint8_t json_obj_bool(char* json, char* key, uint8_t fallback); + +/* + * Fetch integer/double value for an object key + * Assumes a zero-terminated validated JSON object as input + */ +int64_t json_obj_int(char* json, char* key, int64_t fallback); +double json_obj_double(char* json, char* key, double fallback); + +/* + * Fetch a string value for an object key + * Assumes a zero-terminated validated JSON object as input + * json_obj_strdup returns a newly-allocated buffer containing + * only the requested value + */ +char* json_obj_str(char* json, char* key, size_t* length); +char* json_obj_strdup(char* json, char* key); diff --git a/backends/maweb.c b/backends/maweb.c new file mode 100644 index 0000000..be4c2ac --- /dev/null +++ b/backends/maweb.c @@ -0,0 +1,695 @@ +#include +#include +#include +#ifndef MAWEB_NO_LIBSSL +#include +#endif + +#include "libmmbackend.h" +#include "maweb.h" + +#define BACKEND_NAME "maweb" +#define WS_LEN(a) ((a) & 0x7F) +#define WS_OP(a) ((a) & 0x0F) +#define WS_FLAG_FIN 0x80 +#define WS_FLAG_MASK 0x80 + +static uint64_t last_keepalive = 0; + +static char* cmdline_keys[] = { + "SET", + "PREV", + "NEXT", + "CLEAR", + "FIXTURE_CHANNEL", + "FIXTURE_GROUP_PRESET", + "EXEC_CUE", + "STORE_UPDATE", + "OOPS", + "ESC", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "PUNKT", + "PLUS", + "MINUS", + "THRU", + "IF", + "AT", + "FULL", + "HIGH", + "ENTER", + "OFF", + "ON", + "ASSIGN", + "LABEL", + "COPY", + "TIME", + "PAGE", + "MACRO", + "DELETE", + "GOTO", + "GO_PLUS", + "GO_MINUS", + "PAUSE", + "SELECT", + "FIXTURE", + "SEQU", + "CUE", + "PRESET", + "EDIT", + "UPDATE", + "EXEC", + "STORE", + "GROUP", + "PROG_ONLY", + "SPECIAL_DIALOGUE", + "SOLO", + "ODD", + "EVEN", + "WINGS", + "RESET", + "MA", + "layerMode", + "featureSort", + "fixtureSort", + "channelSort", + "hideName" +}; + +int init(){ + backend maweb = { + .name = BACKEND_NAME, + .conf = maweb_configure, + .create = maweb_instance, + .conf_instance = maweb_configure_instance, + .channel = maweb_channel, + .handle = maweb_set, + .process = maweb_handle, + .start = maweb_start, + .shutdown = maweb_shutdown + }; + + if(sizeof(maweb_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "maweb channel identification union out of bounds\n"); + return 1; + } + + //register backend + if(mm_backend_register(maweb)){ + fprintf(stderr, "Failed to register maweb backend\n"); + return 1; + } + return 0; +} + +static int maweb_configure(char* option, char* value){ + fprintf(stderr, "The maweb backend does not take any global configuration\n"); + return 1; +} + +static int maweb_configure_instance(instance* inst, char* option, char* value){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + char* host = NULL, *port = NULL; + #ifndef MAWEB_NO_LIBSSL + uint8_t password_hash[MD5_DIGEST_LENGTH]; + #endif + + if(!strcmp(option, "host")){ + mmbackend_parse_hostspec(value, &host, &port); + if(!host){ + fprintf(stderr, "Invalid host specified for maweb instance %s\n", inst->name); + return 1; + } + free(data->host); + data->host = strdup(host); + free(data->port); + data->port = NULL; + if(port){ + data->port = strdup(port); + } + return 0; + } + else if(!strcmp(option, "user")){ + free(data->user); + data->user = strdup(value); + return 0; + } + else if(!strcmp(option, "password")){ + #ifndef MAWEB_NO_LIBSSL + size_t n; + MD5((uint8_t*) value, strlen(value), (uint8_t*) password_hash); + data->pass = realloc(data->pass, (2 * MD5_DIGEST_LENGTH + 1) * sizeof(char)); + for(n = 0; n < MD5_DIGEST_LENGTH; n++){ + snprintf(data->pass + 2 * n, 3, "%02x", password_hash[n]); + } + return 0; + #else + fprintf(stderr, "This build of the maweb backend only supports the default password\n"); + return 1; + #endif + } + + fprintf(stderr, "Unknown configuration parameter %s for manet instance %s\n", option, inst->name); + return 1; +} + +static instance* maweb_instance(){ + instance* inst = mm_instance(); + if(!inst){ + return NULL; + } + + maweb_instance_data* data = calloc(1, sizeof(maweb_instance_data)); + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + data->fd = -1; + data->buffer = calloc(MAWEB_RECV_CHUNK, sizeof(uint8_t)); + if(!data->buffer){ + fprintf(stderr, "Failed to allocate memory\n"); + free(data); + return NULL; + } + data->allocated = MAWEB_RECV_CHUNK; + + inst->impl = data; + return inst; +} + +static channel* maweb_channel(instance* inst, char* spec){ + maweb_channel_ident ident = { + .label = 0 + }; + char* next_token = NULL; + size_t n; + + if(!strncmp(spec, "page", 4)){ + ident.fields.page = strtoul(spec + 4, &next_token, 10); + if(*next_token != '.'){ + fprintf(stderr, "Failed to parse maweb channel spec %s: Missing separator\n", spec); + return NULL; + } + + next_token++; + if(!strncmp(next_token, "fader", 5)){ + ident.fields.type = exec_fader; + next_token += 5; + } + else if(!strncmp(next_token, "upper", 5)){ + ident.fields.type = exec_upper; + next_token += 5; + } + else if(!strncmp(next_token, "lower", 5)){ + ident.fields.type = exec_lower; + next_token += 5; + } + else if(!strncmp(next_token, "flash", 5)){ + ident.fields.type = exec_flash; + next_token += 5; + } + else if(!strncmp(next_token, "button", 6)){ + ident.fields.type = exec_fader; + next_token += 6; + } + ident.fields.index = strtoul(next_token, NULL, 10); + } + else{ + for(n = 0; n < sizeof(cmdline_keys) / sizeof(char*); n++){ + if(!strcmp(spec, cmdline_keys[n])){ + ident.fields.type = cmdline_button; + ident.fields.index = n + 1; + ident.fields.page = 1; + break; + } + } + } + + if(ident.fields.type && ident.fields.index && ident.fields.page + && ident.fields.index <= 90){ + //actually, those are zero-indexed... + ident.fields.index--; + ident.fields.page--; + return mm_channel(inst, ident.label, 1); + } + fprintf(stderr, "Failed to parse maweb channel spec %s\n", spec); + return NULL; +} + +static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload, size_t len){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + uint8_t frame_header[MAWEB_FRAME_HEADER_LENGTH] = ""; + size_t header_bytes = 2; + uint16_t* payload_len16 = (uint16_t*) (frame_header + 2); + uint64_t* payload_len64 = (uint64_t*) (frame_header + 2); + + frame_header[0] = WS_FLAG_FIN | op; + if(len <= 125){ + frame_header[1] = WS_FLAG_MASK | len; + } + else if(len <= 0xFFFF){ + frame_header[1] = WS_FLAG_MASK | 126; + *payload_len16 = htobe16(len); + header_bytes += 2; + } + else{ + frame_header[1] = WS_FLAG_MASK | 127; + *payload_len64 = htobe64(len); + header_bytes += 8; + } + //send a zero masking key because masking is stupid + header_bytes += 4; + + if(mmbackend_send(data->fd, frame_header, header_bytes) + || mmbackend_send(data->fd, payload, len)){ + return 1; + } + + return 0; +} + +static int maweb_handle_message(instance* inst, char* payload, size_t payload_length){ + char xmit_buffer[MAWEB_XMIT_CHUNK]; + char* field; + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + + fprintf(stderr, "maweb message (%lu): %s\n", payload_length, payload); + if(json_obj(payload, "session") == JSON_NUMBER){ + data->session = json_obj_int(payload, "session", data->session); + fprintf(stderr, "maweb session id is now %ld\n", data->session); + } + + if(json_obj_bool(payload, "forceLogin", 0)){ + fprintf(stderr, "maweb sending user credentials\n"); + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"login\",\"username\":\"%s\",\"password\":\"%s\",\"session\":%ld}", + data->user, data->pass, data->session); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + } + + if(json_obj(payload, "status") && json_obj(payload, "appType")){ + fprintf(stderr, "maweb connection established\n"); + maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13); + } + + if(json_obj(payload, "responseType") == JSON_STRING){ + field = json_obj_str(payload, "responseType", NULL); + if(!strncmp(field, "login", 5)){ + if(json_obj_bool(payload, "result", 0)){ + fprintf(stderr, "maweb login successful\n"); + data->login = 1; + } + else{ + fprintf(stderr, "maweb login failed\n"); + data->login = 0; + } + } + else if(!strncmp(field, "getdata", 7)){ + //FIXME stupid keepalive logic + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"getdata\"," + "\"data\":\"set,clear,solo,high\"," + "\"realtime\":true," + "\"maxRequests\":10," + ",\"session\":%ld}", + data->session); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + } + } + + return 0; +} + +static int maweb_connect(instance* inst){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + if(!data->host){ + return 1; + } + + //unregister old fd from core + if(data->fd >= 0){ + mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL); + } + + data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0); + if(data->fd < 0){ + return 1; + } + + data->state = ws_new; + if(mmbackend_send_str(data->fd, "GET /?ma=1 HTTP/1.1\r\n") + || mmbackend_send_str(data->fd, "Connection: Upgrade\r\n") + || mmbackend_send_str(data->fd, "Upgrade: websocket\r\n") + || mmbackend_send_str(data->fd, "Sec-WebSocket-Version: 13\r\n") + //the websocket key probably should not be hardcoded, but this is not security criticial + //and the whole websocket 'accept key' dance is plenty stupid as it is + || mmbackend_send_str(data->fd, "Sec-WebSocket-Key: rbEQrXMEvCm4ZUjkj6juBQ==\r\n") + || mmbackend_send_str(data->fd, "\r\n")){ + fprintf(stderr, "maweb backend failed to communicate with peer\n"); + return 1; + } + + //register new fd + if(mm_manage_fd(data->fd, BACKEND_NAME, 1, (void*) inst)){ + fprintf(stderr, "maweb backend failed to register fd\n"); + return 1; + } + return 0; +} + +static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + size_t n, begin = 0; + + for(n = 0; n < bytes_read - 2; n++){ + if(!strncmp((char*) data->buffer + data->offset + n, "\r\n", 2)){ + if(data->state == ws_new){ + if(!strncmp((char*) data->buffer, "HTTP/1.1 101", 12)){ + data->state = ws_http; + } + else{ + fprintf(stderr, "maweb received invalid HTTP response for instance %s\n", inst->name); + return -1; + } + } + else{ + //ignore all http stuff until the end of headers since we don't actually care... + if(n == begin){ + data->state = ws_open; + } + } + begin = n + 2; + } + } + + return begin; +} + +static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + size_t header_length = 2; + uint64_t payload_length = 0; + uint16_t* payload_len16 = (uint16_t*) (data->buffer + 2); + uint64_t* payload_len64 = (uint64_t*) (data->buffer + 2); + uint8_t* payload = data->buffer + 2; + uint8_t terminator_temp = 0; + + if(data->offset + bytes_read < 2){ + return 0; + } + + //using varint as payload length is stupid, but some people seem to think otherwise... + payload_length = WS_LEN(data->buffer[1]); + switch(payload_length){ + case 126: + if(data->offset + bytes_read < 4){ + return 0; + } + payload_length = htobe16(*payload_len16); + payload = data->buffer + 4; + header_length = 4; + break; + case 127: + if(data->offset + bytes_read < 10){ + return 0; + } + payload_length = htobe64(*payload_len64); + payload = data->buffer + 10; + header_length = 10; + break; + default: + break; + } + + if(data->offset + bytes_read < header_length + payload_length){ + return 0; + } + + switch(WS_OP(data->buffer[0])){ + case ws_text: + //terminate message + terminator_temp = payload[payload_length]; + payload[payload_length] = 0; + if(maweb_handle_message(inst, (char*) payload, payload_length)){ + return data->offset + bytes_read; + } + payload[payload_length] = terminator_temp; + break; + case ws_ping: + //answer server ping with a pong + if(maweb_send_frame(inst, ws_pong, payload, payload_length)){ + fprintf(stderr, "maweb failed to send pong\n"); + } + return header_length + payload_length; + default: + fprintf(stderr, "maweb encountered unhandled frame type %02X\n", WS_OP(data->buffer[0])); + //this is somewhat dicey, it might be better to handle only header + payload length for known but unhandled types + return data->offset + bytes_read; + } + + return header_length + payload_length; +} + +static int maweb_handle_fd(instance* inst){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + ssize_t bytes_read, bytes_left = data->allocated - data->offset, bytes_handled; + + if(bytes_left < 3){ + data->buffer = realloc(data->buffer, (data->allocated + MAWEB_RECV_CHUNK) * sizeof(uint8_t)); + if(!data->buffer){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + data->allocated += MAWEB_RECV_CHUNK; + bytes_left += MAWEB_RECV_CHUNK; + } + + bytes_read = recv(data->fd, data->buffer + data->offset, bytes_left - 1, 0); + if(bytes_read < 0){ + fprintf(stderr, "maweb backend failed to receive: %s\n", strerror(errno)); + //TODO close, reopen + return 1; + } + else if(bytes_read == 0){ + //client closed connection + //TODO try to reopen + return 0; + } + + do{ + switch(data->state){ + case ws_new: + case ws_http: + bytes_handled = maweb_handle_lines(inst, bytes_read); + break; + case ws_open: + bytes_handled = maweb_handle_ws(inst, bytes_read); + break; + case ws_closed: + bytes_handled = data->offset + bytes_read; + break; + } + + if(bytes_handled < 0){ + bytes_handled = data->offset + bytes_read; + //TODO close, reopen + fprintf(stderr, "maweb failed to handle incoming data\n"); + return 1; + } + else if(bytes_handled == 0){ + break; + } + + memmove(data->buffer, data->buffer + bytes_handled, (data->offset + bytes_read) - bytes_handled); + + //FIXME this might be somewhat borked + bytes_read -= data->offset; + bytes_handled -= data->offset; + bytes_read -= bytes_handled; + data->offset = 0; + } while(bytes_read > 0); + + data->offset += bytes_read; + return 0; +} + +static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + char xmit_buffer[MAWEB_XMIT_CHUNK]; + maweb_channel_ident ident; + size_t n; + + if(num && !data->login){ + fprintf(stderr, "maweb instance %s can not send output, not logged in\n", inst->name); + } + + for(n = 0; n < num; n++){ + ident.label = c[n]->ident; + switch(ident.fields.type){ + case exec_fader: + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"playbacks_userInput\"," + "\"execIndex\":%d," + "\"pageIndex\":%d," + "\"faderValue\":%f," + "\"type\":1," + "\"session\":%ld" + "}", ident.fields.index, ident.fields.page, v[n].normalised, data->session); + fprintf(stderr, "maweb out %s\n", xmit_buffer); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + break; + case exec_upper: + case exec_lower: + case exec_flash: + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"playbacks_userInput\"," + //"\"cmdline\":\"\"," + "\"execIndex\":%d," + "\"pageIndex\":%d," + "\"buttonId\":%d," + "\"pressed\":%s," + "\"released\":%s," + "\"type\":0," + "\"session\":%ld" + "}", ident.fields.index, ident.fields.page, + (exec_flash - ident.fields.type), + (v[n].normalised > 0.9) ? "true" : "false", + (v[n].normalised > 0.9) ? "false" : "true", + data->session); + fprintf(stderr, "maweb out %s\n", xmit_buffer); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + break; + case cmdline_button: + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"keyname\":\"%s\"," + //"\"autoSubmit\":false," + "\"value\":%d" + "}", cmdline_keys[ident.fields.index], + (v[n].normalised > 0.9) ? 1 : 0); + fprintf(stderr, "maweb out %s\n", xmit_buffer); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + break; + default: + fprintf(stderr, "maweb control not yet implemented\n"); + break; + } + } + return 0; +} + +static int maweb_keepalive(){ + size_t n, u; + instance** inst = NULL; + maweb_instance_data* data = NULL; + char xmit_buffer[MAWEB_XMIT_CHUNK]; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + //send keep-alive messages for logged-in instances + for(u = 0; u < n; u++){ + data = (maweb_instance_data*) inst[u]->impl; + if(data->login){ + snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"session\":%ld}", data->session); + maweb_send_frame(inst[u], ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + } + } + + free(inst); + return 0; +} + +static int maweb_handle(size_t num, managed_fd* fds){ + size_t n = 0; + int rv = 0; + + for(n = 0; n < num; n++){ + rv |= maweb_handle_fd((instance*) fds[n].impl); + } + + if(last_keepalive && mm_timestamp() - last_keepalive >= MAWEB_CONNECTION_KEEPALIVE){ + rv |= maweb_keepalive(); + last_keepalive = mm_timestamp(); + } + + return rv; +} + +static int maweb_start(){ + size_t n, u; + instance** inst = NULL; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + for(u = 0; u < n; u++){ + if(maweb_connect(inst[u])){ + fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); + return 1; + } + } + + free(inst); + if(!n){ + return 0; + } + + fprintf(stderr, "maweb backend registering %lu descriptors to core\n", n); + + //initialize keepalive timeout + last_keepalive = mm_timestamp(); + return 0; +} + +static int maweb_shutdown(){ + size_t n, u; + instance** inst = NULL; + maweb_instance_data* data = NULL; + + //fetch all instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + for(u = 0; u < n; u++){ + data = (maweb_instance_data*) inst[u]->impl; + free(data->host); + data->host = NULL; + free(data->port); + data->port = NULL; + free(data->user); + data->user = NULL; + free(data->pass); + data->pass = NULL; + + close(data->fd); + data->fd = -1; + + free(data->buffer); + data->buffer = NULL; + + data->offset = data->allocated = 0; + data->state = ws_new; + } + + free(inst); + + fprintf(stderr, "maweb backend shut down\n"); + return 0; +} diff --git a/backends/maweb.h b/backends/maweb.h new file mode 100644 index 0000000..6e6e652 --- /dev/null +++ b/backends/maweb.h @@ -0,0 +1,69 @@ +#include "midimonster.h" + +int init(); +static int maweb_configure(char* option, char* value); +static int maweb_configure_instance(instance* inst, char* option, char* value); +static instance* maweb_instance(); +static channel* maweb_channel(instance* inst, char* spec); +static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v); +static int maweb_handle(size_t num, managed_fd* fds); +static int maweb_start(); +static int maweb_shutdown(); + +//Default login password: MD5("midimonster") +#define MAWEB_DEFAULT_PASSWORD "2807623134739142b119aff358f8a219" +#define MAWEB_DEFAULT_PORT "80" +#define MAWEB_RECV_CHUNK 1024 +#define MAWEB_XMIT_CHUNK 2048 +#define MAWEB_FRAME_HEADER_LENGTH 16 +#define MAWEB_CONNECTION_KEEPALIVE 10000 + +typedef enum /*_maweb_channel_type*/ { + type_unset = 0, + exec_fader = 1, + exec_button = 2, + exec_upper = 3, + exec_lower = 4, + exec_flash = 5, + cmdline_button +} maweb_channel_type; + +typedef enum /*_ws_conn_state*/ { + ws_new, + ws_http, + ws_open, + ws_closed +} maweb_state; + +typedef enum /*_ws_frame_op*/ { + ws_text = 1, + ws_binary = 2, + ws_ping = 9, + ws_pong = 10 +} maweb_operation; + +typedef union { + struct { + uint8_t padding[3]; + uint8_t type; + uint16_t page; + uint16_t index; + } fields; + uint64_t label; +} maweb_channel_ident; + +typedef struct /*_maweb_instance_data*/ { + char* host; + char* port; + char* user; + char* pass; + + uint8_t login; + int64_t session; + + int fd; + maweb_state state; + size_t offset; + size_t allocated; + uint8_t* buffer; +} maweb_instance_data; diff --git a/backends/maweb.md b/backends/maweb.md new file mode 100644 index 0000000..eb1ed44 --- /dev/null +++ b/backends/maweb.md @@ -0,0 +1,142 @@ +### The `maweb` backend + +This backend connects directly with the integrated *MA Web Remote* of MA Lighting consoles and OnPC +instances (GrandMA2 / GrandMA2 OnPC / GrandMA Dot2 / GrandMA Dot2 OnPC). +It grants read-write access to the console's playback faders and buttons as well as write access to +the command line buttons. + +To allow this backend to connect to the console, enter the console configuration (`Setup` key), +select `Console`/`Global Settings` and set the `Remotes` option to `Login enabled`. +Create an additional user that is able to log into the Web Remote using `Setup`/`Console`/`User & Profiles Setup`. + +#### Global configuration + +The `maweb` backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|---------------------------------------------------------------| +| `host` | `10.23.42.21 80` | none | Host address (and optional port) of the MA Web Remote | +| `user` | `midimonster` | none | User for the remote session | +| `password` | `midimonster` | `midimonster` | Password for the remote session | + +#### Channel specification + +Currently, three types of channels can be assigned + +##### Executors + +Executors are arranged in pages, with each page having 90 fader executors and 90 button executors. +Note that when creating a new show, only the first page is created and active. + +A fader executor consists of a fader, two buttons (`upper`, `lower`) above it and one `flash` button below it. + +These controls can be adressed like + +``` +mw1.page1.fader5 > mw1.page1.upper5 +mw1.page3.lower3 > mw1.page2.flash2 +``` + +A button executor can likewise be mapped using the syntax + +``` +mw1.page2.button3 > mw1.page3.fader1 +``` + +##### Command line buttons + +Command line buttons will be pressed when the incoming event value is greater than `0.9` and released when it is less than that. +They can be mapped using the syntax + +``` +mw1. +``` + +The following button names are recognized by the backend: + +* `SET` +* `PREV` +* `NEXT` +* `CLEAR` +* `FIXTURE_CHANNEL` +* `FIXTURE_GROUP_PRESET` +* `EXEC_CUE` +* `STORE_UPDATE` +* `OOPS` +* `ESC` +* `0` +* `1` +* `2` +* `3` +* `4` +* `5` +* `6` +* `7` +* `8` +* `9` +* `PUNKT` +* `PLUS` +* `MINUS` +* `THRU` +* `IF` +* `AT` +* `FULL` +* `HIGH` +* `ENTER` +* `OFF` +* `ON` +* `ASSIGN` +* `LABEL` +* `COPY` +* `TIME` +* `PAGE` +* `MACRO` +* `DELETE` +* `GOTO` +* `GO_PLUS` +* `GO_MINUS` +* `PAUSE` +* `SELECT` +* `FIXTURE` +* `SEQU` +* `CUE` +* `PRESET` +* `EDIT` +* `UPDATE` +* `EXEC` +* `STORE` +* `GROUP` +* `PROG_ONLY` +* `SPECIAL_DIALOGUE` +* `SOLO` +* `ODD` +* `EVEN` +* `WINGS` +* `RESET` +* `MA` +* `layerMode` +* `featureSort` +* `fixtureSort` +* `channelSort` +* `hideName` + +Note that each Web Remote connection has it's own command line, as such commands entered using this backend will not affect +the command line on the main console. To do that, you will need to use another backend to feed input to the MA, such as +the ArtNet or MIDI backends. + +#### Known bugs / problems + +To properly encode the user password, this backend depends on a library providing cryptographic functions (`libssl` / `openssl`). +Since this may be a problem on some platforms, the backend can be built with this requirement disabled, which also disables the possibility +to set arbitrary passwords. The backend will always try to log in with the default password `midimonster` in this case. The user name is still +configurable. + +This backend is currently in active development. It therefore has some limitations: + +* It outputs a lot of debug information +* It currently is write-only, channel events are only sent to the MA, not consumed by it +* Fader executors (and their buttons) seem to work, I haven't tested button executors yet. +* Command line events are sent, but I'm not sure they're being handled yet +* I have so far only tested it with GradMA2 OnPC diff --git a/midimonster.c b/midimonster.c index 1e47698..25cf4a0 100644 --- a/midimonster.c +++ b/midimonster.c @@ -298,6 +298,9 @@ int main(int argc, char** argv){ plugins_close(); return usage(argv[0]); } + + //load an initial timestamp + update_timestamp(); //start backends if(backends_start()){ diff --git a/monster.cfg b/monster.cfg index 2413f6d..d272cee 100644 --- a/monster.cfg +++ b/monster.cfg @@ -7,6 +7,33 @@ bind = 0.0.0.0 universe = 0 dest = 255.255.255.255 +[backend midi] +detect = on + +[backend evdev] +;detect = on + +[midi bcf] +read = BCF +write = BCF + +[evdev mouse] +input = TPPS +relaxis.REL_X = 255 +relaxis.REL_Y = -255 + +[maweb ma] +;host = 10.23.23.248 +host = 127.0.0.1 4040 +user = web +password = web + [map] +bcf.channel{0..7}.pitch > bcf.channel{0..7}.pitch +bcf.channel{0..7}.pitch > art.{1..8} -art.1+2 > loop.b +bcf.channel{0..7}.pitch > ma.page1.fader{1..8} +bcf.channel0.note{16..23} > ma.page1.upper{1..8} +bcf.channel0.note{24..31} > ma.page1.lower{1..8} +mouse.EV_REL.REL_Y > ma.page1.fader1 +mouse.EV_KEY.BTN_LEFT > ma.ASSIGN -- cgit v1.2.3 From 8b016f61a4b3d3be0c7b1e311209ab991276af0c Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 22 Aug 2019 21:13:48 +0200 Subject: Implement input for the maweb backend (with a few limitations) --- README.md | 2 +- backends/Makefile | 4 +- backends/evdev.h | 3 +- backends/libmmbackend.c | 241 ++++++++++++++++++++++++++++++-- backends/libmmbackend.h | 33 ++--- backends/maweb.c | 358 ++++++++++++++++++++++++++++++++++++++++-------- backends/maweb.h | 14 +- backends/maweb.md | 36 +++-- backends/midi.h | 3 +- backends/osc.h | 3 +- 10 files changed, 582 insertions(+), 115 deletions(-) (limited to 'backends/libmmbackend.h') diff --git a/README.md b/README.md index 4d8fe18..40f8fbb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ on any other (or the same) supported protocol, for example to: * Translate MIDI Control Changes into Notes ([Example configuration](configs/unifest-17.cfg)) * Translate MIDI Notes into ArtNet or sACN ([Example configuration](configs/launchctl-sacn.cfg)) * Translate OSC messages into MIDI ([Example configuration](configs/midi-osc.cfg)) -* Dynamically route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller or run effects on TouchOSC (Flying faders demo [configuration](configs/flying-faders.cfg) and [script](configs/flying-faders.lua)) +* Dynamically generate, route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller or run effects on TouchOSC (Flying faders demo [configuration](configs/flying-faders.cfg) and [script](configs/flying-faders.lua)) * Use an OSC app as a simple lighting controller via ArtNet or sACN * Visualize ArtNet data using OSC tools * Control lighting fixtures or DAWs using gamepad controllers, trackballs, etc ([Example configuration](configs/evdev.cfg)) diff --git a/backends/Makefile b/backends/Makefile index 582655c..5c5b677 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 -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o diff --git a/backends/evdev.h b/backends/evdev.h index b26664b..48bd0ab 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -52,4 +52,5 @@ typedef union { uint16_t code; } fields; uint64_t label; -} evdev_channel_ident; \ No newline at end of file +} evdev_channel_ident; + diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index c98cfe3..ccbeb52 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -208,10 +208,11 @@ size_t json_validate(char* json, size_t length){ size_t json_validate_string(char* json, size_t length){ size_t string_length = 0, offset; - for(offset = 0; json[offset] && offset < length && json[offset] != '"'; offset++){ + //skip leading whitespace + for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){ } - if(offset == length){ + if(offset == length || json[offset] != '"'){ return 0; } @@ -230,17 +231,122 @@ size_t json_validate_string(char* json, size_t length){ } size_t json_validate_array(char* json, size_t length){ - //TODO + size_t offset = 0; + + //skip leading whitespace + for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){ + } + + if(offset == length || json[offset] != '['){ + return 0; + } + + for(offset++; offset < length; offset++){ + offset += json_validate(json + offset, length - offset); + + //skip trailing whitespace, find terminator + for(; offset < length && isspace(json[offset]); offset++){ + } + + if(json[offset] == ','){ + continue; + } + + if(json[offset] == ']'){ + return offset + 1; + } + + break; + } + return 0; } size_t json_validate_object(char* json, size_t length){ - //TODO + size_t offset = 0; + + //skip whitespace + for(offset = 0; json[offset] && isspace(json[offset]); offset++){ + } + + if(offset == length || json[offset] != '{'){ + return 0; + } + + for(offset++; offset < length; offset++){ + if(json_identify(json + offset, length - offset) != JSON_STRING){ + //still could be an empty object... + for(; offset < length && isspace(json[offset]); offset++){ + } + if(json[offset] == '}'){ + return offset + 1; + } + return 0; + } + offset += json_validate(json + offset, length - offset); + + //find value separator + for(; offset < length && isspace(json[offset]); offset++){ + } + + if(json[offset] != ':'){ + return 0; + } + + offset++; + offset += json_validate(json + offset, length - offset); + + //skip trailing whitespace + for(; json[offset] && isspace(json[offset]); offset++){ + } + + if(json[offset] == '}'){ + return offset + 1; + } + else if(json[offset] != ','){ + return 0; + } + } return 0; } size_t json_validate_value(char* json, size_t length){ - //TODO + size_t offset = 0, value_length; + + //skip leading whitespace + for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){ + } + + if(offset == length){ + return 0; + } + + //match complete values + if(length - offset >= 4 && !strncmp(json + offset, "null", 4)){ + return offset + 4; + } + else if(length - offset >= 4 && !strncmp(json + offset, "true", 4)){ + return offset + 4; + } + else if(length - offset >= 5 && !strncmp(json + offset, "false", 5)){ + return offset + 5; + } + + if(json[offset] == '-' || isdigit(json[offset])){ + //json number parsing is dumb. + for(value_length = 1; offset + value_length < length && + (isdigit(json[offset + value_length]) + || json[offset + value_length] == '+' + || json[offset + value_length] == '-' + || json[offset + value_length] == '.' + || tolower(json[offset + value_length]) == 'e'); value_length++){ + } + + if(value_length > 0){ + return offset + value_length; + } + } + return 0; } @@ -284,13 +390,51 @@ size_t json_obj_offset(char* json, char* key){ //add length of value offset += json_validate(json + offset, strlen(json + offset)); - //find comma or closing brace - for(; json[offset] && json[offset] != ',' && json[offset] != '}'; offset++){ + //skip trailing whitespace + for(; json[offset] && isspace(json[offset]); offset++){ } if(json[offset] == ','){ offset++; + continue; + } + + break; + } + + return 0; +} + +size_t json_array_offset(char* json, uint64_t key){ + size_t offset = 0, index = 0; + + //skip leading whitespace + for(offset = 0; json[offset] && isspace(json[offset]); offset++){ + } + + if(json[offset] != '['){ + return 0; + } + + for(offset++; index <= key; offset++){ + //skip whitespace + for(; json[offset] && isspace(json[offset]); offset++){ + } + + if(index == key){ + return offset; + } + + offset += json_validate(json + offset, strlen(json + offset)); + + //skip trailing whitespace, find terminator + for(; json[offset] && isspace(json[offset]); offset++){ } + + if(json[offset] != ','){ + break; + } + index++; } return 0; @@ -304,6 +448,14 @@ json_type json_obj(char* json, char* key){ return JSON_INVALID; } +json_type json_array(char* json, uint64_t key){ + size_t offset = json_array_offset(json, key); + if(offset){ + return json_identify(json + offset, strlen(json + offset)); + } + return JSON_INVALID; +} + uint8_t json_obj_bool(char* json, char* key, uint8_t fallback){ size_t offset = json_obj_offset(json, key); if(offset){ @@ -317,6 +469,19 @@ uint8_t json_obj_bool(char* json, char* key, uint8_t fallback){ return fallback; } +uint8_t json_array_bool(char* json, uint64_t key, uint8_t fallback){ + size_t offset = json_array_offset(json, key); + if(offset){ + if(!strncmp(json + offset, "true", 4)){ + return 1; + } + if(!strncmp(json + offset, "false", 5)){ + return 0; + } + } + return fallback; +} + int64_t json_obj_int(char* json, char* key, int64_t fallback){ char* next_token = NULL; int64_t result; @@ -332,7 +497,7 @@ int64_t json_obj_int(char* json, char* key, int64_t fallback){ double json_obj_double(char* json, char* key, double fallback){ char* next_token = NULL; - int64_t result; + double result; size_t offset = json_obj_offset(json, key); if(offset){ result = strtod(json + offset, &next_token); @@ -343,6 +508,32 @@ double json_obj_double(char* json, char* key, double fallback){ return fallback; } +int64_t json_array_int(char* json, uint64_t key, int64_t fallback){ + char* next_token = NULL; + int64_t result; + size_t offset = json_array_offset(json, key); + if(offset){ + result = strtol(json + offset, &next_token, 10); + if(next_token != json + offset){ + return result; + } + } + return fallback; +} + +double json_array_double(char* json, uint64_t key, double fallback){ + char* next_token = NULL; + double result; + size_t offset = json_array_offset(json, key); + if(offset){ + result = strtod(json + offset, &next_token); + if(next_token != json + offset){ + return result; + } + } + return fallback; +} + char* json_obj_str(char* json, char* key, size_t* length){ size_t offset = json_obj_offset(json, key), raw_length; if(offset){ @@ -356,15 +547,37 @@ char* json_obj_str(char* json, char* key, size_t* length){ } char* json_obj_strdup(char* json, char* key){ - size_t offset = json_obj_offset(json, key), raw_length; - char* rv = NULL; + size_t len = 0; + char* value = json_obj_str(json, key, &len), *rv = NULL; + if(len){ + rv = calloc(len + 1, sizeof(char)); + if(rv){ + memcpy(rv, value, len); + } + } + return rv; +} + +char* json_array_str(char* json, uint64_t key, size_t* length){ + size_t offset = json_array_offset(json, key), raw_length; if(offset){ raw_length = json_validate_string(json + offset, strlen(json + offset)); - rv = calloc(raw_length - 1, sizeof(char)); - if(rv){ - memcpy(rv, json + offset + 1, raw_length - 2); + if(length){ + *length = raw_length - 2; } - return rv; + return json + offset + 1; } return NULL; } + +char* json_array_strdup(char* json, uint64_t key){ + size_t len = 0; + char* value = json_array_str(json, key, &len), *rv = NULL; + if(len){ + rv = calloc(len + 1, sizeof(char)); + if(rv){ + memcpy(rv, value, len); + } + } + return rv; +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index aa0ac0c..5749119 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -78,49 +78,50 @@ json_type json_identify(char* json, size_t length); * Returns the length of a detected JSON document, 0 otherwise (ie. parse failures) */ size_t json_validate(char* json, size_t length); - size_t json_validate_string(char* json, size_t length); - size_t json_validate_array(char* json, size_t length); - size_t json_validate_object(char* json, size_t length); - size_t json_validate_value(char* json, size_t length); /* * Calculate offset for value of `key` - * Assumes a zero-terminated, validated JSON object as input + * Assumes a zero-terminated, validated JSON object / array as input * Returns offset on success, 0 on failure */ size_t json_obj_offset(char* json, char* key); +size_t json_array_offset(char* json, uint64_t key); /* - * Check for for a key within a JSON object - * Assumes a zero-terminated, validated JSON object as input + * Check for for a key within a JSON object / index within an array + * Assumes a zero-terminated, validated JSON object / array as input * Returns type of value */ json_type json_obj(char* json, char* key); - -//json_type json_array(char* json, size_t index) +json_type json_array(char* json, uint64_t key); /* - * Fetch boolean value for an object key - * Assumes a zero-terminated, validated JSON object as input + * Fetch boolean value for an object / array key + * Assumes a zero-terminated, validated JSON object / array as input */ uint8_t json_obj_bool(char* json, char* key, uint8_t fallback); +uint8_t json_array_bool(char* json, uint64_t key, uint8_t fallback); /* - * Fetch integer/double value for an object key - * Assumes a zero-terminated validated JSON object as input + * Fetch integer/double value for an object / array key + * Assumes a zero-terminated validated JSON object / array as input */ int64_t json_obj_int(char* json, char* key, int64_t fallback); double json_obj_double(char* json, char* key, double fallback); +int64_t json_array_int(char* json, uint64_t key, int64_t fallback); +double json_array_double(char* json, uint64_t key, double fallback); /* - * Fetch a string value for an object key - * Assumes a zero-terminated validated JSON object as input - * json_obj_strdup returns a newly-allocated buffer containing + * Fetch a string value for an object / array key + * Assumes a zero-terminated validated JSON object / array as input + * json_*_strdup returns a newly-allocated buffer containing * only the requested value */ char* json_obj_str(char* json, char* key, size_t* length); char* json_obj_strdup(char* json, char* key); +char* json_array_str(char* json, uint64_t key, size_t* length); +char* json_array_strdup(char* json, uint64_t key); diff --git a/backends/maweb.c b/backends/maweb.c index 79e223f..07595be 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -14,7 +14,13 @@ #define WS_FLAG_FIN 0x80 #define WS_FLAG_MASK 0x80 +//TODO test using different pages simultaneously +//TODO test dot2 button virtual faders in fader view + static uint64_t last_keepalive = 0; +static uint64_t update_interval = 50; +static uint64_t last_update = 0; +static uint64_t updates_inflight = 0; static char* cmdline_keys[] = { "SET", @@ -94,7 +100,8 @@ int init(){ .handle = maweb_set, .process = maweb_handle, .start = maweb_start, - .shutdown = maweb_shutdown + .shutdown = maweb_shutdown, + .interval = maweb_interval }; if(sizeof(maweb_channel_ident) != sizeof(uint64_t)){ @@ -110,8 +117,27 @@ int init(){ return 0; } +static int channel_comparator(const void* raw_a, const void* raw_b){ + maweb_channel_ident* a = (maweb_channel_ident*) raw_a; + maweb_channel_ident* b = (maweb_channel_ident*) raw_b; + + if(a->fields.page != b->fields.page){ + return a->fields.page - b->fields.page; + } + return a->fields.index - b->fields.index; +} + +static uint32_t maweb_interval(){ + return update_interval - (last_update % update_interval); +} + static int maweb_configure(char* option, char* value){ - fprintf(stderr, "The maweb backend does not take any global configuration\n"); + if(!strcmp(option, "interval")){ + update_interval = strtoul(value, NULL, 10); + return 0; + } + + fprintf(stderr, "Unknown maweb backend configuration option %s\n", option); return 1; } @@ -187,6 +213,7 @@ static instance* maweb_instance(){ } static channel* maweb_channel(instance* inst, char* spec){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; maweb_channel_ident ident = { .label = 0 }; @@ -214,7 +241,7 @@ static channel* maweb_channel(instance* inst, char* spec){ next_token += 5; } else if(!strncmp(next_token, "flash", 5)){ - ident.fields.type = exec_flash; + ident.fields.type = exec_button; next_token += 5; } else if(!strncmp(next_token, "button", 6)){ @@ -238,6 +265,24 @@ static channel* maweb_channel(instance* inst, char* spec){ //actually, those are zero-indexed... ident.fields.index--; ident.fields.page--; + + //check if the channel is already known + for(n = 0; n < data->input_channels; n++){ + if(data->input_channel[n].label == ident.label){ + break; + } + } + + if(n == data->input_channels){ + data->input_channel = realloc(data->input_channel, (data->input_channels + 1) * sizeof(maweb_channel_ident)); + if(!data->input_channel){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + data->input_channel[n].label = ident.label; + data->input_channels++; + } + return mm_channel(inst, ident.label, 1); } fprintf(stderr, "Failed to parse maweb channel spec %s\n", spec); @@ -276,11 +321,216 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload return 0; } +static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_type metatype, char* payload, size_t payload_length){ + size_t exec_blocks = json_obj_offset(payload, (metatype == 2) ? "executorBlocks" : "bottomButtons"), offset, block = 0, control; + channel* chan = NULL; + channel_value evt; + maweb_channel_ident ident = { + .fields.page = page, + .fields.index = json_obj_int(payload, "iExec", 191) + }; + + if(!exec_blocks){ + if(metatype == 3){ + //ignore unused buttons + return 0; + } + fprintf(stderr, "maweb missing exec block data on exec %d\n", ident.fields.index); + return 1; + } + + if(metatype == 3){ + exec_blocks += json_obj_offset(payload + exec_blocks, "items"); + } + + //TODO detect unused faders + //TODO state tracking for fader values / exec run state + + //iterate over executor blocks + for(offset = json_array_offset(payload + exec_blocks, block); offset; offset = json_array_offset(payload + exec_blocks, block)){ + control = exec_blocks + offset + json_obj_offset(payload + exec_blocks + offset, "fader"); + ident.fields.type = exec_fader; + chan = mm_channel(inst, ident.label, 0); + if(chan){ + evt.normalised = json_obj_double(payload + control, "v", 0.0); + mm_channel_event(chan, evt); + } + + ident.fields.type = exec_button; + chan = mm_channel(inst, ident.label, 0); + if(chan){ + evt.normalised = json_obj_int(payload, "isRun", 0); + mm_channel_event(chan, evt); + } + + //printf("maweb page %ld exec %d value %f running %lu\n", page, ident.fields.index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); + ident.fields.index++; + block++; + } + + return 0; +} + +static int maweb_process_playbacks(instance* inst, int64_t page, char* payload, size_t payload_length){ + size_t base_offset = json_obj_offset(payload, "itemGroups"), group_offset, subgroup_offset, item_offset; + uint64_t group = 0, subgroup, item, metatype; + + if(!page){ + fprintf(stderr, "maweb received playbacks for invalid page\n"); + return 0; + } + + if(!base_offset){ + fprintf(stderr, "maweb playback data missing item key\n"); + return 0; + } + + //iterate .itemGroups + for(group_offset = json_array_offset(payload + base_offset, group); + group_offset; + group_offset = json_array_offset(payload + base_offset, group)){ + metatype = json_obj_int(payload + base_offset + group_offset, "itemsType", 0); + //iterate .itemGroups.items + //FIXME this is problematic if there is no "items" key + group_offset = group_offset + json_obj_offset(payload + base_offset + group_offset, "items"); + if(group_offset){ + subgroup = 0; + group_offset += base_offset; + for(subgroup_offset = json_array_offset(payload + group_offset, subgroup); + subgroup_offset; + subgroup_offset = json_array_offset(payload + group_offset, subgroup)){ + //iterate .itemGroups.items[n] + item = 0; + subgroup_offset += group_offset; + for(item_offset = json_array_offset(payload + subgroup_offset, item); + item_offset; + item_offset = json_array_offset(payload + subgroup_offset, item)){ + maweb_process_playback(inst, page, metatype, + payload + subgroup_offset + item_offset, + payload_length - subgroup_offset - item_offset); + item++; + } + subgroup++; + } + } + group++; + } + updates_inflight--; + fprintf(stderr, "maweb playback message processing done, %lu updates inflight\n", updates_inflight); + return 0; +} + +static int maweb_request_playbacks(instance* inst){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + char xmit_buffer[MAWEB_XMIT_CHUNK]; + int rv = 0; + + char item_indices[1024] = "[0,100,200]", item_counts[1024] = "[21,21,21]", item_types[1024] = "[2,3,3]"; + //char item_indices[1024] = "[300,400]", item_counts[1024] = "[18,18]", item_types[1024] = "[3,3]"; + size_t page_index = 0, view = 2, channel = 0, offsets[3], channel_offset, channels; + + if(updates_inflight){ + fprintf(stderr, "maweb skipping update request, %lu updates still inflight\n", updates_inflight); + return 0; + } + + for(channel = 0; channel < data->input_channels; channel++){ + offsets[0] = offsets[1] = offsets[2] = 0; + page_index = data->input_channel[channel].fields.page; + if(data->peer_type == peer_dot2){ + //TODO implement poll segmentation for dot + //"\"startIndex\":[0,100,200]," + //"\"itemsCount\":[21,21,21]," + //"\"itemsType\":[2,3,3]," + //"\"view\":2," + //view = (data->input_channel[channel].fields.index >= 300) ? 3 : 2; + //observed + //"startIndex":[300,400,500,600,700,800], + //"itemsCount":[13,13,13,13,13,13] + //"itemsType":[3,3,3,3,3,3] + /*fprintf(stderr, "range start at %lu.%lu (%lu/%lu) end at %lu.%lu (%lu/%lu)\n", + page_index, + data->input_channel[channel].fields.index, + channel, + data->input_channels, + page_index, + data->input_channel[channel + channel_offset - 1].fields.index, + channel + channel_offset - 1, + data->input_channels + );*/ + //only send one request currently + channel = data->input_channels; + } + else{ + view = (data->input_channel[channel].fields.index >= 100) ? 3 : 2; + //for the ma, the view equals the exec type + snprintf(item_types, sizeof(item_types), "[%lu]", view); + //this channel must be included, so it must be in range for the first startindex + snprintf(item_indices, sizeof(item_indices), "[%d]", (data->input_channel[channel].fields.index / 5) * 5); + + for(channel_offset = 1; channel + channel_offset < data->input_channels + && data->input_channel[channel].fields.page == data->input_channel[channel + channel_offset].fields.page + && data->input_channel[channel].fields.index / 100 == data->input_channel[channel + channel_offset].fields.index / 100; channel_offset++){ + } + + channels = data->input_channel[channel + channel_offset - 1].fields.index - (data->input_channel[channel].fields.index / 5) * 5; + + + snprintf(item_counts, sizeof(item_indices), "[%lu]", ((channels / 5) * 5 + 5)); + channel += channel_offset - 1; + } + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{" + "\"requestType\":\"playbacks\"," + "\"startIndex\":%s," + "\"itemsCount\":%s," + "\"pageIndex\":%lu," + "\"itemsType\":%s," + "\"view\":%lu," + "\"execButtonViewMode\":2," //extended + "\"buttonsViewMode\":0," //get vfader for button execs + "\"session\":%lu" + "}", + item_indices, + item_counts, + page_index, + item_types, + view, + data->session); + rv |= maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + //fprintf(stderr, "req: %s\n", xmit_buffer); + updates_inflight++; + } + + return rv; +} + static int maweb_handle_message(instance* inst, char* payload, size_t payload_length){ char xmit_buffer[MAWEB_XMIT_CHUNK]; char* field; maweb_instance_data* data = (maweb_instance_data*) inst->impl; + //query this early to save on unnecessary parser passes with stupid-huge data messages + if(json_obj(payload, "responseType") == JSON_STRING){ + field = json_obj_str(payload, "responseType", NULL); + if(!strncmp(field, "login", 5)){ + if(json_obj_bool(payload, "result", 0)){ + fprintf(stderr, "maweb login successful\n"); + data->login = 1; + } + else{ + fprintf(stderr, "maweb login failed\n"); + data->login = 0; + } + } + if(!strncmp(field, "playbacks", 9)){ + if(maweb_process_playbacks(inst, json_obj_int(payload, "iPage", 0), payload, payload_length)){ + fprintf(stderr, "maweb failed to handle/request input data\n"); + } + return 0; + } + } + fprintf(stderr, "maweb message (%lu): %s\n", payload_length, payload); if(json_obj(payload, "session") == JSON_NUMBER){ data->session = json_obj_int(payload, "session", data->session); @@ -294,7 +544,6 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le (data->peer_type == peer_dot2) ? "remote" : data->user, data->pass, data->session); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } - if(json_obj(payload, "status") && json_obj(payload, "appType")){ fprintf(stderr, "maweb connection established\n"); field = json_obj_str(payload, "appType", NULL); @@ -307,31 +556,6 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13); } - if(json_obj(payload, "responseType") == JSON_STRING){ - field = json_obj_str(payload, "responseType", NULL); - if(!strncmp(field, "login", 5)){ - if(json_obj_bool(payload, "result", 0)){ - fprintf(stderr, "maweb login successful\n"); - data->login = 1; - } - else{ - fprintf(stderr, "maweb login failed\n"); - data->login = 0; - } - } - else if(!strncmp(field, "getdata", 7)){ - //FIXME stupid keepalive logic - snprintf(xmit_buffer, sizeof(xmit_buffer), - "{\"requestType\":\"getdata\"," - "\"data\":\"set,clear,solo,high\"," - "\"realtime\":true," - "\"maxRequests\":10," - ",\"session\":%ld}", - data->session); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); - } - } - return 0; } @@ -376,7 +600,7 @@ static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; size_t n, begin = 0; - for(n = 0; n < bytes_read - 2; n++){ + for(n = 0; n < bytes_read - 1; n++){ if(!strncmp((char*) data->buffer + data->offset + n, "\r\n", 2)){ if(data->state == ws_new){ if(!strncmp((char*) data->buffer, "HTTP/1.1 101", 12)){ @@ -397,7 +621,7 @@ static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ } } - return begin; + return data->offset + begin; } static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){ @@ -507,6 +731,7 @@ static int maweb_handle_fd(instance* inst){ if(bytes_handled < 0){ bytes_handled = data->offset + bytes_read; + data->offset = 0; //TODO close, reopen fprintf(stderr, "maweb failed to handle incoming data\n"); return 1; @@ -517,8 +742,6 @@ static int maweb_handle_fd(instance* inst){ memmove(data->buffer, data->buffer + bytes_handled, (data->offset + bytes_read) - bytes_handled); - //FIXME this might be somewhat borked - bytes_read -= data->offset; bytes_handled -= data->offset; bytes_read -= bytes_handled; data->offset = 0; @@ -551,30 +774,10 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"type\":1," "\"session\":%ld" "}", ident.fields.index, ident.fields.page, v[n].normalised, data->session); - fprintf(stderr, "maweb out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; case exec_upper: case exec_lower: - case exec_flash: - snprintf(xmit_buffer, sizeof(xmit_buffer), - "{\"requestType\":\"playbacks_userInput\"," - //"\"cmdline\":\"\"," - "\"execIndex\":%d," - "\"pageIndex\":%d," - "\"buttonId\":%d," - "\"pressed\":%s," - "\"released\":%s," - "\"type\":0," - "\"session\":%ld" - "}", ident.fields.index, ident.fields.page, - (data->peer_type == peer_dot2) ? (ident.fields.type - 3) : (exec_flash - ident.fields.type), - (v[n].normalised > 0.9) ? "true" : "false", - (v[n].normalised > 0.9) ? "false" : "true", - data->session); - fprintf(stderr, "maweb out %s\n", xmit_buffer); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); - break; case exec_button: snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"requestType\":\"playbacks_userInput\"," @@ -586,13 +789,11 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"released\":%s," "\"type\":0," "\"session\":%ld" - "}", ident.fields.index, - ident.fields.page, - 0, + "}", ident.fields.index, ident.fields.page, + (data->peer_type == peer_dot2 && ident.fields.type == exec_upper) ? 0 : (ident.fields.type - exec_button), (v[n].normalised > 0.9) ? "true" : "false", (v[n].normalised > 0.9) ? "false" : "true", data->session); - fprintf(stderr, "maweb out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; case cmdline_button: @@ -602,7 +803,6 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"value\":%d" "}", cmdline_keys[ident.fields.index], (v[n].normalised > 0.9) ? 1 : 0); - fprintf(stderr, "maweb out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; default: @@ -638,6 +838,29 @@ static int maweb_keepalive(){ return 0; } +static int maweb_poll(){ + size_t n, u; + instance** inst = NULL; + maweb_instance_data* data = NULL; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + //send data polls for logged-in instances + for(u = 0; u < n; u++){ + data = (maweb_instance_data*) inst[u]->impl; + if(data->login){ + maweb_request_playbacks(inst[u]); + } + } + + free(inst); + return 0; +} + static int maweb_handle(size_t num, managed_fd* fds){ size_t n = 0; int rv = 0; @@ -646,17 +869,24 @@ static int maweb_handle(size_t num, managed_fd* fds){ rv |= maweb_handle_fd((instance*) fds[n].impl); } + //FIXME all keepalive processing allocates temporary buffers, this might an optimization target if(last_keepalive && mm_timestamp() - last_keepalive >= MAWEB_CONNECTION_KEEPALIVE){ rv |= maweb_keepalive(); last_keepalive = mm_timestamp(); } + if(last_update && mm_timestamp() - last_update >= update_interval){ + rv |= maweb_poll(); + last_update = mm_timestamp(); + } + return rv; } static int maweb_start(){ size_t n, u; instance** inst = NULL; + maweb_instance_data* data = NULL; //fetch all defined instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ @@ -665,6 +895,10 @@ static int maweb_start(){ } for(u = 0; u < n; u++){ + //sort channels + data = (maweb_instance_data*) inst[u]->impl; + qsort(data->input_channel, data->input_channels, sizeof(maweb_channel_ident), channel_comparator); + if(maweb_connect(inst[u])){ fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); return 1; @@ -678,8 +912,8 @@ static int maweb_start(){ fprintf(stderr, "maweb backend registering %lu descriptors to core\n", n); - //initialize keepalive timeout - last_keepalive = mm_timestamp(); + //initialize timeouts + last_keepalive = last_update = mm_timestamp(); return 0; } @@ -713,6 +947,10 @@ static int maweb_shutdown(){ data->offset = data->allocated = 0; data->state = ws_new; + + free(data->input_channel); + data->input_channel = NULL; + data->input_channels = 0; } free(inst); diff --git a/backends/maweb.h b/backends/maweb.h index 5f59cc1..a868426 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -9,22 +9,22 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v); static int maweb_handle(size_t num, managed_fd* fds); static int maweb_start(); static int maweb_shutdown(); +static uint32_t maweb_interval(); //Default login password: MD5("midimonster") #define MAWEB_DEFAULT_PASSWORD "2807623134739142b119aff358f8a219" #define MAWEB_DEFAULT_PORT "80" #define MAWEB_RECV_CHUNK 1024 -#define MAWEB_XMIT_CHUNK 2048 +#define MAWEB_XMIT_CHUNK 4096 #define MAWEB_FRAME_HEADER_LENGTH 16 #define MAWEB_CONNECTION_KEEPALIVE 10000 typedef enum /*_maweb_channel_type*/ { type_unset = 0, exec_fader = 1, - exec_button = 2, - exec_upper = 3, - exec_lower = 4, - exec_flash = 5, + exec_button = 2, //gma: 0 dot: 0 + exec_lower = 3, //gma: 1 dot: 1 + exec_upper = 4, //gma: 2 dot: 0 cmdline_button } maweb_channel_type; @@ -69,6 +69,10 @@ typedef struct /*_maweb_instance_data*/ { int64_t session; maweb_peer_type peer_type; + //need to keep an internal registry to optimize data polls + size_t input_channels; + maweb_channel_ident* input_channel; + int fd; maweb_state state; size_t offset; diff --git a/backends/maweb.md b/backends/maweb.md index d713d82..fe430db 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -2,8 +2,7 @@ This backend connects directly with the integrated *MA Web Remote* of MA Lighting consoles and OnPC instances (GrandMA2 / GrandMA2 OnPC / GrandMA Dot2 / GrandMA Dot2 OnPC). -It grants read-write access to the console's playback faders and buttons as well as write access to -the command line buttons. +It grants read-write access to the console's playback controls as well as write access to the command line. #### Setting up the console @@ -16,7 +15,9 @@ Web Remote. Set a web remote password using the option below the activation sett #### Global configuration -The `maweb` backend does not take any global configuration. +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|---------------------------------------------------------------| +| `interval` | `100` | `50` | Query interval for input data polling (in msec) | #### Instance configuration @@ -28,39 +29,44 @@ The `maweb` backend does not take any global configuration. #### Channel specification -Currently, three types of channels can be assigned +Currently, three types of MA controls can be assigned, with each having some subcontrols + +* Fader executor +* Button executor +* Command line buttons ##### Executors * For the GrandMA2, executors are arranged in pages, with each page having 90 fader executors (numbered 1 through 90) and 90 button executors (numbered 101 through 190). - * A fader executor consists of a `fader`, two buttons above it (`upper`, `lower`) and one `flash` button below it. - * A button executor consists of a `button` control. + * A fader executor consists of a `fader`, two buttons above it (`upper`, `lower`) and one `button` below it. + * A button executor consists of a `button` control and a virtual `fader` (visible on the console in the "Action Buttons" view). * For the dot2, executors are also arranged in pages, but the controls are non-obviously numbered. * For the faders, they are numerically right-to-left from the Core Fader section (Faders 6 to 1) over the F-Wing 1 (Faders 13 to 6) to F-Wing 2 (Faders 21 to 14). * Above the fader sections are two rows of 21 `button` executors, numbered 122 through 101 (upper row) and 222 through 201 (lower row), in the same order as the faders are. * Fader executors have two buttons below them (`upper` and `lower`). - * The button executor section consists of six rows of 18 buttons, divided into two button wings. Buttons on the wings + * The button executor section consists of six rows of 16 buttons, divided into two button wings. Buttons on the wings are once again numbered right-to-left. - * B-Wing 1 has `button` executors 308 to 301 (top row), 408 to 401 (second row), and so on until 808 through 801 (bottom row) + * B-Wing 1 has `button` controls 308 to 301 (top row), 408 to 401 (second row), and so on until 808 through 801 (bottom row) * B-Wing 2 has 316 to 309 (top row) through 816 to 809 (bottom row) When creating a new show, only the first page is created and active. Additional pages have to be created explicitly within -the console before being usable. +the console before being usable. `fader` controls, when mapped as outputs from the MA, output their value, `button` controls +output 1 when the corresponding executor is running, 0 otherwise. These controls can be addressed like ``` mw1.page1.fader5 > mw1.page1.upper5 -mw1.page3.lower3 > mw1.page2.flash2 +mw1.page3.lower3 > mw1.page2.button2 ``` A button executor can likewise be mapped using the syntax ``` -mw1.page2.button103 > mw1.page3.button101 +mw1.page2.button103 > mw1.page3.fader101 mw1.page2.button803 > mw1.page3.button516 ``` @@ -99,10 +105,12 @@ Since this may be a problem on some platforms, the backend can be built with thi to set arbitrary passwords. The backend will always try to log in with the default password `midimonster` in this case. The user name is still configurable. +Data input from the console is done by actively querying the state of all mapped controls, which is resource-intensive if done +at low latency. A lower input interval value will produce data with lower latency, at the cost of network & CPU usage. +Higher values will make the input "step" more, but will not consume as many CPU cycles and network bandwidth. + This backend is currently in active development. It therefore has some limitations: * It outputs a lot of debug information -* It currently is write-only, channel events are only sent to the MA, not generated by it -* Fader executors (and their buttons) seem to work, I haven't tested button executors yet. * Command line events are sent, but I'm not sure they're being handled yet -* I have so far only tested it with GradMA2 OnPC +* For the dot2, currently only the Core & F-Wings are supported for input from the console, not the B-Wings diff --git a/backends/midi.h b/backends/midi.h index 5ec17ea..6c3fcf9 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -24,4 +24,5 @@ typedef union { uint8_t control; } fields; uint64_t label; -} midi_channel_ident; \ No newline at end of file +} midi_channel_ident; + diff --git a/backends/osc.h b/backends/osc.h index ab19463..dd5afb0 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -73,4 +73,5 @@ typedef union { uint32_t parameter; } fields; uint64_t label; -} osc_channel_ident; \ No newline at end of file +} osc_channel_ident; + -- cgit v1.2.3