aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile18
-rw-r--r--README.md5
-rw-r--r--assets/MIDIMonster.svg (renamed from MIDIMonster.svg)0
-rw-r--r--assets/TODO (renamed from TODO)3
-rw-r--r--assets/ci-config.yml (renamed from .travis.yml)0
-rwxr-xr-xassets/ci.sh (renamed from .ci.sh)0
-rw-r--r--assets/midimonster.1 (renamed from midimonster.1)0
-rw-r--r--assets/midimonster.ico (renamed from midimonster.ico)bin321510 -> 321510 bytes
-rw-r--r--assets/midimonster.rc (renamed from midimonster.rc)0
-rw-r--r--backends/Makefile8
-rw-r--r--backends/maweb.c2
-rw-r--r--backends/mqtt.c1005
-rw-r--r--backends/mqtt.h87
-rw-r--r--backends/mqtt.md85
-rw-r--r--backends/osc.c3
-rw-r--r--backends/osc.md2
-rw-r--r--core/backend.c (renamed from backend.c)47
-rw-r--r--core/backend.h (renamed from backend.h)1
-rw-r--r--core/config.c (renamed from config.c)0
-rw-r--r--core/config.h (renamed from config.h)0
-rw-r--r--core/plugin.c (renamed from plugin.c)0
-rw-r--r--core/plugin.h (renamed from plugin.h)0
-rwxr-xr-xinstaller.sh76
-rw-r--r--midimonster.c10
-rw-r--r--midimonster.h25
26 files changed, 1321 insertions, 57 deletions
diff --git a/.gitignore b/.gitignore
index ccb500c..e7c62d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ libmmapi.a
*.so
*.dll
__pycache__
+.vscode/ \ No newline at end of file
diff --git a/Makefile b/Makefile
index bda7bb1..50fc73e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
.PHONY: all clean run sanitize backends windows full backends-full install
-OBJS = config.o backend.o plugin.o
+OBJS = core/config.o core/backend.o core/plugin.o
PREFIX ?= /usr
PLUGIN_INSTALL = $(PREFIX)/lib/midimonster
@@ -13,12 +13,16 @@ CFLAGS ?= -g -Wall -Wpedantic
# Hide all non-API symbols for export
CFLAGS += -fvisibility=hidden
+# Subdirectory objects need the include path
+RCCFLAGS += -I./
+CFLAGS += -I./
+
midimonster: LDLIBS = -ldl
# Replace version string with current git-describe if possible
ifneq "$(GITVERSION)" ""
midimonster: CFLAGS += -DMIDIMONSTER_VERSION=\"$(GITVERSION)\"
midimonster.exe: CFLAGS += -DMIDIMONSTER_VERSION=\"$(GITVERSION)\"
-resource.o: RCCFLAGS += -DMIDIMONSTER_VERSION=\\\"$(GITVERSION)\\\"
+assets/resource.o: RCCFLAGS += -DMIDIMONSTER_VERSION=\\\"$(GITVERSION)\\\"
endif
# Work around strange linker passing convention differences in Linux and OSX
@@ -55,10 +59,10 @@ backends-full:
midimonster: midimonster.c portability.h $(OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@
-resource.o: midimonster.rc midimonster.ico
+assets/resource.o: assets/midimonster.rc assets/midimonster.ico
$(RCC) $(RCCFLAGS) $< -o $@ --output-format=coff
-midimonster.ico: MIDIMonster.svg
+assets/midimonster.ico: assets/MIDIMonster.svg
convert -density 384 $< -define icon:auto-resize $@
midimonster.exe: export CC = x86_64-w64-mingw32-gcc
@@ -66,14 +70,14 @@ midimonster.exe: RCC ?= x86_64-w64-mingw32-windres
midimonster.exe: CFLAGS += -Wno-format
midimonster.exe: LDLIBS = -lws2_32
midimonster.exe: LDFLAGS += -Wl,--out-implib,libmmapi.a
-midimonster.exe: midimonster.c portability.h $(OBJS) resource.o
- $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) resource.o $(LDLIBS) -o $@
+midimonster.exe: midimonster.c portability.h $(OBJS) assets/resource.o
+ $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) assets/resource.o $(LDLIBS) -o $@
clean:
$(RM) midimonster
$(RM) midimonster.exe
$(RM) libmmapi.a
- $(RM) resource.o
+ $(RM) assets/resource.o
$(RM) $(OBJS)
$(MAKE) -C backends clean
diff --git a/README.md b/README.md
index 26a8c90..696a46f 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
# The MIDIMonster
-<img align="right" src="/MIDIMonster.svg?raw=true&sanitize=true" alt="MIDIMonster Logo" width="20%">
+<img align="right" src="/assets/MIDIMonster.svg?raw=true&sanitize=true" alt="MIDIMonster Logo" width="20%">
-[![Build Status](https://travis-ci.com/cbdevnet/midimonster.svg?branch=master)](https://travis-ci.com/cbdevnet/midimonster)
[![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168)
[![IRC Channel](https://static.midimonster.net/hackint-badge.svg)](https://webirc.hackint.org/#irc://irc.hackint.org/#midimonster)
@@ -16,6 +15,7 @@ Currently, the MIDIMonster supports the following protocols:
| ArtNet | Linux, Windows, OSX | Version 4 | [`artnet`](backends/artnet.md) |
| Streaming ACN (sACN / E1.31) | Linux, Windows, OSX | | [`sacn`](backends/sacn.md) |
| OpenSoundControl (OSC) | Linux, Windows, OSX | | [`osc`](backends/osc.md) |
+| MQTT | Linux, Windows, OSX | Protocol versions 5 and 3.1.1 | [`mqtt`](backends/mqtt.md) |
| RTP-MIDI | Linux, Windows, OSX | AppleMIDI sessions supported | [`rtpmidi`](backends/rtpmidi.md) |
| OpenPixelControl | Linux, Windows, OSX | 8 Bit & 16 Bit modes | [`openpixelcontrol`](backends/openpixelcontrol.md) |
| Input devices (Mouse, Keyboard, etc)| Linux, Windows | | [`evdev`](backends/evdev.md), [`wininput`](backends/wininput.md) |
@@ -160,6 +160,7 @@ special information. These documentation files are located in the `backends/` di
* [`loopback` backend documentation](backends/loopback.md)
* [`ola` backend documentation](backends/ola.md)
* [`osc` backend documentation](backends/osc.md)
+* [`mqtt` backend documentation](backends/mqtt.md)
* [`openpixelcontrol` backend documentation](backends/openpixelcontrol.md)
* [`lua` backend documentation](backends/lua.md)
* [`python` backend documentation](backends/python.md)
diff --git a/MIDIMonster.svg b/assets/MIDIMonster.svg
index 7e411dc..7e411dc 100644
--- a/MIDIMonster.svg
+++ b/assets/MIDIMonster.svg
diff --git a/TODO b/assets/TODO
index 2ab5f10..9158e24 100644
--- a/TODO
+++ b/assets/TODO
@@ -10,3 +10,6 @@ move all typenames to _t
per-channel filters
* invert
* edge detection
+
+channel discovery / enumeration
+note exit condition/reconnection details for backends
diff --git a/.travis.yml b/assets/ci-config.yml
index 9fbe236..9fbe236 100644
--- a/.travis.yml
+++ b/assets/ci-config.yml
diff --git a/.ci.sh b/assets/ci.sh
index 4a646a9..4a646a9 100755
--- a/.ci.sh
+++ b/assets/ci.sh
diff --git a/midimonster.1 b/assets/midimonster.1
index 44c414e..44c414e 100644
--- a/midimonster.1
+++ b/assets/midimonster.1
diff --git a/midimonster.ico b/assets/midimonster.ico
index 9391160..9391160 100644
--- a/midimonster.ico
+++ b/assets/midimonster.ico
Binary files differ
diff --git a/midimonster.rc b/assets/midimonster.rc
index 45a88aa..45a88aa 100644
--- a/midimonster.rc
+++ b/assets/midimonster.rc
diff --git a/backends/Makefile b/backends/Makefile
index d815f84..aa9c988 100644
--- a/backends/Makefile
+++ b/backends/Makefile
@@ -2,9 +2,9 @@
# Backends that can only be built on Linux
LINUX_BACKENDS = midi.so evdev.so
# Backends that can only be built on Windows (mostly due to the .DLL extension)
-WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll wininput.dll visca.dll
+WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll wininput.dll visca.dll mqtt.dll
# Backends that can be built on any platform that can load .SO libraries
-BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.so visca.so
+BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.so visca.so mqtt.so
# Backends that require huge dependencies to be installed
OPTIONAL_BACKENDS = ola.so
# Backends that need to be built manually (but still should be included in the clean target)
@@ -51,6 +51,10 @@ visca.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
visca.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
visca.dll: LDLIBS += -lws2_32
+mqtt.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
+mqtt.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
+mqtt.dll: LDLIBS += -lws2_32
+
openpixelcontrol.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
openpixelcontrol.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
openpixelcontrol.dll: LDLIBS += -lws2_32
diff --git a/backends/maweb.c b/backends/maweb.c
index 39ef7a6..8b878b0 100644
--- a/backends/maweb.c
+++ b/backends/maweb.c
@@ -1111,7 +1111,7 @@ static int maweb_start(size_t n, instance** inst){
//re-set channel identifiers
for(p = 0; p < data->channels; p++){
- data->channel[p].chan->ident = p;
+ mm_channel_update(data->channel[p].chan, p);
}
//try to connect to any available host
diff --git a/backends/mqtt.c b/backends/mqtt.c
new file mode 100644
index 0000000..f2a7c83
--- /dev/null
+++ b/backends/mqtt.c
@@ -0,0 +1,1005 @@
+#define BACKEND_NAME "mqtt"
+//#define DEBUG
+
+#include <string.h>
+#include <time.h>
+#include <math.h>
+
+#include "libmmbackend.h"
+#include "mqtt.h"
+
+static uint64_t last_maintenance = 0;
+/* according to spec 2.2.2.2 */
+static struct {
+ uint8_t property;
+ uint8_t storage;
+} property_lengths[] = {
+ {0x01, STORAGE_U8},
+ {0x02, STORAGE_U32},
+ {0x03, STORAGE_PREFIXED},
+ {0x08, STORAGE_PREFIXED},
+ {0x09, STORAGE_PREFIXED},
+ {0x0B, STORAGE_VARINT},
+ {0x11, STORAGE_U32},
+
+ {0x12, STORAGE_PREFIXED},
+ {0x13, STORAGE_U16},
+ {0x15, STORAGE_PREFIXED},
+ {0x16, STORAGE_PREFIXED},
+ {0x17, STORAGE_U8},
+ {0x18, STORAGE_U32},
+ {0x19, STORAGE_U8},
+ {0x1A, STORAGE_PREFIXED},
+ {0x1C, STORAGE_PREFIXED},
+ {0x1F, STORAGE_PREFIXED},
+ {0x21, STORAGE_U16},
+ {0x22, STORAGE_U16},
+ {0x23, STORAGE_U16},
+ {0x24, STORAGE_U8},
+ {0x25, STORAGE_U8},
+ {0x26, STORAGE_PREFIXPAIR},
+ {0x27, STORAGE_U32},
+ {0x28, STORAGE_U8},
+ {0x29, STORAGE_U8},
+ {0x2A, STORAGE_U8}
+};
+
+/*
+ * TODO
+ * * proper RETAIN handling
+ * * TLS
+ * * JSON subchannels
+ */
+
+MM_PLUGIN_API int init(){
+ backend mqtt = {
+ .name = BACKEND_NAME,
+ .conf = mqtt_configure,
+ .create = mqtt_instance,
+ .conf_instance = mqtt_configure_instance,
+ .channel = mqtt_channel,
+ .handle = mqtt_set,
+ .process = mqtt_handle,
+ .start = mqtt_start,
+ .shutdown = mqtt_shutdown
+ };
+
+ //register backend
+ if(mm_backend_register(mqtt)){
+ LOG("Failed to register backend");
+ return 1;
+ }
+ return 0;
+}
+
+static int mqtt_parse_hostspec(instance* inst, char* hostspec){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ char* host = strchr(hostspec, '@'), *password = NULL, *port = NULL;
+
+ //mqtt[s]://[username][:password]@host.domain[:port]
+ if(!strncmp(hostspec, "mqtt://", 7)){
+ hostspec += 7;
+ }
+ else if(!strncmp(hostspec, "mqtts://", 8)){
+ data->tls = 1;
+ hostspec += 8;
+ }
+
+ if(host){
+ //parse credentials, separate out host spec
+ *host = 0;
+ host++;
+
+ password = strchr(hostspec, ':');
+ if(password){
+ //password supplied, store
+ *password = 0;
+ password++;
+ mmbackend_strdup(&(data->password), password);
+ }
+
+ //store username
+ mmbackend_strdup(&(data->user), hostspec);
+ }
+ else{
+ host = hostspec;
+ }
+
+ //parse port if supplied
+ port = strchr(host, ':');
+ if(port){
+ *port = 0;
+ port++;
+ mmbackend_strdup(&(data->port), port);
+ }
+
+ mmbackend_strdup(&(data->host), host);
+ return 0;
+}
+
+static int mqtt_generate_instanceid(instance* inst){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ char clientid[24] = "";
+
+ snprintf(clientid, sizeof(clientid), "MIDIMonster-%d-%s", (uint32_t) time(NULL), inst->name);
+ return mmbackend_strdup(&(data->client_id), clientid);
+}
+
+static size_t mqtt_pop_varint(uint8_t* buffer, size_t len, uint32_t* result){
+ size_t value = 0, offset = 0;
+ do {
+ if(offset >= len){
+ return 0;
+ }
+
+ value |= (buffer[offset] & 0x7F) << (7 * offset);
+ offset++;
+ } while(buffer[offset - 1] & 0x80);
+
+ if(result){
+ *result = value;
+ }
+ return offset;
+}
+
+static size_t mqtt_pop_property(uint8_t* buffer, size_t bytes){
+ size_t length = 0, u;
+
+ if(bytes){
+ for(u = 0; u < sizeof(property_lengths)/sizeof(property_lengths[0]); u++){
+ if(property_lengths[u].property == buffer[0]){
+ switch(property_lengths[u].storage){
+ case STORAGE_U8:
+ return 2;
+ case STORAGE_U16:
+ return 3;
+ case STORAGE_U32:
+ return 5;
+ case STORAGE_VARINT:
+ return mqtt_pop_varint(buffer + 1, bytes - 1, NULL) + 1;
+ case STORAGE_PREFIXED:
+ if(bytes >= 3){
+ return ((buffer[1] << 8) | buffer[2]) + 1;
+ }
+ //best-effort guess
+ return 3;
+ case STORAGE_PREFIXPAIR:
+ if(bytes >= 3){
+ length = ((buffer[1] << 8) | buffer[2]);
+ if(bytes >= length + 5){
+ return (1 + 2 + length + 2 + ((buffer[length + 3] << 8) | buffer[length + 4]));
+ }
+ return length + 3;
+ }
+ //best-effort guess
+ return 5;
+ }
+ }
+ }
+ }
+
+ LOGPF("Storage class for property %02X was unknown", buffer[0]);
+ return 1;
+}
+
+static size_t mqtt_push_varint(size_t value, size_t maxlen, uint8_t* buffer){
+ //implementation conforming to spec 1.5.5
+ size_t offset = 0;
+ do {
+ buffer[offset] = value % 128;
+ value = value / 128;
+ if(value){
+ buffer[offset] |= 0x80;
+ }
+ offset++;
+ } while(value);
+ return offset;
+}
+
+static size_t mqtt_push_binary(uint8_t* buffer, size_t buffer_length, uint8_t* content, size_t length){
+ if(buffer_length < length + 2 || length > 65535){
+ LOG("Failed to push length-prefixed data blob, buffer size exceeded");
+ return 0;
+ }
+
+ buffer[0] = (length >> 8) & 0xFF;
+ buffer[1] = length & 0xFF;
+
+ memcpy(buffer + 2, content, length);
+ return length + 2;
+}
+
+static size_t mqtt_push_utf8(uint8_t* buffer, size_t buffer_length, char* content){
+ //FIXME might want to validate the string for valid UTF-8
+ return mqtt_push_binary(buffer, buffer_length, (uint8_t*) content, strlen(content));
+}
+
+static size_t mqtt_pop_utf8(uint8_t* buffer, size_t buffer_length, char** data){
+ size_t length = 0;
+ *data = NULL;
+
+ if(buffer_length < 2){
+ return 0;
+ }
+
+ length = (buffer[0] << 8) | buffer[1];
+ if(buffer_length >= length + 2){
+ *data = (char*) buffer + 2;
+ }
+ return length;
+}
+
+static void mqtt_disconnect(instance* inst){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ size_t u;
+
+ data->last_control = 0;
+
+ //reset aliases as they can not be reused across sessions
+ data->server_max_alias = 0;
+ data->current_alias = 1;
+ for(u = 0; u < data->nchannels; u++){
+ data->channel[u].topic_alias_sent = 0;
+ data->channel[u].topic_alias_rcvd = 0;
+ }
+
+ //unmanage the fd
+ mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL);
+
+ close(data->fd);
+ data->fd = -1;
+}
+
+static int mqtt_transmit(instance* inst, uint8_t type, size_t vh_length, uint8_t* vh, size_t payload_length, uint8_t* payload){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ uint8_t fixed_header[5];
+ size_t offset = 0;
+
+ //how in the world is it a _fixed_ header if it contains a variable length integer? eh...
+ fixed_header[offset++] = type;
+ offset += mqtt_push_varint(vh_length + payload_length, sizeof(fixed_header) - offset, fixed_header + offset);
+
+ if(mmbackend_send(data->fd, fixed_header, offset)
+ || (vh && vh_length && mmbackend_send(data->fd, vh, vh_length))
+ || (payload && payload_length && mmbackend_send(data->fd, payload, payload_length))){
+ LOGPF("Failed to transmit control message for %s, assuming connection failure", inst->name);
+ mqtt_disconnect(inst);
+ return 1;
+ }
+
+ data->last_control = mm_timestamp();
+ return 0;
+}
+
+static int mqtt_configure(char* option, char* value){
+ LOG("This backend does not take global configuration");
+ return 1;
+}
+
+static int mqtt_reconnect(instance* inst){
+ uint8_t variable_header[MQTT_BUFFER_LENGTH] = {0x00, 0x04, 'M', 'Q', 'T', 'T', MQTT_VERSION_DEFAULT, 0x00 /*flags*/, ((MQTT_KEEPALIVE * 2) >> 8) & 0xFF, (MQTT_KEEPALIVE * 2) & 0xFF};
+ uint8_t payload[MQTT_BUFFER_LENGTH];
+ size_t vh_offset = 10, payload_offset = 0;
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+
+ if(!data->host){
+ LOGPF("No host specified for instance %s", inst->name);
+ return 2;
+ }
+
+ if(data->fd >= 0){
+ mqtt_disconnect(inst);
+ }
+
+ LOGPF("Connecting instance %s to host %s port %s (TLS: %s, Authentication: %s, Protocol: %s)",
+ inst->name, data->host,
+ data->port ? data->port : (data->tls ? MQTT_TLS_PORT : MQTT_PORT),
+ data->tls ? "yes " : "no",
+ (data->user || data->password) ? "yes" : "no",
+ (data->mqtt_version == 0x05) ? "v5" : "v3.1.1");
+
+ data->fd = mmbackend_socket(data->host,
+ data->port ? data->port : (data->tls ? MQTT_TLS_PORT : MQTT_PORT),
+ SOCK_STREAM, 0, 0, 1);
+
+ if(data->fd < 0){
+ //retry later
+ return 1;
+ }
+
+ //prepare CONNECT message header
+ variable_header[6] = data->mqtt_version;
+ variable_header[7] = 0x02 /*clean start*/ | (data->user ? 0x80 : 0x00) | (data->user ? 0x40 : 0x00);
+
+ if(data->mqtt_version == 0x05){ //mqtt v5 has additional options
+ //push number of option bytes (as a varint, no less) before actually pushing the option data.
+ //obviously someone thought saving 3 whole bytes in exchange for not being able to sequentially creating the package was smart..
+ variable_header[vh_offset++] = 8;
+ //push maximum packet size option
+ variable_header[vh_offset++] = 0x27;
+ variable_header[vh_offset++] = (MQTT_BUFFER_LENGTH >> 24) & 0xFF;
+ variable_header[vh_offset++] = (MQTT_BUFFER_LENGTH >> 16) & 0xFF;
+ variable_header[vh_offset++] = (MQTT_BUFFER_LENGTH >> 8) & 0xFF;
+ variable_header[vh_offset++] = (MQTT_BUFFER_LENGTH) & 0xFF;
+ //push topic alias maximum option
+ variable_header[vh_offset++] = 0x22;
+ variable_header[vh_offset++] = 0xFF;
+ variable_header[vh_offset++] = 0xFF;
+ }
+
+ //prepare CONNECT payload
+ //push client id
+ payload_offset += mqtt_push_utf8(payload + payload_offset, sizeof(payload) - payload_offset, data->client_id);
+ if(data->user){
+ payload_offset += mqtt_push_utf8(payload + payload_offset, sizeof(payload) - payload_offset, data->user);
+ }
+ if(data->password){
+ payload_offset += mqtt_push_utf8(payload + payload_offset, sizeof(payload) - payload_offset, data->password);
+ }
+
+ mqtt_transmit(inst, MSG_CONNECT, vh_offset, variable_header, payload_offset, payload);
+
+ //register the fd
+ if(mm_manage_fd(data->fd, BACKEND_NAME, 1, (void*) inst)){
+ LOG("Failed to register FD");
+ return 2;
+ }
+
+ return 0;
+}
+
+static int mqtt_configure_channel(instance* inst, char* option, char* value){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ char* next_token = NULL;
+ channel* configure = NULL;
+ uint8_t mark = 0;
+ mqtt_channel_value config = {
+ 0
+ };
+
+ if(!strncmp(value, "range ", 6)){
+ //we support min > max for range configurations
+ value += 6;
+
+ config.min = strtod(value, &next_token);
+ if(value == next_token){
+ LOGPF("Failed to parse range preconfiguration for topic %s.%s", inst->name, option);
+ return 1;
+ }
+
+ config.max = strtod(next_token, &value);
+ if(value == next_token){
+ LOGPF("Failed to parse range preconfiguration for topic %s.%s", inst->name, option);
+ return 1;
+ }
+ }
+ else if(!strncmp(value, "discrete ", 9)){
+ value += 9;
+
+ for(; *value && isspace(*value); value++){
+ }
+ if(value[0] == '!'){
+ mark = 1;
+ value++;
+ }
+ config.min = clamp(strtod(value, &next_token), 1.0, 0.0);
+ value = next_token;
+
+ for(; *value && isspace(*value); value++){
+ }
+ if(value[0] == '!'){
+ mark = 2;
+ value++;
+ }
+
+ config.max = clamp(strtod(value, &next_token), 1.0, 0.0);
+ value = next_token;
+ if(config.max < config.min){
+ LOGPF("Discrete topic configuration for %s.%s has invalid limit ordering", inst->name, option);
+ return 1;
+ }
+
+ for(; *value && isspace(*value); value++){
+ }
+
+ config.discrete = strdup(value);
+ config.normal = mark ? ((mark == 1) ? config.min : config.max) : (config.min + (config.max - config.min) / 2);
+ }
+ else{
+ LOGPF("Unknown instance configuration option or invalid preconfiguration %s on instance %s", option, inst->name);
+ return 1;
+ }
+
+ configure = mqtt_channel(inst, option, 0);
+ if(!configure
+ //if configuring scale, no other config is possible
+ || (!config.discrete && data->channel[configure->ident].values)
+ //if configuring discrete, the previous one can't be a a scale
+ || (config.discrete && data->channel[configure->ident].values && !data->channel[configure->ident].value[0].discrete)){
+ LOGPF("Failed to configure topic %s.%s", inst->name, option);
+ free(config.discrete);
+ return 1;
+ }
+
+ data->channel[configure->ident].value = realloc(data->channel[configure->ident].value, (data->channel[configure->ident].values + 1) * sizeof(mqtt_channel_value));
+ if(!data->channel[configure->ident].value){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ DBGPF("Configuring value on %s.%s: min %f max %f normal %f discrete %s", inst->name, option, config.min, config.max, config.normal, config.discrete ? config.discrete : "-");
+ data->channel[configure->ident].value[data->channel[configure->ident].values] = config;
+ data->channel[configure->ident].values++;
+ DBGPF("Value configuration for %s.%s now at %" PRIsize_t " entries", inst->name, option, data->channel[configure->ident].values);
+ return 0;
+}
+
+static int mqtt_configure_instance(instance* inst, char* option, char* value){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+
+ if(!strcmp(option, "user")){
+ mmbackend_strdup(&(data->user), value);
+ return 0;
+ }
+ else if(!strcmp(option, "password")){
+ mmbackend_strdup(&(data->password), value);
+ return 0;
+ }
+ else if(!strcmp(option, "host")){
+ if(mqtt_parse_hostspec(inst, value)){
+ return 1;
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "clientid")){
+ if(strlen(value)){
+ mmbackend_strdup(&(data->client_id), value);
+ return 0;
+ }
+ else{
+ return mqtt_generate_instanceid(inst);
+ }
+ }
+ else if(!strcmp(option, "protocol")){
+ data->mqtt_version = MQTT_VERSION_DEFAULT;
+ if(!strcmp(value, "3.1.1")){
+ data->mqtt_version = 4;
+ }
+ return 0;
+ }
+
+ //try to register as channel preconfig
+ return mqtt_configure_channel(inst, option, value);
+}
+
+static int mqtt_push_subscriptions(instance* inst){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ uint8_t variable_header[3] = {0};
+ uint8_t payload[MQTT_BUFFER_LENGTH];
+ size_t u, subs = 0, payload_offset = 0;
+
+ //FIXME might want to aggregate multiple subscribes into one packet
+ for(u = 0; u < data->nchannels; u++){
+ payload_offset = 0;
+ if(data->channel[u].flags & mmchannel_input){
+ DBGPF("Subscribing %s.%s, channel %" PRIsize_t ", flags %d", inst->name, data->channel[u].topic, u, data->channel[u].flags);
+ variable_header[0] = (data->packet_identifier >> 8) & 0xFF;
+ variable_header[1] = (data->packet_identifier) & 0xFF;
+
+ payload_offset += mqtt_push_utf8(payload + payload_offset, sizeof(payload) - payload_offset, data->channel[u].topic);
+ payload[payload_offset++] = (data->mqtt_version == 0x05) ? MQTT5_NO_LOCAL : 0;
+
+ data->packet_identifier++;
+ //zero is not a valid packet identifier
+ if(!data->packet_identifier){
+ data->packet_identifier++;
+ }
+
+ mqtt_transmit(inst, MSG_SUBSCRIBE, data->mqtt_version == 0x05 ? 3 : 2, variable_header, payload_offset, payload);
+ subs++;
+ }
+ }
+
+ LOGPF("Subscribed %" PRIsize_t " channels on %s", subs, inst->name);
+ return 0;
+}
+
+static int mqtt_instance(instance* inst){
+ mqtt_instance_data* data = calloc(1, sizeof(mqtt_instance_data));
+
+ if(!data){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ data->fd = -1;
+ data->mqtt_version = MQTT_VERSION_DEFAULT;
+ data->packet_identifier = 1;
+ data->current_alias = 1;
+ inst->impl = data;
+
+ if(mqtt_generate_instanceid(inst)){
+ return 1;
+ }
+ return 0;
+}
+
+static channel* mqtt_channel(instance* inst, char* spec, uint8_t flags){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ size_t u;
+
+ //check spec for compliance
+ if(strchr(spec, '+') || strchr(spec, '#')){
+ LOGPF("Invalid character in channel specification %s", spec);
+ return NULL;
+ }
+
+ //find matching channel
+ for(u = 0; u < data->nchannels; u++){
+ if(!strcmp(spec, data->channel[u].topic)){
+ data->channel[u].flags |= flags;
+ DBGPF("Reusing existing channel %" PRIsize_t " for spec %s.%s, flags are now %02X", u, inst->name, spec, data->channel[u].flags);
+ break;
+ }
+ }
+
+ //allocate new channel
+ if(u == data->nchannels){
+ data->channel = realloc(data->channel, (data->nchannels + 1) * sizeof(mqtt_channel_data));
+ if(!data->channel){
+ LOG("Failed to allocate memory");
+ return NULL;
+ }
+
+ data->channel[u].topic = strdup(spec);
+ data->channel[u].topic_alias_sent = 0;
+ data->channel[u].topic_alias_rcvd = 0;
+ data->channel[u].flags = flags;
+ data->channel[u].values = 0;
+ data->channel[u].value = NULL;
+
+ if(!data->channel[u].topic){
+ LOG("Failed to allocate memory");
+ return NULL;
+ }
+
+ DBGPF("Allocated channel %" PRIsize_t " for spec %s.%s, flags are %02X", u, inst->name, spec, data->channel[u].flags);
+ data->nchannels++;
+ }
+
+ return mm_channel(inst, u, 1);
+}
+
+static int mqtt_maintenance(){
+ size_t n, u;
+ instance** inst = NULL;
+ mqtt_instance_data* data = NULL;
+
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ LOG("Failed to fetch instance list");
+ return 1;
+ }
+
+ DBGPF("Running maintenance operations on %" PRIsize_t " instances", n);
+ for(u = 0; u < n; u++){
+ data = (mqtt_instance_data*) inst[u]->impl;
+ if(data->fd <= 0){
+ if(mqtt_reconnect(inst[u]) >= 2){
+ LOGPF("Failed to reconnect instance %s, terminating", inst[u]->name);
+ free(inst);
+ return 1;
+ }
+ }
+ else if(data->last_control && mm_timestamp() - data->last_control >= MQTT_KEEPALIVE * 1000){
+ //send keepalive ping requests
+ mqtt_transmit(inst[u], MSG_PINGREQ, 0, NULL, 0, NULL);
+ }
+ }
+
+ free(inst);
+ return 0;
+}
+
+static int mqtt_deserialize(instance* inst, channel* output, mqtt_channel_data* input, char* buffer, size_t length){
+ char* next_token = NULL, conversion_buffer[1024] = {0};
+ channel_value val;
+ double range, raw;
+ size_t u;
+ //FIXME implement json subchannels
+
+ //unconfigured channel
+ if(!input->values){
+ //the original buffer is the result of an unterminated receive, move it over
+ memcpy(conversion_buffer, buffer, length);
+ val.normalised = clamp(strtod(conversion_buffer, &next_token), 1.0, 0.0);
+ if(conversion_buffer == next_token){
+ LOGPF("Failed to parse incoming data for %s.%s", inst->name, input->topic);
+ return 1;
+ }
+ }
+ //ranged channel
+ else if(!input->value[0].discrete){
+ memcpy(conversion_buffer, buffer, length);
+ raw = clamp(strtod(conversion_buffer, &next_token), max(input->value[0].max, input->value[0].min), min(input->value[0].max, input->value[0].min));
+ if(conversion_buffer == next_token){
+ LOGPF("Failed to parse incoming data for %s.%s", inst->name, input->topic);
+ return 1;
+ }
+ range = fabs(input->value[0].max - input->value[0].min);
+ val.normalised = (raw - input->value[0].min) / range;
+ if(input->value[0].max < input->value[0].min){
+ val.normalised = fabs(val.normalised);
+ }
+ }
+ else{
+ for(u = 0; u < input->values; u++){
+ if(length == strlen(input->value[u].discrete)
+ && !strncmp(input->value[u].discrete, buffer, length)){
+ val.normalised = input->value[u].normal;
+ break;
+ }
+ }
+
+ if(u == input->values){
+ LOGPF("Failed to parse incoming data for %s.%s, no matching discrete token", inst->name, input->topic);
+ return 1;
+ }
+ }
+
+ val.normalised = clamp(val.normalised, 1.0, 0.0);
+ mm_channel_event(output, val);
+ return 0;
+}
+
+static size_t mqtt_serialize(instance* inst, mqtt_channel_data* input, char* output, size_t length, double value){
+ double range;
+ size_t u, invert = 0;
+
+ //unconfigured channel
+ if(!input->values){
+ return snprintf(output, length, "%f", value);
+ }
+ //ranged channel
+ else if(!input->value[0].discrete){
+ range = fabs(input->value[0].max - input->value[0].min);
+ if(input->value[0].max < input->value[0].min){
+ invert = 1;
+ }
+ return snprintf(output, length, "%f", (value * range) * (invert ? -1 : 1) + input->value[0].min);
+ }
+ else{
+ for(u = 0; u < input->values; u++){
+ if(input->value[u].min <= value
+ && input->value[u].max >= value){
+ memcpy(output, input->value[u].discrete, min(strlen(input->value[u].discrete), length));
+ return min(strlen(input->value[u].discrete), length);
+ }
+ }
+ }
+
+ LOGPF("No discrete value on %s.%s defined for normalized value %f", inst->name, input->topic, value);
+ return 0;
+}
+
+static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ uint8_t variable_header[MQTT_BUFFER_LENGTH];
+ uint8_t payload[MQTT_BUFFER_LENGTH], alias_assigned = 0;
+ size_t vh_length = 0, payload_length = 0, u;
+
+ for(u = 0; u < num; u++){
+ vh_length = payload_length = alias_assigned = 0;
+
+ if(data->mqtt_version == 0x05){
+ if(data->channel[c[u]->ident].topic_alias_sent){
+ //push zero-length topic
+ variable_header[vh_length++] = 0;
+ variable_header[vh_length++] = 0;
+ }
+ else{
+ //push topic
+ vh_length += mqtt_push_utf8(variable_header + vh_length, sizeof(variable_header) - vh_length, data->channel[c[u]->ident].topic);
+ //generate topic alias if possible
+ if(data->current_alias <= data->server_max_alias){
+ data->channel[c[u]->ident].topic_alias_sent = data->current_alias++;
+ DBGPF("Assigned outbound topic alias %" PRIu16 " to topic %s.%s", data->channel[c[u]->ident].topic_alias_sent, inst->name, data->channel[c[u]->ident].topic);
+
+ alias_assigned = 1;
+ }
+ }
+
+ //push property length
+ variable_header[vh_length++] = (data->channel[c[u]->ident].topic_alias_sent) ? 5 : 2;
+
+ //push payload type (0x01)
+ variable_header[vh_length++] = 0x01;
+ variable_header[vh_length++] = 1;
+
+ if(data->channel[c[u]->ident].topic_alias_sent){
+ //push topic alias (0x23)
+ variable_header[vh_length++] = 0x23;
+ variable_header[vh_length++] = (data->channel[c[u]->ident].topic_alias_sent >> 8) & 0xFF;
+ variable_header[vh_length++] = data->channel[c[u]->ident].topic_alias_sent & 0xFF;
+ }
+
+ payload_length = mqtt_serialize(inst, data->channel + c[u]->ident, (char*) (payload + 2), sizeof(payload) - 2, v[u].normalised);
+ if(payload_length){
+ payload[0] = (payload_length >> 8) & 0xFF;
+ payload[1] = payload_length & 0xFF;
+ payload_length += 2;
+ }
+ }
+ else{
+ //push topic
+ vh_length += mqtt_push_utf8(variable_header + vh_length, sizeof(variable_header) - vh_length, data->channel[c[u]->ident].topic);
+ if(data->mqtt_version == 0x05){
+ //push property length
+ variable_header[vh_length++] = 2;
+
+ //push payload type (0x01)
+ variable_header[vh_length++] = 0x01;
+ variable_header[vh_length++] = 1;
+ }
+ payload_length = mqtt_serialize(inst, data->channel + c[u]->ident, (char*) payload, sizeof(payload), v[u].normalised);
+ }
+
+ if(payload_length){
+ DBGPF("Transmitting %" PRIsize_t " bytes for %s", payload_length, inst->name);
+ mqtt_transmit(inst, MSG_PUBLISH, vh_length, variable_header, payload_length, payload);
+ }
+ else if(alias_assigned){
+ //undo alias assignment
+ data->channel[c[u]->ident].topic_alias_sent = 0;
+ data->current_alias--;
+ }
+ }
+
+ return 0;
+}
+
+static int mqtt_handle_publish(instance* inst, uint8_t type, uint8_t* variable_header, size_t length){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ char* topic = NULL, *payload = NULL;
+ channel* changed = NULL;
+ uint8_t qos = (type & 0x06) >> 1, content_utf8 = 0;
+ uint16_t topic_alias = 0;
+ uint32_t property_length = 0;
+ size_t u = data->nchannels, property_offset, payload_offset, payload_length;
+ size_t topic_length = mqtt_pop_utf8(variable_header, length, &topic);
+
+ property_offset = payload_offset = topic_length + 2 + ((qos > 0) ? 2 : 0);
+ if(data->mqtt_version == 0x05){
+ //read properties length
+ payload_offset += mqtt_pop_varint(variable_header + property_offset, length - property_offset, &property_length);
+ payload_offset += property_length;
+
+ property_offset += mqtt_pop_varint(variable_header + property_offset, length - property_offset, NULL);
+ //parse properties
+ while(property_offset < payload_offset){
+ DBGPF("Property %02X at offset %" PRIsize_t " of %" PRIu32, variable_header[property_offset], property_offset, property_length);
+ //read payload format indicator
+ if(variable_header[property_offset] == 0x01){
+ content_utf8 = variable_header[property_offset + 1];
+ }
+ //read topic alias
+ else if(variable_header[property_offset] == 0x23){
+ topic_alias = (variable_header[property_offset + 1] << 8) | variable_header[property_offset + 2];
+ }
+
+ property_offset += mqtt_pop_property(variable_header + property_offset, length - property_offset);
+ }
+ }
+
+ //match via topic alias
+ if(!topic_length && topic_alias){
+ for(u = 0; u < data->nchannels; u++){
+ if(data->channel[u].topic_alias_rcvd == topic_alias){
+ break;
+ }
+ }
+ }
+ //match via topic
+ else if(topic_length){
+ for(u = 0; u < data->nchannels; u++){
+ if(!strncmp(data->channel[u].topic, topic, topic_length)){
+ break;
+ }
+ }
+
+ if(topic_alias){
+ data->channel[u].topic_alias_rcvd = topic_alias;
+ }
+ }
+
+ if(content_utf8){
+ payload_length = mqtt_pop_utf8(variable_header + payload_offset, length - payload_offset, &payload);
+ }
+ else{
+ payload_length = length - payload_offset;
+ payload = (char*) (variable_header + payload_offset);
+ }
+
+ if(u != data->nchannels && payload_length && payload){
+ DBGPF("Received PUBLISH for %s.%s, QoS %d, payload length %" PRIsize_t, inst->name, data->channel[u].topic, qos, payload_length);
+ changed = mm_channel(inst, u, 0);
+ if(changed){
+ mqtt_deserialize(inst, changed, data->channel + u, payload, payload_length);
+ }
+ }
+ return 0;
+}
+
+static int mqtt_handle_connack(instance* inst, uint8_t type, uint8_t* variable_header, size_t length){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ size_t property_offset = 2;
+
+ if(length >= 2){
+ if(variable_header[1]){
+ if(variable_header[1] == 1 && data->mqtt_version == 0x05){
+ LOGPF("Connection on %s was rejected for protocol incompatibility, downgrading to protocol 3.1.1", inst->name);
+ data->mqtt_version = 0x04;
+ return 0;
+ }
+ LOGPF("Connection on %s was rejected, reason code %d", inst->name, variable_header[1]);
+ mqtt_disconnect(inst);
+ return 0;
+ }
+
+ //parse response properties if present
+ if(data->mqtt_version == 0x05){
+ property_offset += mqtt_pop_varint(variable_header + property_offset, length - property_offset, NULL);
+ while(property_offset < length){
+ DBGPF("Property %02X at offset %" PRIsize_t " of %" PRIsize_t, variable_header[property_offset], property_offset, length);
+
+ //read maximum topic alias
+ if(variable_header[property_offset] == 0x22){
+ data->server_max_alias = (variable_header[property_offset + 1] << 8) | variable_header[property_offset + 2];
+ DBGPF("Connection supports maximum connection alias %" PRIu16, data->server_max_alias);
+ }
+
+ property_offset += mqtt_pop_property(variable_header + property_offset, length - property_offset);
+ }
+ }
+
+ LOGPF("Connection on %s established", inst->name);
+ return mqtt_push_subscriptions(inst);
+ }
+
+ LOGPF("Received malformed CONNACK on %s", inst->name);
+ return 1;
+}
+
+static int mqtt_handle_message(instance* inst, uint8_t type, uint8_t* variable_header, size_t length){
+ switch(type){
+ case MSG_CONNACK:
+ return mqtt_handle_connack(inst, type, variable_header, length);
+ case MSG_PINGRESP:
+ case MSG_SUBACK:
+ //ignore most responses
+ //FIXME error check SUBACK
+ break;
+ default:
+ if((type & 0xF0) == MSG_PUBLISH){
+ return mqtt_handle_publish(inst, type, variable_header, length);
+ }
+ LOGPF("Unhandled MQTT message type 0x%02X on %s", type, inst->name);
+ }
+ return 0;
+}
+
+static int mqtt_handle_fd(instance* inst){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ ssize_t bytes_read = 0, bytes_left = sizeof(data->receive_buffer) - data->receive_offset;
+ uint32_t message_length = 0, header_length = 0;
+
+ bytes_read = recv(data->fd, data->receive_buffer + data->receive_offset, bytes_left, 0);
+ if(bytes_read < 0){
+ LOGPF("Failed to receive data on instance %s: %s", inst->name, mmbackend_socket_strerror(errno));
+ return 1;
+ }
+ else if(bytes_read == 0){
+ //disconnected, try to reconnect
+ LOGPF("Instance %s disconnected, reconnection queued", inst->name);
+ mqtt_disconnect(inst);
+ return 1;
+ }
+
+ DBGPF("Instance %s, offset %" PRIsize_t ", read %" PRIsize_t " bytes", inst->name, data->receive_offset, bytes_read);
+ data->receive_offset += bytes_read;
+
+ while(data->receive_offset >= 2){
+ //check for complete message
+ header_length = mqtt_pop_varint(data->receive_buffer + 1, data->receive_offset - 1, &message_length);
+ if(header_length && data->receive_offset >= message_length + header_length + 1){
+ DBGPF("Received complete message of %" PRIu32 " bytes, total received %" PRIsize_t ", payload %" PRIu32 ", message type %02X", message_length + header_length + 1, data->receive_offset, message_length, data->receive_buffer[0]);
+ if(mqtt_handle_message(inst, data->receive_buffer[0], data->receive_buffer + header_length + 1, message_length)){
+ //TODO handle failures properly
+ }
+
+ //remove handled message
+ if(data->receive_offset > message_length + header_length + 1){
+ memmove(data->receive_buffer, data->receive_buffer + message_length + header_length + 1, data->receive_offset - (message_length + header_length + 1));
+ }
+ data->receive_offset -= message_length + header_length + 1;
+ }
+ else{
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static int mqtt_handle(size_t num, managed_fd* fds){
+ size_t n = 0;
+
+ for(n = 0; n < num; n++){
+ if(mqtt_handle_fd((instance*) fds[n].impl) >= 2){
+ //propagate critical failures
+ return 1;
+ }
+ }
+
+ //keepalive/reconnect processing
+ if(last_maintenance && mm_timestamp() - last_maintenance >= MQTT_KEEPALIVE * 1000){
+ if(mqtt_maintenance()){
+ return 1;
+ }
+ last_maintenance = mm_timestamp();
+ }
+
+ return 0;
+}
+
+static int mqtt_start(size_t n, instance** inst){
+ size_t u = 0, fds = 0;
+
+ for(u = 0; u < n; u++){
+ switch(mqtt_reconnect(inst[u])){
+ case 1:
+ LOGPF("Failed to connect to host for instance %s, will be retried", inst[u]->name);
+ break;
+ case 2:
+ LOGPF("Failed to connect to host for instance %s, aborting", inst[u]->name);
+ return 1;
+ default:
+ fds++;
+ break;
+ }
+ }
+ LOGPF("Registered %" PRIsize_t " descriptors to core", fds);
+
+ //initialize maintenance timer
+ last_maintenance = mm_timestamp();
+ return 0;
+}
+
+static int mqtt_shutdown(size_t n, instance** inst){
+ size_t u, p, v;
+ mqtt_instance_data* data = NULL;
+
+ for(u = 0; u < n; u++){
+ data = (mqtt_instance_data*) inst[u]->impl;
+ mqtt_disconnect(inst[u]);
+
+ for(p = 0; p < data->nchannels; p++){
+ for(v = 0; v < data->channel[p].values; v++){
+ free(data->channel[p].value[v].discrete);
+ }
+ free(data->channel[p].value);
+ free(data->channel[p].topic);
+ }
+ free(data->channel);
+ free(data->host);
+ free(data->port);
+ free(data->user);
+ free(data->password);
+ free(data->client_id);
+
+ free(inst[u]->impl);
+ inst[u]->impl = NULL;
+ }
+
+ LOG("Backend shut down");
+ return 0;
+}
diff --git a/backends/mqtt.h b/backends/mqtt.h
new file mode 100644
index 0000000..c684f99
--- /dev/null
+++ b/backends/mqtt.h
@@ -0,0 +1,87 @@
+#include "midimonster.h"
+
+MM_PLUGIN_API int init();
+static int mqtt_configure(char* option, char* value);
+static int mqtt_configure_instance(instance* inst, char* option, char* value);
+static int mqtt_instance(instance* inst);
+static channel* mqtt_channel(instance* inst, char* spec, uint8_t flags);
+static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int mqtt_handle(size_t num, managed_fd* fds);
+static int mqtt_start(size_t n, instance** inst);
+static int mqtt_shutdown(size_t n, instance** inst);
+
+#define MQTT_PORT "1883"
+#define MQTT_TLS_PORT "8883"
+#define MQTT_BUFFER_LENGTH 8192
+#define MQTT_KEEPALIVE 10
+#define MQTT_VERSION_DEFAULT 0x05
+
+#define MQTT5_NO_LOCAL 0x04
+
+enum /*_mqtt_property_storage_classes*/ {
+ STORAGE_U8,
+ STORAGE_U16,
+ STORAGE_U32,
+ STORAGE_VARINT,
+ STORAGE_PREFIXED,
+ STORAGE_PREFIXPAIR
+};
+
+enum {
+ MSG_RESERVED = 0x00,
+ MSG_CONNECT = 0x10,
+ MSG_CONNACK = 0x20,
+ MSG_PUBLISH = 0x30,
+ MSG_PUBACK = 0x40,
+ MSG_PUBREC = 0x50,
+ MSG_PUBREL = 0x60,
+ MSG_PUBCOMP = 0x70,
+ MSG_SUBSCRIBE = 0x82,
+ MSG_SUBACK = 0x90,
+ MSG_UNSUBSCRIBE = 0xA0,
+ MSG_UNSUBACK = 0xB0,
+ MSG_PINGREQ = 0xC0,
+ MSG_PINGRESP = 0xD0,
+ MSG_DISCONNECT = 0xE0,
+ MSG_AUTH = 0xF0
+};
+
+typedef struct /*_mqtt_value_mapping*/ {
+ double min;
+ double max;
+ double normal;
+ char* discrete;
+} mqtt_channel_value;
+
+typedef struct /*_mqtt_channel*/ {
+ char* topic;
+ uint16_t topic_alias_sent;
+ uint16_t topic_alias_rcvd;
+ uint8_t flags;
+
+ size_t values;
+ mqtt_channel_value* value;
+} mqtt_channel_data;
+
+typedef struct /*_mqtt_instance_data*/ {
+ uint8_t tls;
+ char* host;
+ char* port;
+ uint8_t mqtt_version;
+
+ char* user;
+ char* password;
+ char* client_id;
+
+ size_t nchannels;
+ mqtt_channel_data* channel;
+
+ int fd;
+ uint8_t receive_buffer[MQTT_BUFFER_LENGTH];
+ size_t receive_offset;
+
+ uint64_t last_control;
+ uint16_t packet_identifier;
+ uint16_t server_max_alias;
+ uint16_t current_alias;
+} mqtt_instance_data;
diff --git a/backends/mqtt.md b/backends/mqtt.md
new file mode 100644
index 0000000..85784ef
--- /dev/null
+++ b/backends/mqtt.md
@@ -0,0 +1,85 @@
+### The `mqtt` backend
+
+This backend provides input from and output to a message queueing telemetry transport (MQTT)
+broker. The MQTT protocol is used in lightweight sensor/actor applications, a wide selection
+of smart home implementations and as a generic message bus in many other domains.
+
+The backend implements both the older protocol version MQTT v3.1.1 as well as the current specification
+for MQTT v5.0.
+
+#### Global configuration
+
+This backend does not take any global configuration.
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|---------------------------------------|
+| `host` | `mqtt://10.23.23.1` | none | Host or URI of the MQTT broker |
+| `user` | `midimonster` | none | User name for broker authentication |
+| `password` | `mm` | none | Password for broker authentication |
+| `clientid` | `MM-main` | random | MQTT client identifier (generated randomly at start if unset) |
+| `protocol` | `3.1.1` | `5` | MQTT protocol version (`5` or `3.1.1`) to use for the connection |
+
+The `host` option can be specified as an URI of the form `mqtt[s]://[username][:password]@host.domain[:port]`.
+This allows specifying all necessary settings in one configuration option.
+
+#### Data exchange format
+
+The MQTT protocol places very few restrictions on the exchanged data. Thus, it is necessary to specify the input
+and output data formats accepted respectively output by the MIDIMonster.
+
+The basic format, without further channel-specific configuration, is an ASCII/UTF-8 string representing a floating
+point number between `0.0` and `1.0`. The MIDIMonster will read these and use the value as the normalized event value.
+
+Channels may be specified to use a different value range or even freeform discrete values by preconfiguring
+the channels in the instance configuration section. This is done by specifying options of the form
+
+```
+<channel> = range <min> <max>
+<channel> = discrete [!]<min> [!]<max> <value>
+```
+
+Example configurations:
+```
+/a/topic = range -10 10
+/another/topic = discrete !0.0 0.5 off
+/another/topic = discrete 0.5 !1.0 on
+```
+
+Note that there may be only one range configuration per topic, but there may be multiple discrete configurations.
+
+The first channel preconfiguration example will change the channel value scale to values between `-10` and `10`.
+For input channels, this sets the normalization range. The MIDIMonster will normalize the input value according to the scale.
+For output channels, this sets the output scaling factors.
+
+The second and third channel preconfigurations define two discrete values (`on` and `off`) with accompanying normalized
+values. For input channels, the normalized channel value for a discrete input will be the value marked with an exclamation mark `!`.
+For output channels, the output will be the first discrete value for which the range between `<min>` and `<max>` contains
+the normalized channel value.
+
+These examples mean
+* For `/a/topic`, when mapped as input, the input value `5.0` will generate a normalized event value of `0.75`.
+* For `/a/topic`, when mapped as output, a normalized event value `0.25` will generate an output of `-5.0`.
+* For `/another/topic`, when mapped as an input, the input value `off` will generate a normalized event value of `0.0`.
+* For `/another/topic`, when mapped as an output, a normalized event value of `0.75` will generate an output of `on`.
+
+Values above the maximum or below the minimum will be clamped. The MIDIMonster will not output values out of the
+configured bounds.
+
+#### Channel specification
+
+A channel specification may be any MQTT topic designator not containing the wildcard characters `+` and `#`.
+
+Example mapping:
+```
+mq1./midimonster/in > mq2./midimonster/out
+```
+
+#### Known bugs / problems
+
+If the connection to a server is lost, the connection will be retried in approximately 10 seconds.
+If the server rejects the connection with reason code `0x01`, a protocol failure is assumed. If the initial
+connection was made with `MQTT v5.0`, it is retried with the older protocol version `MQTT v3.1.1`.
+
+Support for TLS-secured connections is planned, but not yet implemented.
diff --git a/backends/osc.c b/backends/osc.c
index 5887a50..e8673bb 100644
--- a/backends/osc.c
+++ b/backends/osc.c
@@ -1,4 +1,5 @@
#define BACKEND_NAME "osc"
+//#define DEBUG
#include <string.h>
#include <ctype.h>
@@ -629,7 +630,7 @@ static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags){
data->channel[u].out = calloc(data->channel[u].params, sizeof(osc_parameter_value));
}
else if(data->patterns){
- LOGPF("No pattern match found for %s", spec);
+ LOGPF("No preconfigured pattern match found for %s", spec);
}
if(!data->channel[u].path
diff --git a/backends/osc.md b/backends/osc.md
index 1446e06..61b3324 100644
--- a/backends/osc.md
+++ b/backends/osc.md
@@ -78,7 +78,7 @@ configuration.
#### Supported types & value ranges
OSC allows controls to have individual value ranges and supports different parameter types.
-The following types are currently supported by the MIDImonster:
+The following types are currently supported by the MIDIMonster:
* **i**: 32-bit signed integer
* **f**: 32-bit IEEE floating point
diff --git a/backend.c b/core/backend.c
index 16e095c..8a8588f 100644
--- a/backend.c
+++ b/core/backend.c
@@ -94,9 +94,9 @@ int backends_notify(size_t nev, channel** c, channel_value* v){
MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create){
size_t u, bucket = channelstore_hash(inst, ident);
- DBGPF("\tSearching for inst %" PRIu64 " ident %" PRIu64, inst, ident);
+ DBGPF("\tSearching for inst %" PRIu64 " ident %" PRIu64, (uint64_t) inst, ident);
for(u = 0; u < channels.n[bucket]; u++){
- DBGPF("\tBucket %" PRIsize_t " entry %" PRIsize_t " inst %" PRIu64 " ident %" PRIu64, bucket, u, channels.entry[bucket][u]->instance, channels.entry[bucket][u]->ident);
+ DBGPF("\tBucket %" PRIsize_t " entry %" PRIsize_t " inst %" PRIu64 " ident %" PRIu64, bucket, u, (uint64_t) channels.entry[bucket][u]->instance, channels.entry[bucket][u]->ident);
if(channels.entry[bucket][u]->instance == inst
&& channels.entry[bucket][u]->ident == ident){
DBGPF("Requested channel %" PRIu64 " on instance %s already exists, reusing (bucket %" PRIsize_t ", %" PRIsize_t " search steps)\n", ident, inst->name, bucket, u);
@@ -128,6 +128,49 @@ MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create){
return channels.entry[bucket][(channels.n[bucket]++)];
}
+MM_API void mm_channel_update(channel* chan, uint64_t ident){
+ size_t bucket = channelstore_hash(chan->instance, chan->ident), new_bucket = channelstore_hash(chan->instance, ident);
+ size_t u;
+
+ DBGPF("Updating identifier for inst %" PRIu64 " ident %" PRIu64 " (bucket %" PRIsize_t " to %" PRIsize_t ") to %" PRIu64, (uint64_t) chan->instance, chan->ident, bucket, new_bucket, ident);
+
+ if(bucket == new_bucket){
+ chan->ident = ident;
+ return;
+ }
+
+ for(u = 0; u < channels.n[bucket]; u++){
+ if(channels.entry[bucket][u]->instance == chan->instance
+ && channels.entry[bucket][u]->ident == chan->ident){
+ break;
+ }
+ }
+
+ if(u == channels.n[bucket]){
+ DBGPF("Failed to find channel to update in bucket %" PRIsize_t, bucket);
+ return;
+ }
+
+ DBGPF("Removing channel from slot %" PRIsize_t " of %" PRIsize_t " of bucket %" PRIsize_t, u, channels.n[bucket], bucket);
+ //remove channel from old bucket
+ for(; u < channels.n[bucket] - 1; u++){
+ channels.entry[bucket][u] = channels.entry[bucket][u + 1];
+ }
+
+ //add to new bucket
+ channels.entry[new_bucket] = realloc(channels.entry[new_bucket], (channels.n[new_bucket] + 1) * sizeof(channel*));
+ if(!channels.entry[new_bucket]){
+ fprintf(stderr, "Failed to allocate memory\n");
+ channels.n[new_bucket] = 0;
+ return;
+ }
+
+ channels.entry[new_bucket][channels.n[new_bucket]] = chan;
+ chan->ident = ident;
+ channels.n[bucket]--;
+ channels.n[new_bucket]++;
+}
+
instance* mm_instance(backend* b){
size_t u = 0, n = 0;
diff --git a/backend.h b/core/backend.h
index 6a69508..46c6c3a 100644
--- a/backend.h
+++ b/core/backend.h
@@ -12,6 +12,7 @@ instance* mm_instance(backend* b);
/* Backend API */
MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create);
+MM_API void mm_channel_update(channel* chan, uint64_t ident);
MM_API instance* mm_instance_find(char* name, uint64_t ident);
MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst);
MM_API int mm_backend_register(backend b);
diff --git a/config.c b/core/config.c
index c1c3124..c1c3124 100644
--- a/config.c
+++ b/core/config.c
diff --git a/config.h b/core/config.h
index b96a866..b96a866 100644
--- a/config.h
+++ b/core/config.h
diff --git a/plugin.c b/core/plugin.c
index e7d8eba..e7d8eba 100644
--- a/plugin.c
+++ b/core/plugin.c
diff --git a/plugin.h b/core/plugin.h
index 64c557f..64c557f 100644
--- a/plugin.h
+++ b/core/plugin.h
diff --git a/installer.sh b/installer.sh
index b18a7ef..2b9f799 100755
--- a/installer.sh
+++ b/installer.sh
@@ -1,7 +1,7 @@
#!/bin/bash
################################################ SETUP ################################################
-deps=(
+dependencies=(
libasound2-dev
libevdev-dev
liblua5.3-dev
@@ -77,24 +77,24 @@ ARGS(){
exit 0
;;
--install-dependencies)
- install_dependencies
+ install_dependencies "${dependencies[@]}"
exit 0
;;
-h|--help|*)
assign_defaults
- printf "${bold}Usage:${normal} ${0} ${c_green}[OPTIONS]${normal}"
- printf "\n\t${c_green}--prefix=${normal}${c_red}<path>${normal}\t\tSet the installation prefix.\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PREFIX"
+ printf "%sUsage: %s[OPTIONS]%s" "${bold}" "${normal} ${0} ${c_green}" "${normal}"
+ printf "\n\t%s--prefix=%s<path>%s\t\tSet the installation prefix.\t\t%sDefault:%s" "${c_green}" "${normal}${c_red}" "${normal}" "${c_mag}" "${normal} ${dim}$VAR_PREFIX${normal}"
printf "\n\t${c_green}--plugins=${normal}${c_red}<path>${normal}\tSet the plugin install path.\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PLUGINS"
printf "\n\t${c_green}--defcfg=${normal}${c_red}<path>${normal}\t\tSet the default configuration path.\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_DEFAULT_CFG"
printf "\n\t${c_green}--examples=${normal}${c_red}<path>${normal}\tSet the path for example configurations.\t${c_mag}Default:${normal} ${dim}%s${normal}\n" "$VAR_EXAMPLE_CFGS"
- printf "\n\t${c_green}--dev${normal}\t\t\tInstall nightly version."
- printf "\n\t${c_green}-d,\t--default${normal}\tUse default values to install."
- printf "\n\t${c_green}-fu,\t--forceupdate${normal}\tForce the updater to update without a version check."
- printf "\n\t${c_green}--selfupdate${normal}\t\tUpdates this script to the newest version and exit."
- printf "\n\t${c_green}--install-updater${normal}\tInstall the updater (Run with midimonster-updater) and exit."
- printf "\n\t${c_green}--install-dependencies${normal}\tInstall dependencies and exit"
- printf "\n\t${c_green}-h,\t--help${normal}\t\tShow this message and exit."
- printf "\n\t${uline}${bold}${c_mag}Each argument can be overwritten by another, the last one is used!.${normal}\n"
+ printf "\n\t%s--dev%s\t\t\tInstall nightly version." "${c_green}" "${normal}"
+ printf "\n\t%s-d,\t--default%s\tUse default values to install." "${c_green}" "${normal}"
+ printf "\n\t%s-fu,\t--forceupdate%s\tForce the updater to update without a version check." "${c_green}" "${normal}"
+ printf "\n\t%s--selfupdate%s\t\tUpdates this script to the newest version and exit." "${c_green}" "${normal}"
+ printf "\n\t%s--install-updater%s\tInstall the updater (Run with midimonster-updater) and exit." "${c_green}" "${normal}"
+ printf "\n\t%s--install-dependencies%s\tInstall dependencies and exit" "${c_green}" "${normal}"
+ printf "\n\t%s-h,\t--help%s\t\tShow this message and exit." "${c_green}" "${normal}"
+ printf "\n\t%sEach argument can be overwritten by another, the last one is used!.%s\n" "${uline}${bold}${c_mag}" "${normal}"
rmdir "$tmp_path"
exit 0
;;
@@ -105,40 +105,50 @@ ARGS(){
# Install unmatched dependencies
install_dependencies(){
- for dependency in ${deps[@]}; do
+ DEBIAN_FRONTEND=noninteractive apt-get update -y -qq > /dev/null || error_handler "There was an error doing apt update."
+# unset "$deps"
+ for dependency in "$@"; do
if [ "$(dpkg-query -W -f='${Status}' "$dependency" 2>/dev/null | grep -c "ok installed")" -eq 0 ]; then
- printf "Installing %s\n" "$dependency"
- apt-get install "$dependency"
+ deps+=("$dependency") # Add not installed dependency to the "to be installed array".
else
- printf "%s already installed!\n" "$dependency"
+ printf "%s already installed!\n" "$dependency" # If the dependency is already installed print it.
fi
done
+
+if [ ! "${#deps[@]}" -ge "1" ]; then # If nothing needs to get installed don't start apt.
+ printf "\nAll dependencies are fulfilled!\n" # Dependency array empty! Not running apt!
+else
+ printf "\nThen following dependencies are going to be installed:\n" # Dependency array contains items. Running apt.
+ printf "\n%s\n" "${deps[@]}" | sed 's/ /, /g'
+ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-suggests --no-install-recommends "${deps[@]}" > /dev/null || error_handler "There was an error doing dependency installation."
+ printf "\nAll dependencies are installed now!\n" # Dependency array empty! Not running apt!
+fi
printf "\n"
}
ask_questions(){
# Only say if necessary
if [ -z "$VAR_PREFIX" ] || [ -z "$VAR_PLUGINS" ] || [ -z "$VAR_DEFAULT_CFG" ] || [ -z "$VAR_EXAMPLE_CFGS" ]; then
- printf "${bold}If you don't know what you're doing, just hit enter a few times.${normal}\n\n"
+ printf "%sIf you don't know what you're doing, just hit enter a few times.%s\n\n" "${bold}" "${normal}"
fi
if [ -z "$VAR_PREFIX" ]; then
- read -e -i "$DEFAULT_PREFIX" -p "PREFIX (Install root directory): " input
+ read -r -e -i "$DEFAULT_PREFIX" -p "PREFIX (Install root directory): " input
VAR_PREFIX="${input:-$VAR_PREFIX}"
fi
if [ -z "$VAR_PLUGINS" ]; then
- read -e -i "$VAR_PREFIX$DEFAULT_PLUGINPATH" -p "PLUGINS (Plugin directory): " input
+ read -r -e -i "$VAR_PREFIX$DEFAULT_PLUGINPATH" -p "PLUGINS (Plugin directory): " input
VAR_PLUGINS="${input:-$VAR_PLUGINS}"
fi
if [ -z "$VAR_DEFAULT_CFG" ]; then
- read -e -i "$DEFAULT_CFGPATH" -p "Default config path: " input
+ read -r -e -i "$DEFAULT_CFGPATH" -p "Default config path: " input
VAR_DEFAULT_CFG="${input:-$VAR_DEFAULT_CFG}"
fi
if [ -z "$VAR_EXAMPLE_CFGS" ]; then
- read -e -i "$VAR_PREFIX$DEFAULT_EXAMPLES" -p "Example config directory: " input
+ read -r -e -i "$VAR_PREFIX$DEFAULT_EXAMPLES" -p "Example config directory: " input
VAR_EXAMPLE_CFGS="${input:-$VAR_EXAMPLE_CFGS}"
fi
}
@@ -151,7 +161,7 @@ prepare_repo(){
# If not set via argument, ask whether to install development build
if [ -z "$NIGHTLY" ]; then
- read -p "Do you want to install the latest development version? (y/n)? " magic
+ read -r -p "Do you want to install the latest development version? (y/n)? " magic
case "$magic" in
y|Y)
printf "OK! You´re a risky person ;D\n\n"
@@ -162,7 +172,7 @@ prepare_repo(){
NIGHTLY=0
;;
*)
- printf "${bold}Invalid input -- INSTALLING LATEST STABLE VERSION!${normal}\n\n"
+ printf "%sInvalid input -- INSTALLING LATEST STABLE VERSION!%s\n\n" "${bold}" "${normal}"
NIGHTLY=0
;;
esac
@@ -170,7 +180,7 @@ prepare_repo(){
# Roll back to last tag if a stable version was requested
if [ "$NIGHTLY" != 1 ]; then
- cd "$tmp_path"
+ cd "$tmp_path" || error_handler "Error doing cd to $tmp_path"
printf "Finding latest stable version...\n"
last_tag=$(git describe --abbrev=0)
printf "Checking out %s...\n" "$last_tag"
@@ -187,7 +197,7 @@ build(){
export DEFAULT_CFG="$VAR_DEFAULT_CFG"
export EXAMPLES="$VAR_EXAMPLE_CFGS"
- cd "$tmp_path"
+ cd "$tmp_path" || error_handler "Error doing cd to $tmp_path"
make clean
make "$makeargs"
make install
@@ -212,8 +222,11 @@ install_script(){
}
error_handler(){
- printf "\nAborting\n"
- exit 1
+ [[ -n $1 ]] && printf "\n%s\n" "$1"
+ printf "\nAborting"
+ for i in {1..3}; do sleep 0.3s && printf "." && sleep 0.2s; done
+ printf "\n"
+ exit "1"
}
cleanup(){
@@ -246,6 +259,7 @@ fi
# Check whether the updater needs to run
if [ -f "$updater_dir/updater.conf" ] || [ "$UPDATER_FORCE" = "1" ]; then
if [ -f "$updater_dir/updater.conf" ]; then
+ # shellcheck source=/dev/null
. "$updater_dir/updater.conf"
# Parse arguments again to compensate overwrite from source
ARGS "$@"
@@ -256,11 +270,11 @@ if [ -f "$updater_dir/updater.conf" ] || [ "$UPDATER_FORCE" = "1" ]; then
printf "Forcing the updater to start...\n\n"
elif [ -x "$VAR_PREFIX/bin/midimonster" ]; then
installed_version="$(midimonster --version)"
- if [[ "$installed_version" =~ "$latest_version" ]]; then
- printf "The installed version ${bold}$installed_version${normal} seems to be up to date\nDoing nothing\n\n"
+ if [[ "$installed_version" =~ $latest_version ]]; then
+ printf "The installed version %s seems to be up to date\nDoing nothing\n\n" "${bold}$installed_version${normal}"
exit 0
else
- printf "The installed version ${bold}$installed_version${normal} does not match the latest version ${bold}$latest_version${normal}\nMaybe you are running a development version?\n\n"
+ printf "The installed version %s does not match the latest version %s\nMaybe you are running a development version?\n\n" "${bold}$installed_version${normal}" "${bold}$latest_version${normal}"
fi
fi
@@ -271,7 +285,7 @@ if [ -f "$updater_dir/updater.conf" ] || [ "$UPDATER_FORCE" = "1" ]; then
build
else
# Run installer steps
- install_dependencies
+ install_dependencies "${dependencies[@]}"
prepare_repo
ask_questions
install_script
diff --git a/midimonster.c b/midimonster.c
index 3849953..5817ac7 100644
--- a/midimonster.c
+++ b/midimonster.c
@@ -12,9 +12,9 @@
#define BACKEND_NAME "core"
#define MM_SWAP_LIMIT 20
#include "midimonster.h"
-#include "config.h"
-#include "backend.h"
-#include "plugin.h"
+#include "core/config.h"
+#include "core/backend.h"
+#include "core/plugin.h"
/* Core-internal structures */
typedef struct /*_event_collection*/ {
@@ -346,7 +346,7 @@ static int core_process(size_t nfds, managed_fd* signaled_fds){
size_t u, swaps = 0;
//run backend processing, collect events
- DBGPF("%lu backend FDs signaled\n", nfds);
+ DBGPF("%lu backend FDs signaled", nfds);
if(backends_handle(nfds, signaled_fds)){
return 1;
}
@@ -354,7 +354,7 @@ static int core_process(size_t nfds, managed_fd* signaled_fds){
//limit number of collector swaps per iteration to prevent complete deadlock
while(routing.events->n && swaps < MM_SWAP_LIMIT){
//swap primary and secondary event collectors
- DBGPF("Swapping event collectors, %lu events in primary\n", routing.events->n);
+ DBGPF("Swapping event collectors, %lu events in primary", routing.events->n);
for(u = 0; u < sizeof(routing.pool) / sizeof(routing.pool[0]); u++){
if(routing.events != routing.pool + u){
secondary = routing.events;
diff --git a/midimonster.h b/midimonster.h
index 9552b7e..89688c4 100644
--- a/midimonster.h
+++ b/midimonster.h
@@ -227,15 +227,21 @@ MM_API int mm_backend_register(backend b);
MM_API instance* mm_instance_find(char* backend, uint64_t ident);
/*
- * Provides a pointer to a channel structure, pre-filled with the provided
- * instance reference and identifier.
+ * This function is the main interface to the core-provided channel registry.
+ * This API is just a convenience function. Creating and managing a
+ * backend-internal channel store is possible (and encouraged for performance
+ * reasons).
+ *
+ * Channels are identified by the (instance, ident) tuple within the registry.
+ *
+ * This API provides a pointer to a channel structure, pre-filled with the
+ * provided instance reference and identifier.
* The `create` parameter is a boolean flag indicating whether a channel
* matching the `ident` parameter should be created in the global channel store
* if none exists yet. If the instance already registered a channel matching
* `ident`, a pointer to the existing channel is returned.
- * This API is just a convenience function. Creating and managing a
- * backend-internal channel store is possible (and encouraged for performance
- * reasons). When returning pointers from a backend-local channel store, the
+ *
+ * When returning pointers from a backend-local channel store, the
* returned pointers must stay valid over the lifetime of the instance and
* provide valid `instance` members, as they are used for callbacks.
* For each channel with a non-NULL `impl` field registered using
@@ -245,6 +251,15 @@ MM_API instance* mm_instance_find(char* backend, uint64_t ident);
MM_API channel* mm_channel(instance* i, uint64_t ident, uint8_t create);
/*
+ * When using the core-provided channel registry, the identification
+ * member of the structure must only be updated using this API.
+ * The tuple of (instance, ident) is used as key to the backing
+ * storage of the channel registry, thus the registry must be notified
+ * of changes.
+ */
+MM_API void mm_channel_update(channel* c, uint64_t ident);
+
+/*
* Register (manage = 1) or unregister (manage = 0) a file descriptor to be
* selected on. The backend will be notified when the descriptor becomes ready
* to read via its registered mmbackend_process_fd call. The `impl` argument