aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorcbdev <cb@cbcdn.com>2019-08-11 20:29:17 +0200
committercbdev <cb@cbcdn.com>2019-08-11 20:29:17 +0200
commitbb6111986bf7a997055287b916d0822957c5d13c (patch)
treef840bc6ba42916c8d37441a3db933733290e198f
parent48bf96602023b2ead855f13477b6f5e26b663b45 (diff)
downloadmidimonster-bb6111986bf7a997055287b916d0822957c5d13c.tar.gz
midimonster-bb6111986bf7a997055287b916d0822957c5d13c.tar.bz2
midimonster-bb6111986bf7a997055287b916d0822957c5d13c.zip
Initial maweb backend
-rw-r--r--README.md1
-rw-r--r--TODO6
-rw-r--r--backends/Makefile2
-rw-r--r--backends/libmmbackend.c217
-rw-r--r--backends/libmmbackend.h75
-rw-r--r--backends/maweb.c695
-rw-r--r--backends/maweb.h69
-rw-r--r--backends/maweb.md142
-rw-r--r--midimonster.c3
-rw-r--r--monster.cfg29
10 files changed, 1238 insertions, 1 deletions
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 <fcntl.h>
#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 <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 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.<button-name>
+```
+
+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