diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | DEVELOPMENT.md | 12 | ||||
-rw-r--r-- | Makefile | 18 | ||||
-rw-r--r-- | README.md | 5 | ||||
-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-x | assets/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) | bin | 321510 -> 321510 bytes | |||
-rw-r--r-- | assets/midimonster.rc (renamed from midimonster.rc) | 0 | ||||
-rw-r--r-- | backends/Makefile | 8 | ||||
-rw-r--r-- | backends/evdev.c | 36 | ||||
-rw-r--r-- | backends/evdev.md | 3 | ||||
-rw-r--r-- | backends/maweb.c | 2 | ||||
-rw-r--r-- | backends/mqtt.c | 1005 | ||||
-rw-r--r-- | backends/mqtt.h | 87 | ||||
-rw-r--r-- | backends/mqtt.md | 85 | ||||
-rw-r--r-- | backends/osc.c | 3 | ||||
-rw-r--r-- | backends/osc.md | 2 | ||||
-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-x | installer.sh | 76 | ||||
-rw-r--r-- | midimonster.c | 10 | ||||
-rw-r--r-- | midimonster.h | 25 |
29 files changed, 1358 insertions, 71 deletions
@@ -6,3 +6,4 @@ libmmapi.a *.so *.dll __pycache__ +.vscode/
\ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 79005a9..3fc2268 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -19,6 +19,7 @@ in spirit rather than by the letter. * Commit messages should be in the imperative voice ("When applied, this commit will: "). * The working language for this repository is english. * External dependencies are only acceptable when necessary and available from package repositories. + * Note that external dependencies make OS portability complicated ### Code style @@ -43,8 +44,19 @@ in spirit rather than by the letter. * Avoid `atoi()`/`itoa()`, use `strto[u]l[l]()` and `snprintf()` * Avoid unsafe functions without explicit bounds parameters (eg. `strcat()`). +# Repository layout + +* Keep the root directory as clean as possible + * Files that are not related directly to the MIDIMonster implementation go into the `assets/` directory +* Prefer vendor-neutral names for configuration files where necessary + # Build pipeline +* The primary build pipeline is `make` + # Architecture +* If there is significant potential for sharing functionality between backends, consider implementing it in `libmmbackend` + # Debugging + @@ -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./ +core/%: 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 @@ -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 @@ -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/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 Binary files differindex 9391160..9391160 100644 --- a/midimonster.ico +++ b/assets/midimonster.ico 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/evdev.c b/backends/evdev.c index 4c734f9..8f7c4f9 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -113,12 +113,14 @@ static int evdev_attach(instance* inst, evdev_instance_data* data, char* node){ static char* evdev_find(char* name){ int fd = -1; struct dirent* file = NULL; - char file_path[PATH_MAX * 2]; + char file_path[PATH_MAX * 2], *result = calloc(PATH_MAX * 2, sizeof(char)); DIR* nodes = opendir(INPUT_NODES); - char device_name[UINPUT_MAX_NAME_SIZE], *result = NULL; + char device_name[UINPUT_MAX_NAME_SIZE]; + size_t min_distance = -1, distance = 0; if(!nodes){ LOGPF("Failed to query input device nodes in %s: %s", INPUT_NODES, strerror(errno)); + free(result); return NULL; } @@ -141,20 +143,23 @@ static char* evdev_find(char* name){ close(fd); if(!strncmp(device_name, name, strlen(name))){ - LOGPF("Matched name %s for %s: %s", device_name, name, file_path); - break; + distance = strlen(device_name) - strlen(name); + LOGPF("Matched name %s as candidate (distance %" PRIsize_t ") for %s: %s", device_name, distance, name, file_path); + if(distance < min_distance){ + strncpy(result, file_path, (PATH_MAX * 2) - 1); + min_distance = distance; + } } } } - if(file){ - result = calloc(strlen(file_path) + 1, sizeof(char)); - if(result){ - strncpy(result, file_path, strlen(file_path)); - } - } - closedir(nodes); + + if(!result[0]){ + free(result); + return NULL; + } + LOGPF("Using %s for input name %s", result, name); return result; } @@ -367,7 +372,9 @@ static int evdev_handle(size_t num, managed_fd* fds){ data = (evdev_instance_data*) inst->impl; - for(read_status = libevdev_next_event(data->input_ev, read_flags, &ev); read_status >= 0; read_status = libevdev_next_event(data->input_ev, read_flags, &ev)){ + for(read_status = libevdev_next_event(data->input_ev, read_flags, &ev); + read_status == LIBEVDEV_READ_STATUS_SUCCESS || read_status == LIBEVDEV_READ_STATUS_SYNC; + read_status = libevdev_next_event(data->input_ev, read_flags, &ev)){ read_flags = LIBEVDEV_READ_FLAG_NORMAL; if(read_status == LIBEVDEV_READ_STATUS_SYNC){ read_flags = LIBEVDEV_READ_FLAG_SYNC; @@ -383,6 +390,11 @@ static int evdev_handle(size_t num, managed_fd* fds){ return 1; } } + + if(read_status != -EAGAIN){ + LOGPF("Failed to handle events: %s\n", strerror(-read_status)); + return 1; + } } return 0; diff --git a/backends/evdev.md b/backends/evdev.md index d57201d..e7ba3cc 100644 --- a/backends/evdev.md +++ b/backends/evdev.md @@ -16,7 +16,7 @@ This functionality may require elevated privileges (such as special group member | Option | Example value | Default value | Description | |---------------|-----------------------|---------------|-------------------------------------------------------| | `device` | `/dev/input/event1` | none | `evdev` device to use as input device | -| `input` | `Xbox Wireless` | none | Presentation name of evdev device to use as input (prefix-matched) | +| `input` | `Xbox Wireless` | none | Presentation name of evdev device to use as input (most-specific prefix matched), can be used instead of the `device` option | | `output` | `My Input Device` | none | Output device presentation name. Setting this option enables the instance for output | | `exclusive` | `1` | `0` | Prevent other processes from using the device | | `id` | `0x1 0x2 0x3` | none | Set output device bus identification (Vendor, Product and Version), optional | @@ -49,7 +49,6 @@ If relative axes are used without specifying their extents, the channel will gen of `0`, `0.5` and `1` for any input less than, equal to and greater than `0`, respectively. As for output, only the values `-1`, `0` and `1` are generated for the same interval. - #### Channel specification A channel is specified by its event type and event code, separated by `.`. For a complete list of event types and codes 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/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 |