diff options
| author | cbdev <cb@cbcdn.com> | 2020-02-23 18:20:12 +0100 | 
|---|---|---|
| committer | cbdev <cb@cbcdn.com> | 2020-02-23 18:20:12 +0100 | 
| commit | dd91621ccee033550312683293b5bf40c3599053 (patch) | |
| tree | b56199bf974a4ff2e65397e4eb0f3a40c65d5740 /backends | |
| parent | b529f4d3a9efc59c6bec46e07ebf1a114b7a0831 (diff) | |
| download | midimonster-dd91621ccee033550312683293b5bf40c3599053.tar.gz midimonster-dd91621ccee033550312683293b5bf40c3599053.tar.bz2 midimonster-dd91621ccee033550312683293b5bf40c3599053.zip | |
Implement OpenPixelControl output
Diffstat (limited to 'backends')
| -rw-r--r-- | backends/Makefile | 11 | ||||
| -rw-r--r-- | backends/openpixelcontrol.c | 353 | ||||
| -rw-r--r-- | backends/openpixelcontrol.h | 48 | ||||
| -rw-r--r-- | backends/openpixelcontrol.md | 49 | 
4 files changed, 459 insertions, 2 deletions
| diff --git a/backends/Makefile b/backends/Makefile index 656e6b6..191a495 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@  .PHONY: all clean full  LINUX_BACKENDS = midi.so evdev.so -WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so  OPTIONAL_BACKENDS = ola.so  BACKEND_LIB = libmmbackend.o @@ -36,6 +36,10 @@ sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB)  sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)  sacn.dll: LDLIBS += -lws2_32 +openpixelcontrol.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +openpixelcontrol.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +openpixelcontrol.dll: LDLIBS += -lws2_32 +  maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB)  maweb.so: LDLIBS = -lssl  maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) @@ -59,6 +63,9 @@ lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 ||  lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"")  lua.dll: LDLIBS += -L../libs -llua53 +python.so: CFLAGS += $(shell pkg-config --cflags python3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +python.so: CFLAGS += $(shell pkg-config --libs python3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +  %.so :: %.c %.h $(BACKEND_LIB)  	$(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c new file mode 100644 index 0000000..062c015 --- /dev/null +++ b/backends/openpixelcontrol.c @@ -0,0 +1,353 @@ +#define BACKEND_NAME "openpixelcontrol" + +#include <string.h> + +#include "libmmbackend.h" +#include "openpixelcontrol.h" + +/* + * TODO handle destination close/unregister/reopen + */ + +MM_PLUGIN_API int init(){ +	backend openpixel = { +		.name = BACKEND_NAME, +		.conf = openpixel_configure, +		.create = openpixel_instance, +		.conf_instance = openpixel_configure_instance, +		.channel = openpixel_channel, +		.handle = openpixel_set, +		.process = openpixel_handle, +		.start = openpixel_start, +		.shutdown = openpixel_shutdown +	}; + +	//register backend +	if(mm_backend_register(openpixel)){ +		LOG("Failed to register backend"); +		return 1; +	} +	return 0; +} + +static int openpixel_configure(char* option, char* value){ +	//no global configuration +	LOG("No backend configuration possible"); +	return 1; +} + +static int openpixel_configure_instance(instance* inst, char* option, char* value){ +	char* host = NULL, *port = NULL; +	openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + +	//FIXME this should store the destination/listen address and establish on _start +	if(!strcmp(option, "destination")){ +		mmbackend_parse_hostspec(value, &host, &port, NULL); +		if(!host || !port){ +			LOGPF("Invalid destination address specified for instance %s", inst->name); +			return 1; +		} + +		data->dest_fd = mmbackend_socket(host, port, SOCK_STREAM, 0, 0); +		if(data->dest_fd >= 0){ +			return 0; +		} +		return 1; +	} +	if(!strcmp(option, "listen")){ +		mmbackend_parse_hostspec(value, &host, &port, NULL); +		if(!host || !port){ +			LOGPF("Invalid listen address specified for instance %s", inst->name); +			return 1; +		} + +		data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0); +		if(data->listen_fd >= 0 && listen(data->listen_fd, SOMAXCONN)){ +			return 0; +		} +		return 1; +	} +	else if(!strcmp(option, "mode")){ +		if(!strcmp(value, "16bit")){ +			data->mode = rgb16; +			return 0; +		} +		else if(!strcmp(value, "8bit")){ +			data->mode = rgb8; +			return 0; +		} +		LOGPF("Unknown instance mode %s\n", value); +		return 1; +	} + +	LOGPF("Unknown instance option %s for instance %s", option, inst->name); +	return 1; +} + +static int openpixel_instance(instance* inst){ +	openpixel_instance_data* data = calloc(1, sizeof(openpixel_instance_data)); +	inst->impl = data; +	if(!inst->impl){ +		LOG("Failed to allocate memory"); +		return 1; +	} + +	data->dest_fd = -1; +	data->listen_fd = -1; +	return 0; +} + +static ssize_t openpixel_buffer_find(openpixel_instance_data* data, uint8_t strip, uint8_t input){ +	ssize_t n = 0; + +	for(n = 0; n < data->buffers; n++){ +		if(data->buffer[n].strip == strip +				&& (data->buffer[n].flags & OPENPIXEL_INPUT) >= input){ +			return n; +		} +	} +	return -1; +} + +static int openpixel_buffer_extend(openpixel_instance_data* data, uint8_t strip, uint8_t input, uint8_t length){ +	ssize_t buffer = openpixel_buffer_find(data, strip, input); +	length = (length % 3) ? ((length / 3) + 1) * 3 : length; +	size_t bytes_required = (data->mode == rgb8) ? length : length * 2; +	if(buffer < 0){ +		//allocate new buffer +		data->buffer = realloc(data->buffer, (data->buffers + 1) * sizeof(openpixel_buffer)); +		if(!data->buffer){ +			data->buffers = 0; +			LOG("Failed to allocate memory"); +			return -1; +		} + +		buffer = data->buffers; +		data->buffers++; + +		data->buffer[buffer].strip = strip; +		data->buffer[buffer].flags = input ? OPENPIXEL_INPUT : 0; +		data->buffer[buffer].bytes = 0; +		data->buffer[buffer].data.u8 = NULL; +	} + +	if(data->buffer[buffer].bytes < bytes_required){ +		//resize buffer +		data->buffer[buffer].data.u8 = realloc(data->buffer[buffer].data.u8, bytes_required); +		if(!data->buffer[buffer].data.u8){ +			data->buffer[buffer].bytes = 0; +			LOG("Failed to allocate memory"); +			return 1; +		} +		//FIXME might want to memset() only newly allocated channels +		memset(data->buffer[buffer].data.u8, 0, bytes_required); +		data->buffer[buffer].bytes = bytes_required; +	} +	return 0; +} + +static channel* openpixel_channel(instance* inst, char* spec, uint8_t flags){ +	uint32_t strip = 0, channel = 0; +	char* token = spec; +	openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + +	//read strip index if supplied +	if(!strncmp(spec, "strip", 5)){ +		strip = strtoul(spec + 5, &token, 10); +		//skip the dot +		token++; +	} + +	//read (and calculate) channel index +	if(!strncmp(token, "channel", 7)){ +		channel = strtoul(token + 7, NULL, 10); +	} +	else if(!strncmp(token, "red", 3)){ +		channel = strtoul(token + 3, NULL, 10) * 3 - 2; +	} +	else if(!strncmp(token, "green", 5)){ +		channel = strtoul(token + 5, NULL, 10) * 3 - 1; +	} +	else if(!strncmp(token, "blue", 4)){ +		channel = strtoul(token + 4, NULL, 10) * 3; +	} + +	if(!channel){ +		LOGPF("Invalid channel specification %s", spec); +		return NULL; +	} + +	//check channel direction +	if(flags & mmchannel_input){ +		//strip 0 (bcast) can not be mapped as input +		if(!strip){ +			LOGPF("Broadcast channel %s.%s can not be mapped as an input", inst->name, spec); +			return NULL; +		} +		if(data->listen_fd < 0){ +			LOGPF("Channel %s mapped as input, but instance %s is not accepting input", spec, inst->name); +			return NULL; +		} + +		if(openpixel_buffer_extend(data, strip, 1, channel)){ +			return NULL; +		} +	} + +	if(flags & mmchannel_output){ +		if(data->dest_fd < 0){ +			LOGPF("Channel %s mapped as output, but instance %s is not sending output", spec, inst->name); +			return NULL; +		} + +		if(openpixel_buffer_extend(data, strip, 0, channel)){ +			return NULL; +		} +	} + +	return mm_channel(inst, ((uint64_t) strip) << 32 | channel, 1); +} + +static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* v){ +	openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; +	size_t u, p; +	ssize_t buffer; +	uint32_t strip, channel; +	openpixel_header hdr; + +	for(u = 0; u < num; u++){ +		//read strip/channel +		strip = c[u]->ident >> 32; +		channel = c[u]->ident & 0xFFFFFFFF; +		channel--; + +		//find the buffer +		buffer = openpixel_buffer_find(data, strip, 0); +		if(buffer < 0){ +			LOGPF("No buffer for channel %s.%d.%d\n", inst->name, strip, channel); +			continue; +		} + +		//mark buffer for output +		data->buffer[buffer].flags |= OPENPIXEL_MARK; + +		//update data +		switch(data->mode){ +			case rgb8: +				data->buffer[buffer].data.u8[channel] = ((uint8_t)(v[u].normalised * 255.0)); +				break; +			case rgb16: +				data->buffer[buffer].data.u16[channel] = ((uint16_t)(v[u].normalised * 65535.0)); +				break; +		} + +		if(strip == 0){ +			//update values in all other output strips, dont mark +			for(p = 0; p < data->buffers; p++){ +				if(!(data->buffer[p].flags & OPENPIXEL_INPUT)){ +					//check whether the buffer is large enough +					if(data->mode == rgb8 && data->buffer[p].bytes >= channel){ +						data->buffer[p].data.u8[channel] = ((uint8_t)(v[u].normalised * 255.0)); +					} +					else if(data->mode == rgb16 && data->buffer[p].bytes >= channel * 2){ +						data->buffer[p].data.u16[channel] = ((uint16_t)(v[u].normalised * 65535.0)); +					} +				} +			} +		} +	} + +	//send updated strips +	for(u = 0; u < data->buffers; u++){ +		if(!(data->buffer[u].flags & OPENPIXEL_INPUT) && (data->buffer[u].flags & OPENPIXEL_MARK)){ +			//remove mark +			data->buffer[u].flags &= ~OPENPIXEL_MARK; + +			//prepare header +			hdr.strip = data->buffer[u].strip; +			hdr.mode = data->mode; +			hdr.length = htobe16(data->buffer[u].bytes); + +			//output data +			if(mmbackend_send(data->dest_fd, (uint8_t*) &hdr, sizeof(hdr)) +					|| mmbackend_send(data->dest_fd, data->buffer[u].data.u8, data->buffer[u].bytes)){ +				return 1; +			} +		} +	} +	return 0; +} + +static int openpixel_handle(size_t num, managed_fd* fds){ +	//TODO handle bcast +	return 0; +} + +static int openpixel_start(size_t n, instance** inst){ +	int rv = -1; +	size_t u, nfds = 0; +	openpixel_instance_data* data = NULL; + +	for(u = 0; u < n; u++){ +		data = (openpixel_instance_data*) inst[u]->impl; + +		//register fds +		if(data->dest_fd >= 0){ +			if(mm_manage_fd(data->dest_fd, BACKEND_NAME, 1, inst[u])){ +				LOGPF("Failed to register destination descriptor for instance %s with core", inst[u]->name); +				goto bail; +			} +			nfds++; +		} +		if(data->listen_fd >= 0){ +			if(mm_manage_fd(data->listen_fd, BACKEND_NAME, 1, inst[u])){ +				LOGPF("Failed to register host descriptor for instance %s with core", inst[u]->name); +				goto bail; +			} +			nfds++; +		} +	} + +	LOGPF("Registered %" PRIsize_t " descriptors to core", nfds); +	rv = 0; +bail: +	return rv; +} + +static int openpixel_shutdown(size_t n, instance** inst){ +	size_t u, p; +	openpixel_instance_data* data = NULL; + +	for(u = 0; u < n; u++){ +		data = (openpixel_instance_data*) inst[u]->impl; + +		//shutdown all clients +		for(p = 0; p < data->clients; p++){ +			if(data->client_fd[p] >= 0){ +				close(data->client_fd[p]); +			} +		} +		free(data->client_fd); +		free(data->bytes_left); + +		//close all configured fds +		if(data->listen_fd >= 0){ +			close(data->listen_fd); +		} +		if(data->dest_fd >= 0){ +			close(data->dest_fd); +		} + +		//free all buffers +		for(p = 0; p < data->buffers; p++){ +			free(data->buffer[p].data.u8); +		} +		free(data->buffer); + +		free(data); +		inst[u]->impl = NULL; +	} + +	LOG("Backend shut down"); +	return 0; +} diff --git a/backends/openpixelcontrol.h b/backends/openpixelcontrol.h new file mode 100644 index 0000000..f1061ea --- /dev/null +++ b/backends/openpixelcontrol.h @@ -0,0 +1,48 @@ +#include "midimonster.h" + +MM_PLUGIN_API int init(); +static int openpixel_configure(char* option, char* value); +static int openpixel_configure_instance(instance* inst, char* option, char* value); +static int openpixel_instance(instance* inst); +static channel* openpixel_channel(instance* inst, char* spec, uint8_t flags); +static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* v); +static int openpixel_handle(size_t num, managed_fd* fds); +static int openpixel_start(size_t n, instance** inst); +static int openpixel_shutdown(size_t n, instance** inst); + +#define OPENPIXEL_INPUT 1 +#define OPENPIXEL_MARK 2 + +typedef struct /*_data_buffer*/ { +	uint8_t strip; +	uint8_t flags; +	uint16_t bytes; +	union { +		uint16_t* u16; +		uint8_t* u8; +	} data; +} openpixel_buffer; + +#pragma pack(push, 1) +typedef struct /*_openpixel_hdr*/ { +	uint8_t strip; +	uint8_t mode; +	uint16_t length; +} openpixel_header; +#pragma pack(pop) + +typedef struct { +	enum { +		rgb8 = 0, +		rgb16 = 2 +	} mode; + +	size_t buffers; +	openpixel_buffer* buffer; + +	int dest_fd; +	int listen_fd; +	size_t clients; +	int* client_fd; +	size_t* bytes_left; +} openpixel_instance_data; diff --git a/backends/openpixelcontrol.md b/backends/openpixelcontrol.md new file mode 100644 index 0000000..ce60278 --- /dev/null +++ b/backends/openpixelcontrol.md @@ -0,0 +1,49 @@ +### The `openpixelcontrol` backend + +This backend provides read-write access to the TCP-based OpenPixelControl protocol, +used for controlling intelligent RGB led strips. + +This backend can both control a remote OpenPixelControl server as well as receive data +from OpenPixelControl clients. + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `destination`	| `10.11.12.1 9001`	| none			| Destination for output data. Setting this option enables the instance for output | +| `listen`	| `10.11.12.2 9002`	| none			| Local address to wait for client connections on. Setting this enables the instance for input | +| `mode`	| `16bit`		| `8bit`		| RGB channel resolution | + +#### Channel specification + +Each instance can control up to 255 strips of RGB LED lights. The OpenPixelControl specification +confusingly calls these strips "channels". + +Strip `0` acts as a "broadcast" strip, setting values on all other strips at once. +Consequently, components on strip 0 can only be mapped as output channels to a destination +(setting components on all strips there), not as input channels. When such messages are received from +a client, the corresponding mapped component channels on all strips will receive events. + +Every single component of any LED on any string can be mapped as an individual MIDIMonster channel. +The components are laid out as sequences of Red - Green - Blue value triplets. + +Channels can be specified by their sequential index (one-based). + +Example mapping (data from Strip 2 LED 66's green component is mapped to the blue component of LED 2 on strip 1): +``` +strip1.channel6 < strip2.channel200 +``` + +Additionally, channels may be referred to by their color component and LED index: +``` +strip1.blue2 < strip2.green66 +``` + +#### Known bugs / problems + +If the connection is lost, it is currently not reestablished and may cause exit the MIDIMonster entirely. +Thisi behaviour may be changed in future releases. | 
