aboutsummaryrefslogtreecommitdiffhomepage
path: root/backends/maweb.c
diff options
context:
space:
mode:
Diffstat (limited to 'backends/maweb.c')
-rw-r--r--backends/maweb.c1072
1 files changed, 1072 insertions, 0 deletions
diff --git a/backends/maweb.c b/backends/maweb.c
new file mode 100644
index 0000000..d008cc0
--- /dev/null
+++ b/backends/maweb.c
@@ -0,0 +1,1072 @@
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#ifndef MAWEB_NO_LIBSSL
+#include <openssl/md5.h>
+#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 uint64_t update_interval = 50;
+static uint64_t last_update = 0;
+static uint64_t updates_inflight = 0;
+
+static maweb_command_key cmdline_keys[] = {
+ {"PREV", 109, 0, 1}, {"SET", 108, 1, 0, 1}, {"NEXT", 110, 0, 1},
+ {"TIME", 58, 1, 1}, {"EDIT", 55, 1, 1}, {"UPDATE", 57, 1, 1},
+ {"OOPS", 53, 1, 1}, {"ESC", 54, 1, 1}, {"CLEAR", 105, 1, 1},
+ {"0", 86, 1, 1}, {"1", 87, 1, 1}, {"2", 88, 1, 1},
+ {"3", 89, 1, 1}, {"4", 90, 1, 1}, {"5", 91, 1, 1},
+ {"6", 92, 1, 1}, {"7", 93, 1, 1}, {"8", 94, 1, 1},
+ {"9", 95, 1, 1}, {"PUNKT", 98, 1, 1}, {"ENTER", 106, 1, 1},
+ {"PLUS", 96, 1, 1}, {"MINUS", 97, 1, 1}, {"THRU", 102, 1, 1},
+ {"IF", 103, 1, 1}, {"AT", 104, 1, 1}, {"FULL", 99, 1, 1},
+ {"MA", 68, 0, 1}, {"HIGH", 100, 1, 1, 1}, {"SOLO", 101, 1, 1, 1},
+ {"SELECT", 42, 1, 1}, {"OFF", 43, 1, 1}, {"ON", 46, 1, 1},
+ {"ASSIGN", 63, 1, 1}, {"LABEL", 0, 1, 1},
+ {"COPY", 73, 1, 1}, {"DELETE", 69, 1, 1}, {"STORE", 59, 1, 1},
+ {"GOTO", 56, 1, 1}, {"PAGE", 70, 1, 1}, {"MACRO", 71, 1, 1},
+ {"PRESET", 72, 1, 1}, {"SEQU", 74, 1, 1}, {"CUE", 75, 1, 1},
+ {"EXEC", 76, 1, 1}, {"FIXTURE", 83, 1, 1}, {"GROUP", 84, 1, 1},
+ {"GO_MINUS", 10, 1, 1}, {"PAUSE", 9, 1, 1}, {"GO_PLUS", 11, 1, 1},
+
+ {"FIXTURE_CHANNEL", 0, 1, 1}, {"FIXTURE_GROUP_PRESET", 0, 1, 1},
+ {"EXEC_CUE", 0, 1, 1}, {"STORE_UPDATE", 0, 1, 1}, {"PROG_ONLY", 0, 1, 1, 1},
+ {"SPECIAL_DIALOGUE", 0, 1, 1},
+ {"ODD", 0, 1, 1}, {"EVEN", 0, 1, 1},
+ {"WINGS", 0, 1, 1}, {"RESET", 0, 1, 1},
+ //gma2 internal only
+ {"CHPGPLUS", 3}, {"CHPGMINUS", 4},
+ {"FDPGPLUS", 5}, {"FDPGMINUS", 6},
+ {"BTPGPLUS", 7}, {"BTPGMINUS", 8},
+ {"X1", 12}, {"X2", 13}, {"X3", 14},
+ {"X4", 15}, {"X5", 16}, {"X6", 17},
+ {"X7", 18}, {"X8", 19}, {"X9", 20},
+ {"X10", 21}, {"X11", 22}, {"X12", 23},
+ {"X13", 24}, {"X14", 25}, {"X15", 26},
+ {"X16", 27}, {"X17", 28}, {"X18", 29},
+ {"X19", 30}, {"X20", 31},
+ {"V1", 120}, {"V2", 121}, {"V3", 122},
+ {"V4", 123}, {"V5", 124}, {"V6", 125},
+ {"V7", 126}, {"V8", 127}, {"V9", 128},
+ {"V10", 129},
+ {"NIPPLE", 40},
+ {"TOOLS", 119}, {"SETUP", 117}, {"BACKUP", 117},
+ {"BLIND", 60}, {"FREEZE", 61}, {"PREVIEW", 62},
+ {"FIX", 41}, {"TEMP", 44}, {"TOP", 45},
+ {"VIEW", 66}, {"EFFECT", 67}, {"CHANNEL", 82},
+ {"MOVE", 85}, {"BLACKOUT", 65},
+ {"PLEASE", 106},
+ {"LIST", 32}, {"USER1", 33}, {"USER2", 34},
+ {"ALIGN", 64}, {"HELP", 116},
+ {"UP", 107}, {"DOWN", 111},
+ {"FASTREVERSE", 47}, {"LEARN", 48}, {"FASTFORWARD", 49},
+ {"GO_MINUS_SMALL", 50}, {"PAUSE_SMALL", 51}, {"GO_PLUS_SMALL", 52}
+};
+
+MM_PLUGIN_API 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,
+ .interval = maweb_interval
+ };
+
+ //register backend
+ if(mm_backend_register(maweb)){
+ fprintf(stderr, "Failed to register maweb backend\n");
+ return 1;
+ }
+ return 0;
+}
+
+static ssize_t maweb_channel_index(maweb_instance_data* data, maweb_channel_type type, uint16_t page, uint16_t index){
+ size_t n;
+ for(n = 0; n < data->channels; n++){
+ if(data->channel[n].type == type
+ && data->channel[n].page == page
+ && data->channel[n].index == index){
+ return n;
+ }
+ }
+ return -1;
+}
+
+static int channel_comparator(const void* raw_a, const void* raw_b){
+ maweb_channel_data* a = (maweb_channel_data*) raw_a;
+ maweb_channel_data* b = (maweb_channel_data*) raw_b;
+
+ //this needs to take into account command line channels
+ //they need to be sorted last so that the channel poll logic works properly
+ if(a->page != b->page){
+ return a->page - b->page;
+ }
+ //execs and their components are sorted by index first, type second
+ if(a->type < cmdline && b->type < cmdline){
+ if(a->index != b->index){
+ return a->index - b->index;
+ }
+ return a->type - b->type;
+ }
+ //if either one is not an exec, sort by type first, index second
+ if(a->type != b->type){
+ return a->type - b->type;
+ }
+ return a->index - b->index;
+}
+
+static uint32_t maweb_interval(){
+ return update_interval - (last_update % update_interval);
+}
+
+static int maweb_configure(char* option, char* value){
+ if(!strcmp(option, "interval")){
+ update_interval = strtoul(value, NULL, 10);
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown maweb backend configuration option %s\n", option);
+ 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;
+
+ 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;
+ uint8_t password_hash[MD5_DIGEST_LENGTH];
+
+ 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
+ }
+ else if(!strcmp(option, "cmdline")){
+ if(!strcmp(value, "console")){
+ data->cmdline = cmd_console;
+ }
+ else if(!strcmp(value, "remote")){
+ data->cmdline = cmd_remote;
+ }
+ else if(!strcmp(value, "downgrade")){
+ data->cmdline = cmd_downgrade;
+ }
+ else{
+ fprintf(stderr, "Unknown maweb commandline mode %s for instance %s\n", value, inst->name);
+ return 1;
+ }
+ return 0;
+ }
+
+ fprintf(stderr, "Unknown configuration parameter %s for maweb 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, uint8_t flags){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ maweb_channel_data chan = {
+ 0
+ };
+ char* next_token = NULL;
+ channel* channel_ref = NULL;
+ size_t n;
+
+ if(!strncmp(spec, "page", 4)){
+ chan.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)){
+ chan.type = exec_fader;
+ next_token += 5;
+ }
+ else if(!strncmp(next_token, "upper", 5)){
+ chan.type = exec_upper;
+ next_token += 5;
+ }
+ else if(!strncmp(next_token, "lower", 5)){
+ chan.type = exec_lower;
+ next_token += 5;
+ }
+ else if(!strncmp(next_token, "flash", 5)){
+ chan.type = exec_button;
+ next_token += 5;
+ }
+ else if(!strncmp(next_token, "button", 6)){
+ chan.type = exec_button;
+ next_token += 6;
+ }
+ chan.index = strtoul(next_token, NULL, 10);
+ }
+ else{
+ for(n = 0; n < sizeof(cmdline_keys) / sizeof(maweb_command_key); n++){
+ if(!strcmp(spec, cmdline_keys[n].name)){
+ if((data->cmdline == cmd_remote && !cmdline_keys[n].press && !cmdline_keys[n].release)
+ || (data->cmdline == cmd_console && !cmdline_keys[n].lua)){
+ fprintf(stderr, "maweb cmdline key %s does not work with the current commandline mode for instance %s\n", spec, inst->name);
+ return NULL;
+ }
+
+ chan.type = cmdline;
+ chan.index = n + 1;
+ chan.page = 1;
+ break;
+ }
+ }
+ }
+
+ if(chan.type && chan.index && chan.page){
+ //actually, those are zero-indexed...
+ chan.index--;
+ chan.page--;
+
+ if(maweb_channel_index(data, chan.type, chan.page, chan.index) == -1){
+ data->channel = realloc(data->channel, (data->channels + 1) * sizeof(maweb_channel_data));
+ if(!data->channel){
+ fprintf(stderr, "Failed to allocate memory\n");
+ return NULL;
+ }
+ data->channel[data->channels] = chan;
+ data->channels++;
+ }
+
+ channel_ref = mm_channel(inst, maweb_channel_index(data, chan.type, chan.page, chan.index), 1);
+ data->channel[maweb_channel_index(data, chan.type, chan.page, chan.index)].chan = channel_ref;
+ return channel_ref;
+ }
+
+ 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_process_playback(instance* inst, int64_t page, maweb_channel_type metatype, char* payload, size_t payload_length){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ size_t exec_blocks = json_obj_offset(payload, (metatype == 2) ? "executorBlocks" : "bottomButtons"), offset, block = 0, control;
+ int64_t exec_index = json_obj_int(payload, "iExec", 191);
+ ssize_t channel_index;
+ channel_value evt;
+
+ if(!exec_blocks){
+ if(metatype == 3){
+ //ignore unused buttons
+ return 0;
+ }
+ fprintf(stderr, "maweb missing exec block data on exec %" PRIu64 ".%" PRIu64 "\n", page, exec_index);
+ return 1;
+ }
+
+ //the bottomButtons key has an additional subentry
+ if(metatype == 3){
+ exec_blocks += json_obj_offset(payload + exec_blocks, "items");
+ }
+
+ //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");
+
+ channel_index = maweb_channel_index(data, exec_fader, page - 1, exec_index);
+ if(channel_index >= 0){
+ if(!data->channel[channel_index].input_blocked){
+ evt.normalised = json_obj_double(payload + control, "v", 0.0);
+ if(evt.normalised != data->channel[channel_index].in){
+ mm_channel_event(mm_channel(inst, channel_index, 0), evt);
+ data->channel[channel_index].in = evt.normalised;
+ }
+ }
+ else{
+ //block input immediately after channel set to prevent feedback loops
+ data->channel[channel_index].input_blocked--;
+ }
+ }
+
+ channel_index = maweb_channel_index(data, exec_button, page - 1, exec_index);
+ if(channel_index >= 0){
+ if(!data->channel[channel_index].input_blocked){
+ evt.normalised = json_obj_int(payload, "isRun", 0);
+ if(evt.normalised != data->channel[channel_index].in){
+ mm_channel_event(mm_channel(inst, channel_index, 0), evt);
+ data->channel[channel_index].in = evt.normalised;
+ }
+ }
+ else{
+ data->channel[channel_index].input_blocked--;
+ }
+ }
+
+ DBGPF("maweb page %" PRIu64 " exec %" PRIu64 " value %f running %" PRIu64 "\n", page, exec_index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0));
+ exec_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--;
+ DBGPF("maweb playback message processing done, %" PRIu64 " 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] = "[300,400,500]", item_counts[1024] = "[16,16,16]", item_types[1024] = "[3,3,3]";
+ size_t page_index = 0, view = 3, channel = 0, offsets[3], channel_offset, channels;
+
+ if(updates_inflight){
+ fprintf(stderr, "maweb skipping update request, %" PRIu64 " updates still inflight\n", updates_inflight);
+ return 0;
+ }
+
+ //only request faders and buttons
+ for(channel = 0; channel < data->channels && data->channel[channel].type < cmdline; channel++){
+ offsets[0] = offsets[1] = offsets[2] = 1;
+ page_index = data->channel[channel].page;
+ //poll logic differs between the consoles because reasons
+ //don't quote me on this section
+ if(data->peer_type == peer_dot2){
+ //blocks 0, 100 & 200 have 21 execs and need to be queried from fader view
+ view = (data->channel[channel].index >= 300) ? 3 : 2;
+
+ for(channel_offset = 1; channel + channel_offset <= data->channels
+ && data->channel[channel + channel_offset].type < cmdline; channel_offset++){
+ channels = channel + channel_offset - 1;
+ //find end for this exec block
+ for(; channel + channel_offset < data->channels; channel_offset++){
+ if(data->channel[channel + channel_offset].page != page_index
+ || (data->channel[channels].index / 100) != (data->channel[channel + channel_offset].index / 100)){
+ break;
+ }
+ }
+
+ //add request block for the exec block
+ offsets[0] += snprintf(item_indices + offsets[0], sizeof(item_indices) - offsets[0], "%d,", data->channel[channels].index);
+ offsets[1] += snprintf(item_counts + offsets[1], sizeof(item_counts) - offsets[1], "%d,", data->channel[channel + channel_offset - 1].index - data->channel[channels].index + 1);
+ offsets[2] += snprintf(item_types + offsets[2], sizeof(item_types) - offsets[2], "%d,", (data->channel[channels].index < 100) ? 2 : 3);
+
+ //send on last channel, page boundary, metamode boundary
+ if(channel + channel_offset >= data->channels
+ || data->channel[channel + channel_offset].page != page_index
+ || (data->channel[channel].index < 300) != (data->channel[channel + channel_offset].index < 300)){
+ break;
+ }
+ }
+
+ //terminate arrays (overwriting the last array separator)
+ offsets[0] += snprintf(item_indices + offsets[0] - 1, sizeof(item_indices) - offsets[0], "]");
+ offsets[1] += snprintf(item_counts + offsets[1] - 1, sizeof(item_counts) - offsets[1], "]");
+ offsets[2] += snprintf(item_types + offsets[2] - 1, sizeof(item_types) - offsets[2], "]");
+ }
+ else{
+ //for the ma, the view equals the exec type requested (we can query all button execs from button view, all fader execs from fader view)
+ view = (data->channel[channel].index >= 100) ? 3 : 2;
+ snprintf(item_types, sizeof(item_types), "[%" PRIsize_t "]", view);
+ //this channel must be included, so it must be in range for the first startindex
+ snprintf(item_indices, sizeof(item_indices), "[%d]", (data->channel[channel].index / 5) * 5);
+
+ //find end of exec block
+ for(channel_offset = 1; channel + channel_offset < data->channels
+ && data->channel[channel].page == data->channel[channel + channel_offset].page
+ && data->channel[channel].index / 100 == data->channel[channel + channel_offset].index / 100; channel_offset++){
+ }
+
+ //gma execs are grouped in blocks of 5
+ channels = data->channel[channel + channel_offset - 1].index - (data->channel[channel].index / 5) * 5;
+ snprintf(item_counts, sizeof(item_indices), "[%" PRIsize_t "]", ((channels / 5) * 5 + 5));
+ }
+
+ DBGPF("maweb poll range first %d: %d.%d last %d: %d.%d next %d: %d.%d\n",
+ data->channel[channel].type, data->channel[channel].page, data->channel[channel].index,
+ data->channel[channel + channel_offset - 1].type, data->channel[channel + channel_offset - 1].page, data->channel[channel + channel_offset - 1].index,
+ data->channel[channel + channel_offset].type, data->channel[channel + channel_offset].page, data->channel[channel + channel_offset].index);
+
+ //advance base channel
+ channel += channel_offset - 1;
+
+ //send current request
+ snprintf(xmit_buffer, sizeof(xmit_buffer),
+ "{"
+ "\"requestType\":\"playbacks\","
+ "\"startIndex\":%s,"
+ "\"itemsCount\":%s,"
+ "\"pageIndex\":%" PRIsize_t ","
+ "\"itemsType\":%s,"
+ "\"view\":%" PRIsize_t ","
+ "\"execButtonViewMode\":2," //extended
+ "\"buttonsViewMode\":0," //get vfader for button execs
+ "\"session\":%" PRIu64
+ "}",
+ 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));
+ DBGPF("maweb poll request: %s\n", xmit_buffer);
+ updates_inflight++;
+ }
+
+ DBGPF("maweb poll request handling done, %" PRIu64 " updates requested\n", 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;
+ }
+ }
+
+ DBGPF("maweb message (%" PRIsize_t "): %s\n", payload_length, payload);
+ if(json_obj(payload, "session") == JSON_NUMBER){
+ data->session = json_obj_int(payload, "session", data->session);
+ if(data->session < 0){
+ fprintf(stderr, "maweb login failed\n");
+ data->login = 0;
+ return 0;
+ }
+ fprintf(stderr, "maweb session id is now %" PRId64 "\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\":%" PRIu64 "}",
+ (data->peer_type == peer_dot2) ? "remote" : data->user, data->pass ? data->pass : MAWEB_DEFAULT_PASSWORD, 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);
+ if(!strncmp(field, "dot2", 4)){
+ data->peer_type = peer_dot2;
+ //the dot2 can't handle lua commands
+ data->cmdline = cmd_remote;
+ }
+ else if(!strncmp(field, "gma2", 4)){
+ data->peer_type = peer_ma2;
+ }
+ maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13);
+ }
+
+ 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 critical
+ //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 - 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)){
+ 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 data->offset + 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;
+ data->offset = 0;
+ //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);
+
+ 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;
+ maweb_channel_data* chan = NULL;
+ char xmit_buffer[MAWEB_XMIT_CHUNK];
+ size_t n;
+
+ if(num && !data->login){
+ fprintf(stderr, "maweb instance %s can not send output, not logged in\n", inst->name);
+ return 0;
+ }
+
+ for(n = 0; n < num; n++){
+ //sanity check
+ if(c[n]->ident >= data->channels){
+ return 1;
+ }
+ chan = data->channel + c[n]->ident;
+
+ //channel state tracking
+ if(chan->out == v[n].normalised){
+ continue;
+ }
+ chan->out = v[n].normalised;
+
+ //i/o value space separation & feedback filtering for faders
+ if(chan->type == exec_fader){
+ chan->input_blocked = 1;
+ chan->in = v[n].normalised;
+ }
+
+ switch(chan->type){
+ case exec_fader:
+ snprintf(xmit_buffer, sizeof(xmit_buffer),
+ "{\"requestType\":\"playbacks_userInput\","
+ "\"execIndex\":%d,"
+ "\"pageIndex\":%d,"
+ "\"faderValue\":%f,"
+ "\"type\":1,"
+ "\"session\":%" PRIu64
+ "}", chan->index, chan->page, v[n].normalised, data->session);
+ break;
+ case exec_upper:
+ case exec_lower:
+ case exec_button:
+ snprintf(xmit_buffer, sizeof(xmit_buffer),
+ "{\"requestType\":\"playbacks_userInput\","
+ //"\"cmdline\":\"\","
+ "\"execIndex\":%d,"
+ "\"pageIndex\":%d,"
+ "\"buttonId\":%d,"
+ "\"pressed\":%s,"
+ "\"released\":%s,"
+ "\"type\":0,"
+ "\"session\":%" PRIu64
+ "}", chan->index, chan->page,
+ (data->peer_type == peer_dot2 && chan->type == exec_upper) ? 0 : (chan->type - exec_button),
+ (v[n].normalised > 0.9) ? "true" : "false",
+ (v[n].normalised > 0.9) ? "false" : "true",
+ data->session);
+ break;
+ case cmdline:
+ if(cmdline_keys[chan->index].lua
+ && (data->cmdline == cmd_console || data->cmdline == cmd_downgrade)
+ && data->peer_type != peer_dot2){
+ //push canbus events
+ snprintf(xmit_buffer, sizeof(xmit_buffer),
+ "{\"command\":\"LUA 'gma.canbus.hardkey(%d, %s, false)'\","
+ "\"requestType\":\"command\","
+ "\"session\":%" PRIu64
+ "}", cmdline_keys[chan->index].lua,
+ (v[n].normalised > 0.9) ? "true" : "false",
+ data->session);
+ }
+ else if((cmdline_keys[chan->index].press || cmdline_keys[chan->index].release)
+ && (data->cmdline != cmd_console)){
+ //send press/release events if required
+ if((cmdline_keys[chan->index].press && v[n].normalised > 0.9)
+ || (cmdline_keys[chan->index].release && v[n].normalised < 0.9)){
+ snprintf(xmit_buffer, sizeof(xmit_buffer),
+ "{\"keyname\":\"%s\","
+ "\"autoSubmit\":%s,"
+ "\"value\":%d,"
+ "\"session\":%" PRIu64
+ "}", cmdline_keys[chan->index].name,
+ cmdline_keys[chan->index].auto_submit ? "true" : "null",
+ (v[n].normalised > 0.9) ? 1 : 0,
+ data->session);
+ }
+ else{
+ continue;
+ }
+ }
+ else{
+ fprintf(stderr, "maweb commandline key %s not executed on %s due to mode mismatch\n",
+ cmdline_keys[chan->index].name, inst->name);
+ continue;
+ }
+ break;
+ default:
+ fprintf(stderr, "maweb control not yet implemented\n");
+ return 1;
+ }
+ DBGPF("maweb command out %s\n", xmit_buffer);
+ maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer));
+ }
+ 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\":%" PRIu64 "}", data->session);
+ maweb_send_frame(inst[u], ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer));
+ }
+ }
+
+ free(inst);
+ 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;
+
+ for(n = 0; n < num; n++){
+ 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, p;
+ 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;
+ }
+
+ for(u = 0; u < n; u++){
+ //sort channels
+ data = (maweb_instance_data*) inst[u]->impl;
+ qsort(data->channel, data->channels, sizeof(maweb_channel_data), channel_comparator);
+
+ //re-set channel identifiers
+ for(p = 0; p < data->channels; p++){
+ data->channel[p].chan->ident = p;
+ }
+
+ if(maweb_connect(inst[u])){
+ fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name);
+ free(inst);
+ return 1;
+ }
+ }
+
+ free(inst);
+ if(!n){
+ return 0;
+ }
+
+ fprintf(stderr, "maweb backend registering %" PRIsize_t " descriptors to core\n", n);
+
+ //initialize timeouts
+ last_keepalive = last_update = 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(data->channel);
+ data->channel = NULL;
+ data->channels = 0;
+ }
+
+ free(inst);
+
+ fprintf(stderr, "maweb backend shut down\n");
+ return 0;
+}