diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | TODO | 6 | ||||
| -rw-r--r-- | backends/Makefile | 2 | ||||
| -rw-r--r-- | backends/libmmbackend.c | 217 | ||||
| -rw-r--r-- | backends/libmmbackend.h | 75 | ||||
| -rw-r--r-- | backends/maweb.c | 695 | ||||
| -rw-r--r-- | backends/maweb.h | 69 | ||||
| -rw-r--r-- | backends/maweb.md | 142 | ||||
| -rw-r--r-- | midimonster.c | 3 | ||||
| -rw-r--r-- | monster.cfg | 29 | 
10 files changed, 1238 insertions, 1 deletions
@@ -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 @@ -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  | 
