From 4f467a30f88a628e0e49858986d9278e12a92ce5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 1 May 2020 16:49:49 +0200 Subject: Implement wininput skeleton and mouse output --- backends/Makefile | 2 +- backends/wininput.c | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/wininput.h | 44 +++++++++++ midimonster.h | 2 +- 4 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 backends/wininput.c create mode 100644 backends/wininput.h diff --git a/backends/Makefile b/backends/Makefile index 700c9b3..9b66728 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,6 +1,6 @@ .PHONY: all clean full LINUX_BACKENDS = midi.so evdev.so -WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll wininput.dll BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o diff --git a/backends/wininput.c b/backends/wininput.c new file mode 100644 index 0000000..352be66 --- /dev/null +++ b/backends/wininput.c @@ -0,0 +1,224 @@ +#define BACKEND_NAME "wininput" +#define DEBUG + +#include +#include "wininput.h" + +MM_PLUGIN_API int init(){ + backend wininput = { + .name = BACKEND_NAME, + .conf = wininput_configure, + .create = wininput_instance, + .conf_instance = wininput_configure_instance, + .channel = wininput_channel, + .handle = wininput_set, + .process = wininput_handle, + .start = wininput_start, + .shutdown = wininput_shutdown + }; + + if(sizeof(wininput_channel_ident) != sizeof(uint64_t)){ + LOG("Channel identification union out of bounds"); + return 1; + } + + //register backend + if(mm_backend_register(wininput)){ + LOG("Failed to register backend"); + return 1; + } + return 0; +} + +static int wininput_configure(char* option, char* value){ + LOG("The backend does not take any global configuration"); + return 1; +} + +static int wininput_configure_instance(instance* inst, char* option, char* value){ + LOG("The backend does not take any instance configuration"); + return 0; +} + +static int wininput_instance(instance* inst){ + wininput_instance_data* data = calloc(1, sizeof(wininput_instance_data)); + if(!data){ + LOG("Failed to allocate memory"); + return 1; + } + + inst->impl = data; + return 0; +} + +static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ + char* token = spec; + wininput_channel_ident ident = { + .label = 0 + }; + + if(!strncmp(spec, "mouse.", 6)){ + //TODO wheel + token += 6; + ident.fields.type = mouse; + if(!strcmp(token, "x")){ + ident.fields.channel = position; + } + else if(!strcmp(token, "y")){ + ident.fields.channel = position; + ident.fields.control = 1; + } + else if(!strcmp(token, "lmb")){ + ident.fields.channel = button; + } + else if(!strcmp(token, "rmb")){ + ident.fields.channel = button; + ident.fields.control = 1; + } + else if(!strcmp(token, "mmb")){ + ident.fields.channel = button; + ident.fields.control = 2; + } + else if(!strcmp(token, "xmb1")){ + ident.fields.channel = button; + ident.fields.control = 3; + } + else if(!strcmp(token, "xmb2")){ + ident.fields.channel = button; + ident.fields.control = 4; + } + else{ + LOGPF("Unknown control %s", token); + return NULL; + } + } + else if(!strncmp(spec, "keyboard.", 9)){ + token += 9; + //TODO + } + else{ + LOGPF("Unknown channel spec %s", spec); + } + + if(ident.label){ + return mm_channel(inst, ident.label, 1); + } + return NULL; +} + +static INPUT wininput_event_mouse(wininput_instance_data* data, uint8_t channel, uint8_t control, double value){ + DWORD flags_down = 0, flags_up = 0; + INPUT ev = { + .type = INPUT_MOUSE + }; + + if(channel == position){ + if(control){ + data->mouse.y = value * 0xFFFF; + } + else{ + data->mouse.x = value * 0xFFFF; + } + + ev.mi.dwFlags |= MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE; + ev.mi.dx = data->mouse.x; + ev.mi.dy = data->mouse.y; + } + if(channel == button){ + switch(control){ + case 0: + flags_up |= MOUSEEVENTF_LEFTUP; + flags_down |= MOUSEEVENTF_LEFTDOWN; + break; + case 1: + flags_up |= MOUSEEVENTF_RIGHTUP; + flags_down |= MOUSEEVENTF_RIGHTDOWN; + break; + case 2: + flags_up |= MOUSEEVENTF_MIDDLEUP; + flags_down |= MOUSEEVENTF_MIDDLEDOWN; + break; + case 3: + case 4: + ev.mi.mouseData = (control == 3) ? XBUTTON1 : XBUTTON2; + flags_up |= MOUSEEVENTF_XUP; + flags_down |= MOUSEEVENTF_XDOWN; + break; + } + + if(value > 0.9){ + ev.mi.dwFlags |= flags_down; + } + else{ + ev.mi.dwFlags |= flags_up; + } + } + + return ev; +} + +static INPUT wininput_event_keyboard(wininput_instance_data* data, uint8_t channel, uint8_t control, double value){ + INPUT ev = { + .type = INPUT_KEYBOARD + }; + + return ev; +} + +static int wininput_set(instance* inst, size_t num, channel** c, channel_value* v){ + wininput_channel_ident ident = { + .label = 0 + }; + wininput_instance_data* data = (wininput_instance_data*) inst->impl; + size_t n = 0, offset = 0; + INPUT events[500]; + + //FIXME might want to coalesce mouse events + if(num > sizeof(events) / sizeof(events[0])){ + LOGPF("Truncating output on %s to the last %" PRIsize_t " events, please notify the developers", inst->name, sizeof(events) / sizeof(events[0])); + offset = num - sizeof(events) / sizeof(events[0]); + } + + for(n = 0; n + offset < num; n++){ + ident.label = c[n + offset]->ident; + if(ident.fields.type == mouse){ + events[n] = wininput_event_mouse(data, ident.fields.channel, ident.fields.control, v[n + offset].normalised); + } + else if(ident.fields.type == keyboard){ + events[n] = wininput_event_keyboard(data, ident.fields.channel, ident.fields.control, v[n + offset].normalised); + } + else{ + n--; + offset++; + } + } + + if(n){ + offset = SendInput(n, events, sizeof(INPUT)); + if(offset != n){ + LOGPF("Output %" PRIsize_t " of %" PRIsize_t " events on %s", offset, n, inst->name); + } + } + return 0; +} + +static int wininput_handle(size_t num, managed_fd* fds){ + //TODO + return 0; +} + +static int wininput_start(size_t n, instance** inst){ + //TODO + return 0; +} + +static int wininput_shutdown(size_t n, instance** inst){ + size_t u; + + for(u = 0; u < n; u++){ + free(inst[u]->impl); + } + + LOG("Backend shut down"); + return 0; +} diff --git a/backends/wininput.h b/backends/wininput.h new file mode 100644 index 0000000..240f1a6 --- /dev/null +++ b/backends/wininput.h @@ -0,0 +1,44 @@ +#include "midimonster.h" + +MM_PLUGIN_API int init(); +static int wininput_configure(char* option, char* value); +static int wininput_configure_instance(instance* inst, char* option, char* value); +static int wininput_instance(instance* inst); +static channel* wininput_channel(instance* inst, char* spec, uint8_t flags); +static int wininput_set(instance* inst, size_t num, channel** c, channel_value* v); +static int wininput_handle(size_t num, managed_fd* fds); +static int wininput_start(size_t n, instance** inst); +static int wininput_shutdown(size_t n, instance** inst); + +enum /*wininput_channel_type*/ { + none = 0, + mouse, + keyboard, + joystick +}; + +enum /*wininput_control_channel*/ { + position, + //wheel, /*relative*/ + button, + + keypress, + key_unicode +}; + +typedef union { + struct { + uint8_t pad[4]; + uint8_t type; + uint8_t channel; + uint16_t control; + } fields; + uint64_t label; +} wininput_channel_ident; + +typedef struct { + struct { + uint16_t x; + uint16_t y; + } mouse; +} wininput_instance_data; diff --git a/midimonster.h b/midimonster.h index 75eb30a..a5de60e 100644 --- a/midimonster.h +++ b/midimonster.h @@ -7,7 +7,7 @@ /* Core version unless set by the build process */ #ifndef MIDIMONSTER_VERSION - #define MIDIMONSTER_VERSION "v0.5-dist" + #define MIDIMONSTER_VERSION "v0.6-dist" #endif /* Set backend name if unset */ -- cgit v1.2.3 From 56507d7ac27d0562689ea7c505fa026ecc38494f Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 2 May 2020 16:55:13 +0200 Subject: Implement keyboard output for wininput --- backends/wininput.c | 148 ++++++++++++++++++++++++++++++++++++++++----------- backends/wininput.h | 10 +++- backends/wininput.md | 114 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 33 deletions(-) create mode 100644 backends/wininput.md diff --git a/backends/wininput.c b/backends/wininput.c index 352be66..46151d4 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -4,6 +4,55 @@ #include #include "wininput.h" +static key_info keys[] = { + {VK_LBUTTON, "lmb", button}, {VK_RBUTTON, "rmb", button}, {VK_MBUTTON, "mmb", button}, + {VK_XBUTTON1, "xmb1", button}, {VK_XBUTTON2, "xmb2", button}, + {VK_BACK, "backspace"}, + {VK_TAB, "tab"}, + {VK_CLEAR, "clear"}, + {VK_RETURN, "enter"}, + {VK_SHIFT, "shift"}, + {VK_CONTROL, "control"}, {VK_MENU, "alt"}, + {VK_CAPITAL, "capslock"}, + {VK_ESCAPE, "escape"}, + {VK_SPACE, "space"}, + {VK_PRIOR, "pageup"}, {VK_NEXT, "pagedown"}, + {VK_END, "end"}, {VK_HOME, "home"}, + {VK_PAUSE, "pause"}, {VK_NUMLOCK, "numlock"}, {VK_SCROLL, "scrolllock"}, + {VK_INSERT, "insert"}, {VK_DELETE, "delete"}, {VK_SNAPSHOT, "printscreen"}, + {VK_LEFT, "left"}, {VK_UP, "up"}, {VK_RIGHT, "right"}, {VK_DOWN, "down"}, + {VK_SELECT, "select"}, + {VK_PRINT, "print"}, + {VK_EXECUTE, "execute"}, + {VK_HELP, "help"}, + {VK_APPS, "apps"}, + {VK_SLEEP, "sleep"}, + {VK_NUMPAD0, "num0"}, {VK_NUMPAD1, "num1"}, {VK_NUMPAD2, "num2"}, {VK_NUMPAD3, "num3"}, + {VK_NUMPAD4, "num4"}, {VK_NUMPAD5, "num5"}, {VK_NUMPAD6, "num6"}, {VK_NUMPAD7, "num7"}, + {VK_NUMPAD8, "num8"}, {VK_NUMPAD9, "num9"}, {VK_MULTIPLY, "multiply"}, {VK_ADD, "plus"}, + {VK_SEPARATOR, "comma"}, {VK_SUBTRACT, "minus"}, {VK_DECIMAL, "dot"}, {VK_DIVIDE, "divide"}, + {VK_F1, "f1"}, {VK_F2, "f2"}, {VK_F3, "f3"}, {VK_F4, "f4"}, {VK_F5, "f5"}, + {VK_F6, "f6"}, {VK_F7, "f7"}, {VK_F8, "f8"}, {VK_F9, "f9"}, {VK_F10, "f10"}, + {VK_F11, "f11"}, {VK_F12, "f12"}, {VK_F13, "f13"}, {VK_F14, "f14"}, {VK_F15, "f15"}, + {VK_F16, "f16"}, {VK_F17, "f17"}, {VK_F18, "f18"}, {VK_F19, "f19"}, {VK_F20, "f20"}, + {VK_F21, "f21"}, {VK_F22, "f22"}, {VK_F23, "f23"}, {VK_F24, "f24"}, + {VK_LWIN, "lwin"}, {VK_RWIN, "rwin"}, + {VK_LSHIFT, "lshift"}, {VK_RSHIFT, "rshift"}, + {VK_LCONTROL, "lctrl"}, {VK_RCONTROL, "rctrl"}, + {VK_LMENU, "lmenu"}, {VK_RMENU, "rmenu"}, + {VK_BROWSER_BACK, "previous"}, {VK_BROWSER_FORWARD, "next"}, {VK_BROWSER_REFRESH, "refresh"}, + {VK_BROWSER_STOP, "stop"}, {VK_BROWSER_SEARCH, "search"}, {VK_BROWSER_FAVORITES, "favorites"}, + {VK_BROWSER_HOME, "homepage"}, + {VK_VOLUME_MUTE, "mute"}, {VK_VOLUME_DOWN, "voldown"}, {VK_VOLUME_UP, "volup"}, + {VK_MEDIA_NEXT_TRACK, "nexttrack"}, {VK_MEDIA_PREV_TRACK, "prevtrack"}, + {VK_MEDIA_STOP, "stopmedia"}, {VK_MEDIA_PLAY_PAUSE, "togglemedia"}, + {VK_LAUNCH_MEDIA_SELECT, "mediaselect"}, + {VK_LAUNCH_MAIL, "mail"}, {VK_LAUNCH_APP1, "app1"}, {VK_LAUNCH_APP2, "app2"}, + {VK_OEM_PLUS, "plus"}, {VK_OEM_COMMA, "comma"}, + {VK_OEM_MINUS, "minus"}, {VK_OEM_PERIOD, "period"}, + {VK_ZOOM, "zoom"} +}; + MM_PLUGIN_API int init(){ backend wininput = { .name = BACKEND_NAME, @@ -52,6 +101,8 @@ static int wininput_instance(instance* inst){ } static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ + size_t u; + uint16_t scancode = 0; char* token = spec; wininput_channel_ident ident = { .label = 0 @@ -68,33 +119,62 @@ static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ ident.fields.channel = position; ident.fields.control = 1; } - else if(!strcmp(token, "lmb")){ - ident.fields.channel = button; - } - else if(!strcmp(token, "rmb")){ - ident.fields.channel = button; - ident.fields.control = 1; - } - else if(!strcmp(token, "mmb")){ - ident.fields.channel = button; - ident.fields.control = 2; - } - else if(!strcmp(token, "xmb1")){ - ident.fields.channel = button; - ident.fields.control = 3; - } - else if(!strcmp(token, "xmb2")){ - ident.fields.channel = button; - ident.fields.control = 4; - } else{ - LOGPF("Unknown control %s", token); - return NULL; + //check the buttons + for(u = 0; u < sizeof(keys) / sizeof(keys[0]); u++){ + if(keys[u].channel == button && !strcmp(keys[u].name, token)){ + DBGPF("Using keymap %" PRIsize_t " (%d) for spec %s", u, keys[u].keycode, token); + ident.fields.channel = button; + ident.fields.control = keys[u].keycode; + break; + } + } } } - else if(!strncmp(spec, "keyboard.", 9)){ - token += 9; - //TODO + else if(!strncmp(spec, "key.", 4)){ + token += 4; + ident.fields.type = keyboard; + ident.fields.channel = keypress; + + for(u = 0; u < sizeof(keys) / sizeof(keys[0]); u++){ + if(keys[u].channel == keypress && !strcmp(keys[u].name, token)){ + DBGPF("Using keymap %" PRIsize_t " (%d) for spec %s", u, keys[u].keycode, token); + ident.fields.control = keys[u].keycode; + break; + } + } + + //no entry in translation table + if(u == sizeof(keys) / sizeof(keys[0])){ + if(strlen(token) == 1){ + //try to translate + scancode = VkKeyScan(token[0]); + if(scancode != 0x7f7f){ + DBGPF("Using keyscan result %02X (via %04X) for spec %s", scancode & 0xFF, scancode, token); + ident.fields.type = keyboard; + ident.fields.channel = keypress; + ident.fields.control = scancode & 0xFF; + } + else{ + LOGPF("Invalid channel specification %s", token); + return NULL; + } + } + else if(strlen(token) > 1){ + //try to use as literal + scancode = strtoul(token, NULL, 0); + if(!scancode){ + LOGPF("Invalid channel specification %s", token); + return NULL; + } + DBGPF("Using direct conversion %d for spec %s", scancode & 0xFF, token); + ident.fields.control = scancode & 0xFF; + } + else{ + LOGPF("Invalid channel specification %s", spec); + return NULL; + } + } } else{ LOGPF("Unknown channel spec %s", spec); @@ -126,21 +206,21 @@ static INPUT wininput_event_mouse(wininput_instance_data* data, uint8_t channel, } if(channel == button){ switch(control){ - case 0: + case VK_LBUTTON: flags_up |= MOUSEEVENTF_LEFTUP; flags_down |= MOUSEEVENTF_LEFTDOWN; break; - case 1: + case VK_RBUTTON: flags_up |= MOUSEEVENTF_RIGHTUP; flags_down |= MOUSEEVENTF_RIGHTDOWN; break; - case 2: + case VK_MBUTTON: flags_up |= MOUSEEVENTF_MIDDLEUP; flags_down |= MOUSEEVENTF_MIDDLEDOWN; break; - case 3: - case 4: - ev.mi.mouseData = (control == 3) ? XBUTTON1 : XBUTTON2; + case VK_XBUTTON1: + case VK_XBUTTON2: + ev.mi.mouseData = (control == VK_XBUTTON1) ? XBUTTON1 : XBUTTON2; flags_up |= MOUSEEVENTF_XUP; flags_down |= MOUSEEVENTF_XDOWN; break; @@ -162,6 +242,13 @@ static INPUT wininput_event_keyboard(wininput_instance_data* data, uint8_t chann .type = INPUT_KEYBOARD }; + if(channel == keypress){ + ev.ki.wVk = control; + if(value < 0.9){ + ev.ki.dwFlags |= KEYEVENTF_KEYUP; + } + } + return ev; } @@ -173,7 +260,6 @@ static int wininput_set(instance* inst, size_t num, channel** c, channel_value* size_t n = 0, offset = 0; INPUT events[500]; - //FIXME might want to coalesce mouse events if(num > sizeof(events) / sizeof(events[0])){ LOGPF("Truncating output on %s to the last %" PRIsize_t " events, please notify the developers", inst->name, sizeof(events) / sizeof(events[0])); offset = num - sizeof(events) / sizeof(events[0]); diff --git a/backends/wininput.h b/backends/wininput.h index 240f1a6..ef817e8 100644 --- a/backends/wininput.h +++ b/backends/wininput.h @@ -18,14 +18,20 @@ enum /*wininput_channel_type*/ { }; enum /*wininput_control_channel*/ { + keypress = 0, + button, position, //wheel, /*relative*/ - button, - keypress, key_unicode }; +typedef struct /*_wininput_key_info*/ { + uint8_t keycode; + char* name; + uint8_t channel; +} key_info; + typedef union { struct { uint8_t pad[4]; diff --git a/backends/wininput.md b/backends/wininput.md new file mode 100644 index 0000000..672a24e --- /dev/null +++ b/backends/wininput.md @@ -0,0 +1,114 @@ +### The `wininput` backend + +This backend allows using the mouse and keyboard as input and output channels on a Windows system. +For example, it can be used to create hotkey-like behaviour (by reading keyboard input) or to control +a computer remotely. + +As Windows merges all keyboard and mouse input into a single data stream, no fine-grained per-device +access (as is available under Linux) is possible. + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +This backend does not take any instance-specific configuration. + +#### Channel specification + +The mouse is exposed as two channels for the position (with the origin being the upper-left corner of the desktop) + +* `mouse.x` +* `mouse.y` + +as well as one channel per mouse button + +* `mouse.lmb` (Left mouse button) +* `mouse.rmb` (Right mouse button) +* `mouse.mmb` (Middle mouse button) +* `mouse.xmb1` (Extra mouse button 1) +* `mouse.xmb2` (Extra mouse button 2) + +All keys that have an [assigned virtual keycode](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes) +are mappable as MIDIMonster channels using the syntax `key.`, with *keyname* being one of the following specifiers: + +* One of the keynames listed below (e.g., `key.enter`) +* For "simple" keys (A-z, 0-9, etc), simply the key glyph (e.g. `key.a`) +* A hexadecimal number specifying the virtual keycode + +Keys are pressed once the normalized event value is greater than `0.9`, and released if under that. + +The following keynames are defined in an internal mapping table: + +| Key name | Description | +|-------------------------------|-----------------------| +| `backspace` | | +| `tab` | | +| `clear` | | +| `enter` | | +| `shift` | | +| `control` | | +| `alt` | | +| `capslock` | | +| `escape` | | +| `space`, | | +| `pageup`, `pagedown` | | +| `end` | | +| `home` | | +| `pause` | | +| `numlock` | | +| `scrolllock` | | +| `insert` | | +| `delete` | | +| `printscreen` | | +| `up`, `down`, `left`, `right` | | +| `select` | | +| `print` | | +| `execute` | | +| `help` | | +| `apps` | | +| `sleep` | | +| `num0` - `num9` | | +| `multiply` | | +| `plus` | | +| `comma` | | +| `minus` | | +| `dot` | | +| `divide` | | +| `f1` - `f24` | | +| `lwin`, `rwin` | | +| `lshift`, `rshift` | | +| `lctrl, `rctrl` | | +| `lmenu`, `rmenu` | | +| `previous`, `next` | Browser controls | +| `refresh` | Browser controls | +| `stop` | Browser controls | +| `search` | Browser controls | +| `favorites` | Browser controls | +| `homepage` | Browser controls | +| `mute` | | +| `voldown`, `volup` | | +| `nexttrack`, `prevtrack` | | +| `stopmedia`, `togglemedia` | | +| `mediaselect` | | +| `mail` | | +| `app1`, `app2` | | +| `zoom` | | + +Example mappings: +``` +generator.x > wi1.mouse.x +input.a > wi1.key.a +input.X > wi1.key.escape +``` + +#### Known bugs / problems + +Keyboard and mouse input is subject to UIPI. You can not send input to applications that run at a higher +privilege level than the MIDIMonster. + +Some antivirus applications may detect this backend as problematic file, because it uses the same system +interfaces to read keyboard and mouse input as any malicious application would. While it is definitely +possible to configure the MIDIMonster to do malicious things, the code itself does not log anything. +You can verify this by reading the backend code yourself. -- cgit v1.2.3 From f9886f3b06ce8e32aea893208646053f93a00a6c Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 2 May 2020 17:20:13 +0200 Subject: Reformat wininput table --- backends/wininput.md | 94 +++++++++++++++++++--------------------------------- 1 file changed, 34 insertions(+), 60 deletions(-) diff --git a/backends/wininput.md b/backends/wininput.md index 672a24e..6455dd1 100644 --- a/backends/wininput.md +++ b/backends/wininput.md @@ -24,11 +24,11 @@ The mouse is exposed as two channels for the position (with the origin being the as well as one channel per mouse button -* `mouse.lmb` (Left mouse button) -* `mouse.rmb` (Right mouse button) -* `mouse.mmb` (Middle mouse button) -* `mouse.xmb1` (Extra mouse button 1) -* `mouse.xmb2` (Extra mouse button 2) +* `mouse.lmb`: Left mouse button +* `mouse.rmb`: Right mouse button +* `mouse.mmb`: Middle mouse button +* `mouse.xmb1`: Extra mouse button 1 +* `mouse.xmb2`: Extra mouse button 2 All keys that have an [assigned virtual keycode](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes) are mappable as MIDIMonster channels using the syntax `key.`, with *keyname* being one of the following specifiers: @@ -41,60 +41,34 @@ Keys are pressed once the normalized event value is greater than `0.9`, and rele The following keynames are defined in an internal mapping table: -| Key name | Description | -|-------------------------------|-----------------------| -| `backspace` | | -| `tab` | | -| `clear` | | -| `enter` | | -| `shift` | | -| `control` | | -| `alt` | | -| `capslock` | | -| `escape` | | -| `space`, | | -| `pageup`, `pagedown` | | -| `end` | | -| `home` | | -| `pause` | | -| `numlock` | | -| `scrolllock` | | -| `insert` | | -| `delete` | | -| `printscreen` | | -| `up`, `down`, `left`, `right` | | -| `select` | | -| `print` | | -| `execute` | | -| `help` | | -| `apps` | | -| `sleep` | | -| `num0` - `num9` | | -| `multiply` | | -| `plus` | | -| `comma` | | -| `minus` | | -| `dot` | | -| `divide` | | -| `f1` - `f24` | | -| `lwin`, `rwin` | | -| `lshift`, `rshift` | | -| `lctrl, `rctrl` | | -| `lmenu`, `rmenu` | | -| `previous`, `next` | Browser controls | -| `refresh` | Browser controls | -| `stop` | Browser controls | -| `search` | Browser controls | -| `favorites` | Browser controls | -| `homepage` | Browser controls | -| `mute` | | -| `voldown`, `volup` | | -| `nexttrack`, `prevtrack` | | -| `stopmedia`, `togglemedia` | | -| `mediaselect` | | -| `mail` | | -| `app1`, `app2` | | -| `zoom` | | +| Key name | Description | Key name | Description | +|-------------------------------|-----------------------|-------------------------------|-----------------------| +| `backspace` | | `tab` | | +| `clear` | | `enter` | | +| `shift` | | `control` | | +| `alt` | | `capslock` | | +| `escape` | | `space` | | +| `pageup`, `pagedown` | | `end` | | +| `home` | | `pause` | | +| `numlock` | | `scrolllock` | | +| `insert` | | `delete` | | +| `printscreen` | | `up`, `down`, `left`, `right` | | +| `select` | | `print` | | +| `execute` | | `help` | | +| `apps` | | `sleep` | | +| `num0` - `num9` | | `multiply` | | +| `plus` | | `comma` | | +| `minus` | | `dot` | | +| `divide` | | `f1` - `f24` | | +| `lwin`, `rwin` | | `lshift`, `rshift` | | +| `lctrl, `rctrl` | | `lmenu`, `rmenu` | | +| `previous`, `next` | Browser controls | `refresh` | Browser controls | +| `stop` | Browser controls | `search` | Browser controls | +| `favorites` | Browser controls | `homepage` | Browser controls | +| `mute` | | `voldown`, `volup` | | +| `nexttrack`, `prevtrack` | | `stopmedia`, `togglemedia` | | +| `mediaselect` | | `mail` | | +| `app1`, `app2` | | `zoom` | | Example mappings: ``` @@ -108,7 +82,7 @@ input.X > wi1.key.escape Keyboard and mouse input is subject to UIPI. You can not send input to applications that run at a higher privilege level than the MIDIMonster. -Some antivirus applications may detect this backend as problematic file, because it uses the same system +Some antivirus applications may detect this backend as problematic because it uses the same system interfaces to read keyboard and mouse input as any malicious application would. While it is definitely possible to configure the MIDIMonster to do malicious things, the code itself does not log anything. You can verify this by reading the backend code yourself. -- cgit v1.2.3 From c2248f6bee6b3eef770b557d9be7659dae586222 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 3 May 2020 19:23:45 +0200 Subject: Strip Windows binaries, update semantics for _interval callback --- .travis-ci.sh | 2 ++ backend.c | 4 ++-- midimonster.h | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis-ci.sh b/.travis-ci.sh index 8f6a5ca..8d5d42d 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -78,6 +78,8 @@ elif [ "$TASK" = "windows" ]; then mkdir ./deployment mkdir ./deployment/backends mkdir ./deployment/docs + # Strip the Windows binaries as they become huge quickly + strip midimonster.exe backends/*.dll cp ./midimonster.exe ./deployment/ cp ./backends/*.dll ./deployment/backends/ cp ./backends/*.dll.disabled ./deployment/backends/ diff --git a/backend.c b/backend.c index 003980f..0fb6679 100644 --- a/backend.c +++ b/backend.c @@ -234,12 +234,12 @@ struct timeval backend_timeout(){ //only call interval if backend has instances if(registry.instances[u] && registry.backends[u].interval){ res = registry.backends[u].interval(); - if((res / 1000) < secs){ + if(res && (res / 1000) < secs){ DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); secs = res / 1000; msecs = res % 1000; } - else if(res / 1000 == secs && (res % 1000) < msecs){ + else if(res && res / 1000 == secs && (res % 1000) < msecs){ DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); msecs = res % 1000; } diff --git a/midimonster.h b/midimonster.h index a5de60e..9552b7e 100644 --- a/midimonster.h +++ b/midimonster.h @@ -129,6 +129,8 @@ struct _managed_fd; * * (optional) mmbackend_interval * Return the maximum sleep interval for this backend in milliseconds. * If not implemented, a maximum interval of one second is used. + * Returning 0 signals that the backend does not have a minimum + * interval. * * mmbackend_shutdown * Clean up all allocations, finalize all hardware connections. All registered * backends receive the shutdown call, regardless of whether they have been -- cgit v1.2.3 From eabdc18aa2209f3526d1068510991ce7f17a4fe6 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 4 May 2020 20:25:00 +0200 Subject: Implement wininput input direction --- backends/wininput.c | 223 ++++++++++++++++++++++++++++++++++++++++++++++----- backends/wininput.h | 13 +-- backends/wininput.md | 6 +- 3 files changed, 215 insertions(+), 27 deletions(-) diff --git a/backends/wininput.c b/backends/wininput.c index 46151d4..0c7e490 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -4,6 +4,9 @@ #include #include "wininput.h" +//TODO check whether feedback elimination is required +//TODO refactor & simplify + static key_info keys[] = { {VK_LBUTTON, "lmb", button}, {VK_RBUTTON, "rmb", button}, {VK_MBUTTON, "mmb", button}, {VK_XBUTTON1, "xmb1", button}, {VK_XBUTTON2, "xmb2", button}, @@ -53,11 +56,23 @@ static key_info keys[] = { {VK_ZOOM, "zoom"} }; +static struct { + int virtual_x, virtual_y, virtual_width, virtual_height; + uint16_t mouse_x, mouse_y; + size_t requests; + wininput_request* request; + uint32_t interval; +} cfg = { + .requests = 0, + .interval = 50 +}; + MM_PLUGIN_API int init(){ backend wininput = { .name = BACKEND_NAME, .conf = wininput_configure, .create = wininput_instance, + .interval = wininput_interval, .conf_instance = wininput_configure_instance, .channel = wininput_channel, .handle = wininput_set, @@ -79,8 +94,17 @@ MM_PLUGIN_API int init(){ return 0; } +static uint32_t wininput_interval(){ + return cfg.interval; +} + static int wininput_configure(char* option, char* value){ - LOG("The backend does not take any global configuration"); + if(!strcmp(option, "interval")){ + cfg.interval = strtoul(value, NULL, 0); + return 0; + } + + LOGPF("Unknown backend configuration option %s", option); return 1; } @@ -90,18 +114,57 @@ static int wininput_configure_instance(instance* inst, char* option, char* value } static int wininput_instance(instance* inst){ - wininput_instance_data* data = calloc(1, sizeof(wininput_instance_data)); - if(!data){ + return 0; +} + +static int wininput_subscribe(wininput_channel_ident ident, channel* chan){ + size_t u, n; + + //find an existing request + for(u = 0; u < cfg.requests; u++){ + if(cfg.request[u].ident.label == ident.label){ + break; + } + } + + if(u == cfg.requests){ + //create a new request + cfg.request = realloc(cfg.request, (cfg.requests + 1) * sizeof(wininput_request)); + if(!cfg.request){ + cfg.requests = 0; + LOG("Failed to allocate memory"); + return 1; + } + + cfg.request[u].ident.label = ident.label; + cfg.request[u].channels = 0; + cfg.request[u].channel = NULL; + cfg.request[u].state = 0; + cfg.requests++; + } + + //check if already in subscriber list + for(n = 0; n < cfg.request[u].channels; n++){ + if(cfg.request[u].channel[n] == chan){ + return 0; + } + } + + //add to subscriber list + cfg.request[u].channel = realloc(cfg.request[u].channel, (cfg.request[u].channels + 1) * sizeof(channel*)); + if(!cfg.request[u].channel){ + cfg.request[u].channels = 0; LOG("Failed to allocate memory"); return 1; } - - inst->impl = data; + cfg.request[u].channel[n] = chan; + cfg.request[u].channels++; return 0; } static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ size_t u; + channel* chan = NULL; uint16_t scancode = 0; char* token = spec; wininput_channel_ident ident = { @@ -129,6 +192,11 @@ static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ break; } } + + if(u == sizeof(keys) / sizeof(keys[0])){ + LOGPF("Unknown mouse control %s", token); + return NULL; + } } } else if(!strncmp(spec, "key.", 4)){ @@ -181,12 +249,26 @@ static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ } if(ident.label){ - return mm_channel(inst, ident.label, 1); + chan = mm_channel(inst, ident.label, 1); + if(chan && (flags & mmchannel_input) && wininput_subscribe(ident, chan)){ + return NULL; + } + return chan; } return NULL; } -static INPUT wininput_event_mouse(wininput_instance_data* data, uint8_t channel, uint8_t control, double value){ +//for some reason, sendinput only takes "normalized absolute coordinates", which are never again used in the API +static void wininput_mouse_normalize(long* x, long* y){ + //TODO this needs to take a possible origin offset into account + long normalized_x = (double) (*x) * (65535.0f / (double) cfg.virtual_width); + long normalized_y = (double) (*y) * (65535.0f / (double) cfg.virtual_height); + + *x = normalized_x; + *y = normalized_y; +} + +static INPUT wininput_event_mouse(uint8_t channel, uint8_t control, double value){ DWORD flags_down = 0, flags_up = 0; INPUT ev = { .type = INPUT_MOUSE @@ -194,15 +276,15 @@ static INPUT wininput_event_mouse(wininput_instance_data* data, uint8_t channel, if(channel == position){ if(control){ - data->mouse.y = value * 0xFFFF; + cfg.mouse_y = value * 0xFFFF; } else{ - data->mouse.x = value * 0xFFFF; + cfg.mouse_x = value * 0xFFFF; } - ev.mi.dwFlags |= MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE; - ev.mi.dx = data->mouse.x; - ev.mi.dy = data->mouse.y; + ev.mi.dwFlags |= MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK; + ev.mi.dx = cfg.mouse_x; + ev.mi.dy = cfg.mouse_y; } if(channel == button){ switch(control){ @@ -237,7 +319,7 @@ static INPUT wininput_event_mouse(wininput_instance_data* data, uint8_t channel, return ev; } -static INPUT wininput_event_keyboard(wininput_instance_data* data, uint8_t channel, uint8_t control, double value){ +static INPUT wininput_event_keyboard(uint8_t channel, uint8_t control, double value){ INPUT ev = { .type = INPUT_KEYBOARD }; @@ -256,7 +338,6 @@ static int wininput_set(instance* inst, size_t num, channel** c, channel_value* wininput_channel_ident ident = { .label = 0 }; - wininput_instance_data* data = (wininput_instance_data*) inst->impl; size_t n = 0, offset = 0; INPUT events[500]; @@ -268,10 +349,10 @@ static int wininput_set(instance* inst, size_t num, channel** c, channel_value* for(n = 0; n + offset < num; n++){ ident.label = c[n + offset]->ident; if(ident.fields.type == mouse){ - events[n] = wininput_event_mouse(data, ident.fields.channel, ident.fields.control, v[n + offset].normalised); + events[n] = wininput_event_mouse(ident.fields.channel, ident.fields.control, v[n + offset].normalised); } else if(ident.fields.type == keyboard){ - events[n] = wininput_event_keyboard(data, ident.fields.channel, ident.fields.control, v[n + offset].normalised); + events[n] = wininput_event_keyboard(ident.fields.channel, ident.fields.control, v[n + offset].normalised); } else{ n--; @@ -289,21 +370,123 @@ static int wininput_set(instance* inst, size_t num, channel** c, channel_value* } static int wininput_handle(size_t num, managed_fd* fds){ - //TODO + channel_value val = { + .normalised = 0 + }; + uint8_t mouse_updated = 0, synthesize_off = 0, push_event = 0; + uint16_t key_state = 0; + POINT cursor_position; + size_t u = 0, n; + + for(u = 0; u < cfg.requests; u++){ + synthesize_off = 0; + push_event = 0; + val.normalised = 0; + + if(cfg.request[u].ident.fields.type == mouse + && cfg.request[u].ident.fields.channel == position){ + if(!mouse_updated){ + //update mouse coordinates + if(!GetCursorPos(&cursor_position)){ + LOG("Failed to update mouse position"); + continue; + } + wininput_mouse_normalize(&cursor_position.x, &cursor_position.y); + mouse_updated = 1; + if(cfg.mouse_x != cursor_position.x + || cfg.mouse_y != cursor_position.y){ + cfg.mouse_x = cursor_position.x; + cfg.mouse_y = cursor_position.y; + mouse_updated = 2; + } + } + + val.normalised = (double) cfg.mouse_x / (double) 0xFFFF; + if(cfg.request[u].ident.fields.control){ + val.normalised = (double) cfg.mouse_y / (double) 0xFFFF; + } + + if(mouse_updated == 2){ + push_event = 1; + } + } + else{ + //check key state + key_state = GetAsyncKeyState(cfg.request[u].ident.fields.control); + if(key_state == 1){ + //pressed and released? + synthesize_off = 1; + } + if((key_state & ~1) != cfg.request[u].state){ + //key state changed + if(key_state){ + val.normalised = 1.0; + } + cfg.request[u].state = key_state & ~1; + push_event = 1; + } + } + + if(push_event){ + //push current value to all channels + DBGPF("Pushing event %f on request %" PRIsize_t, val.normalised, u); + for(n = 0; n < cfg.request[u].channels; n++){ + mm_channel_event(cfg.request[u].channel[n], val); + } + + if(synthesize_off){ + val.normalised = 0; + //push synthesized value to all channels + DBGPF("Synthesizing event %f on request %" PRIsize_t, val.normalised, u); + for(n = 0; n < cfg.request[u].channels; n++){ + mm_channel_event(cfg.request[u].channel[n], val); + } + } + } + } return 0; } static int wininput_start(size_t n, instance** inst){ - //TODO + POINT cursor_position; + + //if no input requested, don't request polling + if(!cfg.requests){ + cfg.interval = 0; + } + + //read virtual desktop extents for later normalization + cfg.virtual_width = GetSystemMetrics(SM_CXVIRTUALSCREEN); + cfg.virtual_height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + cfg.virtual_x = GetSystemMetrics(SM_XVIRTUALSCREEN); + cfg.virtual_y = GetSystemMetrics(SM_YVIRTUALSCREEN); + DBGPF("Virtual screen is %dx%d with offset %dx%d", cfg.virtual_width, cfg.virtual_height, cfg.virtual_x, cfg.virtual_y); + + //initialize mouse position + if(!GetCursorPos(&cursor_position)){ + LOG("Failed to read initial mouse position"); + return 1; + } + + DBGPF("Current mouse coordinates: %dx%d (%04Xx%04X)", cursor_position.x, cursor_position.y, cursor_position.x, cursor_position.y); + wininput_mouse_normalize(&cursor_position.x, &cursor_position.y); + DBGPF("Current normalized mouse position: %04Xx%04X", cursor_position.x, cursor_position.y); + cfg.mouse_x = cursor_position.x; + cfg.mouse_y = cursor_position.y; + + DBGPF("Tracking %" PRIsize_t " input requests", cfg.requests); return 0; } static int wininput_shutdown(size_t n, instance** inst){ size_t u; - for(u = 0; u < n; u++){ - free(inst[u]->impl); + for(u = 0; u < cfg.requests; u++){ + free(cfg.request[u].channel); } + free(cfg.request); + cfg.request = NULL; + cfg.requests = 0; LOG("Backend shut down"); return 0; diff --git a/backends/wininput.h b/backends/wininput.h index ef817e8..0318724 100644 --- a/backends/wininput.h +++ b/backends/wininput.h @@ -5,6 +5,7 @@ static int wininput_configure(char* option, char* value); static int wininput_configure_instance(instance* inst, char* option, char* value); static int wininput_instance(instance* inst); static channel* wininput_channel(instance* inst, char* spec, uint8_t flags); +static uint32_t wininput_interval(); static int wininput_set(instance* inst, size_t num, channel** c, channel_value* v); static int wininput_handle(size_t num, managed_fd* fds); static int wininput_start(size_t n, instance** inst); @@ -42,9 +43,9 @@ typedef union { uint64_t label; } wininput_channel_ident; -typedef struct { - struct { - uint16_t x; - uint16_t y; - } mouse; -} wininput_instance_data; +typedef struct /*_input_request*/ { + wininput_channel_ident ident; + size_t channels; + channel** channel; + uint16_t state; +} wininput_request; diff --git a/backends/wininput.md b/backends/wininput.md index 6455dd1..6ead158 100644 --- a/backends/wininput.md +++ b/backends/wininput.md @@ -61,7 +61,7 @@ The following keynames are defined in an internal mapping table: | `minus` | | `dot` | | | `divide` | | `f1` - `f24` | | | `lwin`, `rwin` | | `lshift`, `rshift` | | -| `lctrl, `rctrl` | | `lmenu`, `rmenu` | | +| `lctrl`, `rctrl` | | `lmenu`, `rmenu` | | | `previous`, `next` | Browser controls | `refresh` | Browser controls | | `stop` | Browser controls | `search` | Browser controls | | `favorites` | Browser controls | `homepage` | Browser controls | @@ -82,6 +82,10 @@ input.X > wi1.key.escape Keyboard and mouse input is subject to UIPI. You can not send input to applications that run at a higher privilege level than the MIDIMonster. +Due to inconsistencies in the Windows API, mouse position input and output may differ for the same cursor location. +This may be correlated with the use and arrangement of multi-monitor desktops. If you encounter problems with either +receiving or sending mouse positions, please include a description of your monitor alignment in the issue. + Some antivirus applications may detect this backend as problematic because it uses the same system interfaces to read keyboard and mouse input as any malicious application would. While it is definitely possible to configure the MIDIMonster to do malicious things, the code itself does not log anything. -- cgit v1.2.3 From faf34959ee4a86441bc3e96229fbd5365869d523 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 4 May 2020 20:31:57 +0200 Subject: Mention wininput in main README (Fixes #57) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2eda183..161fcaa 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Currently, the MIDIMonster supports the following protocols: | OpenSoundControl (OSC) | Linux, Windows, OSX | | [`osc`](backends/osc.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) | -| evdev input devices | Linux | Virtual output supported | [`evdev`](backends/evdev.md) | +| Input devices (Mouse, Keyboard, etc)| Linux, Windows | | [`evdev`](backends/evdev.md), [`wininput`](backends/wininput.md) | | Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | | MA Lighting Web Remote | Linux, Windows, OSX | GrandMA2 and dot2 (incl. OnPC) | [`maweb`](backends/maweb.md) | | JACK/LV2 Control Voltage (CV) | Linux, OSX | | [`jack`](backends/jack.md) | @@ -156,6 +156,7 @@ special information. These documentation files are located in the `backends/` di * [`lua` backend documentation](backends/lua.md) * [`python` backend documentation](backends/python.md) * [`maweb` backend documentation](backends/maweb.md) +* [`wininput` backend documentation](backends/wininput.md) ## Installation -- cgit v1.2.3 From 63f226019991479ee03b230ba567c8ccc69bb9db Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 9 May 2020 20:55:58 +0200 Subject: Fix sACN compile-time warning --- backends/sacn.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/sacn.c b/backends/sacn.c index 0c0fd10..5858f16 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -612,7 +612,7 @@ static int sacn_start(size_t n, instance** inst){ .label = 0 }; struct ip_mreq mcast_req = { - .imr_interface = { INADDR_ANY } + .imr_interface.s_addr = INADDR_ANY }; struct sockaddr_in* dest_v4 = NULL; -- cgit v1.2.3 From 9e11398e21981f7a1899eecf993a308d927fab84 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 9 May 2020 21:10:58 +0200 Subject: Add DEPLOY switch to CI script --- .travis-ci.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis-ci.sh b/.travis-ci.sh index 8d5d42d..c72070e 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -73,7 +73,7 @@ elif [ "$TASK" = "windows" ]; then fi make -C backends lua.dll travis_fold end "make_windows" - if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then + if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then travis_fold start "deploy_windows" mkdir ./deployment mkdir ./deployment/backends @@ -98,7 +98,7 @@ else exit "$?" fi travis_fold end "make" - if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then + if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then travis_fold start "deploy_unix" mkdir ./deployment mkdir ./deployment/backends -- cgit v1.2.3 From 9b9256b55ec4f61f14d5199be2a010ad3aeb1896 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 10 May 2020 12:58:52 +0200 Subject: Refactor wininput channel parsing, implement basic joystick queries --- backends/Makefile | 2 + backends/wininput.c | 167 ++++++++++++++++++++++++++++------------------------ 2 files changed, 92 insertions(+), 77 deletions(-) diff --git a/backends/Makefile b/backends/Makefile index 9b66728..dbb9f55 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -53,6 +53,8 @@ rtpmidi.dll: LDLIBS += -lws2_32 -liphlpapi winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) winmidi.dll: LDLIBS += -lwinmm -lws2_32 +wininput.dll: LDLIBS += -lwinmm + jack.so: LDLIBS = -ljack -lpthread midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev || echo "-DBUILD_ERROR=\"Missing pkg-config data for libevdev\"") diff --git a/backends/wininput.c b/backends/wininput.c index 0c7e490..f5303cc 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -4,8 +4,9 @@ #include #include "wininput.h" +#include + //TODO check whether feedback elimination is required -//TODO refactor & simplify static key_info keys[] = { {VK_LBUTTON, "lmb", button}, {VK_RBUTTON, "rmb", button}, {VK_MBUTTON, "mmb", button}, @@ -117,16 +118,16 @@ static int wininput_instance(instance* inst){ return 0; } -static int wininput_subscribe(wininput_channel_ident ident, channel* chan){ +static int wininput_subscribe(uint64_t ident, channel* chan){ size_t u, n; //find an existing request for(u = 0; u < cfg.requests; u++){ - if(cfg.request[u].ident.label == ident.label){ + if(cfg.request[u].ident.label == ident){ break; } } - + if(u == cfg.requests){ //create a new request cfg.request = realloc(cfg.request, (cfg.requests + 1) * sizeof(wininput_request)); @@ -136,7 +137,7 @@ static int wininput_subscribe(wininput_channel_ident ident, channel* chan){ return 1; } - cfg.request[u].ident.label = ident.label; + cfg.request[u].ident.label = ident; cfg.request[u].channels = 0; cfg.request[u].channel = NULL; cfg.request[u].state = 0; @@ -162,95 +163,96 @@ static int wininput_subscribe(wininput_channel_ident ident, channel* chan){ return 0; } -static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ +static uint64_t wininput_channel_mouse(instance* inst, char* spec, uint8_t flags){ size_t u; - channel* chan = NULL; - uint16_t scancode = 0; - char* token = spec; wininput_channel_ident ident = { - .label = 0 + .fields.type = mouse }; - if(!strncmp(spec, "mouse.", 6)){ - //TODO wheel - token += 6; - ident.fields.type = mouse; - if(!strcmp(token, "x")){ - ident.fields.channel = position; - } - else if(!strcmp(token, "y")){ - ident.fields.channel = position; - ident.fields.control = 1; - } - else{ - //check the buttons - for(u = 0; u < sizeof(keys) / sizeof(keys[0]); u++){ - if(keys[u].channel == button && !strcmp(keys[u].name, token)){ - DBGPF("Using keymap %" PRIsize_t " (%d) for spec %s", u, keys[u].keycode, token); - ident.fields.channel = button; - ident.fields.control = keys[u].keycode; - break; - } - } - - if(u == sizeof(keys) / sizeof(keys[0])){ - LOGPF("Unknown mouse control %s", token); - return NULL; - } - } + if(!strcmp(spec, "x")){ + ident.fields.channel = position; } - else if(!strncmp(spec, "key.", 4)){ - token += 4; - ident.fields.type = keyboard; - ident.fields.channel = keypress; - + else if(!strcmp(spec, "y")){ + ident.fields.channel = position; + ident.fields.control = 1; + } + else{ + //check the buttons for(u = 0; u < sizeof(keys) / sizeof(keys[0]); u++){ - if(keys[u].channel == keypress && !strcmp(keys[u].name, token)){ - DBGPF("Using keymap %" PRIsize_t " (%d) for spec %s", u, keys[u].keycode, token); + if(keys[u].channel == button && !strcmp(keys[u].name, spec)){ + DBGPF("Using keymap %" PRIsize_t " (%d) for spec %s", u, keys[u].keycode, spec); + ident.fields.channel = button; ident.fields.control = keys[u].keycode; break; } } - //no entry in translation table if(u == sizeof(keys) / sizeof(keys[0])){ - if(strlen(token) == 1){ - //try to translate - scancode = VkKeyScan(token[0]); - if(scancode != 0x7f7f){ - DBGPF("Using keyscan result %02X (via %04X) for spec %s", scancode & 0xFF, scancode, token); - ident.fields.type = keyboard; - ident.fields.channel = keypress; - ident.fields.control = scancode & 0xFF; - } - else{ - LOGPF("Invalid channel specification %s", token); - return NULL; - } - } - else if(strlen(token) > 1){ - //try to use as literal - scancode = strtoul(token, NULL, 0); - if(!scancode){ - LOGPF("Invalid channel specification %s", token); - return NULL; - } - DBGPF("Using direct conversion %d for spec %s", scancode & 0xFF, token); - ident.fields.control = scancode & 0xFF; - } - else{ - LOGPF("Invalid channel specification %s", spec); - return NULL; - } + LOGPF("Unknown mouse control %s", spec); + return 0; } } + + return ident.label; +} + +static uint64_t wininput_channel_key(instance* inst, char* spec, uint8_t flags){ + size_t u; + uint16_t scancode = 0; + wininput_channel_ident ident = { + .fields.type = keyboard, + .fields.channel = keypress + }; + + for(u = 0; u < sizeof(keys) / sizeof(keys[0]); u++){ + if(keys[u].channel == keypress && !strcmp(keys[u].name, spec)){ + DBGPF("Using keymap %" PRIsize_t " (%d) for spec %s", u, keys[u].keycode, spec); + ident.fields.control = keys[u].keycode; + return ident.label; + } + } + + //no entry in translation table + if(strlen(spec) == 1){ + //try to translate + scancode = VkKeyScan(spec[0]); + if(scancode != 0x7f7f){ + DBGPF("Using keyscan result %02X (via %04X) for spec %s", scancode & 0xFF, scancode, spec); + ident.fields.control = scancode & 0xFF; + return ident.label; + } + } + else if(strlen(spec) > 1){ + //try to use as literal + scancode = strtoul(spec, NULL, 0); + if(scancode){ + DBGPF("Using direct conversion %d for spec %s", scancode & 0xFF, spec); + ident.fields.control = scancode & 0xFF; + return ident.label; + } + } + + LOGPF("Invalid channel specification %s", spec); + return 0; +} + +static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ + channel* chan = NULL; + uint64_t label = 0; + + if(!strncmp(spec, "mouse.", 6)){ + label = wininput_channel_mouse(inst, spec + 6, flags); + } + else if(!strncmp(spec, "key.", 4)){ + label = wininput_channel_key(inst, spec + 4, flags); + } else{ - LOGPF("Unknown channel spec %s", spec); + LOGPF("Unknown channel spec type %s", spec); } - if(ident.label){ - chan = mm_channel(inst, ident.label, 1); - if(chan && (flags & mmchannel_input) && wininput_subscribe(ident, chan)){ + if(label){ + chan = mm_channel(inst, label, 1); + if(chan && (flags & mmchannel_input) && wininput_subscribe(label, chan)){ return NULL; } return chan; @@ -448,13 +450,24 @@ static int wininput_handle(size_t num, managed_fd* fds){ } static int wininput_start(size_t n, instance** inst){ + size_t u; POINT cursor_position; + JOYINFOEX joy_info; //if no input requested, don't request polling if(!cfg.requests){ cfg.interval = 0; } + DBGPF("This system supports a maximum of %u joysticks", joyGetNumDevs()); + for(u = 0; u < joyGetNumDevs(); u++){ + joy_info.dwSize = sizeof(joy_info); + joy_info.dwFlags = 0; + if(joyGetPosEx(u, &joy_info) == JOYERR_NOERROR){ + LOGPF("Joystick %" PRIsize_t " is available for input", u); + } + } + //read virtual desktop extents for later normalization cfg.virtual_width = GetSystemMetrics(SM_CXVIRTUALSCREEN); cfg.virtual_height = GetSystemMetrics(SM_CYVIRTUALSCREEN); -- cgit v1.2.3 From 262cb2a3878af58863c8423c8452f9d725bb856c Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 15 May 2020 21:07:03 +0200 Subject: Clarify wininput message, do not build in debug mode --- backends/wininput.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/wininput.c b/backends/wininput.c index f5303cc..0f35364 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "wininput" -#define DEBUG +//#define DEBUG #include #include "wininput.h" @@ -232,7 +232,7 @@ static uint64_t wininput_channel_key(instance* inst, char* spec, uint8_t flags){ } } - LOGPF("Invalid channel specification %s", spec); + LOGPF("Unknown keyboard control %s", spec); return 0; } -- cgit v1.2.3 From 100b8f2ea0f269094331ee39699ef5fb3e571584 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 16 May 2020 16:18:19 +0200 Subject: Implement maweb dynamic interval --- backends/maweb.c | 50 ++++++++++++++++++++++++++++++++------------------ backends/maweb.h | 2 ++ backends/maweb.md | 2 +- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 97d4cea..980a914 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "maweb" +//#define DEBUG #include #include @@ -15,14 +16,9 @@ #define WS_FLAG_FIN 0x80 #define WS_FLAG_MASK 0x80 -/* - * TODO handle peer close/unregister/reopen and fallback connections - */ - static uint64_t last_keepalive = 0; -static uint64_t update_interval = 50; +static uint64_t update_interval = 0; static uint64_t last_update = 0; -static uint64_t updates_inflight = 0; static uint64_t quiet_mode = 0; static maweb_command_key cmdline_keys[] = { @@ -136,7 +132,10 @@ static int channel_comparator(const void* raw_a, const void* raw_b){ } static uint32_t maweb_interval(){ - return update_interval - (last_update % update_interval); + if(update_interval){ + return update_interval - (last_update % update_interval); + } + return 0; } static int maweb_configure(char* option, char* value){ @@ -423,6 +422,7 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty } static int maweb_process_playbacks(instance* inst, int64_t page, char* payload, size_t payload_length){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; size_t base_offset = json_obj_offset(payload, "itemGroups"), group_offset, subgroup_offset, item_offset; uint64_t group = 0, subgroup, item, metatype; @@ -466,8 +466,9 @@ static int maweb_process_playbacks(instance* inst, int64_t page, char* payload, } group++; } - updates_inflight--; - DBGPF("Playback message processing done, %" PRIu64 " updates inflight", updates_inflight); + + data->updates_inflight--; + DBGPF("Playback message processing done, %" PRIu64 " updates inflight on %s", data->updates_inflight, inst->name); return 0; } @@ -479,9 +480,9 @@ static int maweb_request_playbacks(instance* inst){ char item_indices[1024] = "[300,400,500]", item_counts[1024] = "[16,16,16]", item_types[1024] = "[3,3,3]"; size_t page_index = 0, view = 3, channel = 0, offsets[3], channel_offset, channels; - if(updates_inflight){ + if(data->updates_inflight){ if(quiet_mode < 1){ - LOGPF("Skipping update request, %" PRIu64 " updates still inflight - consider raising the interval time", updates_inflight); + LOGPF("Skipping update request on %s, %" PRIu64 " updates still inflight - consider raising the interval time", inst->name, data->updates_inflight); } return 0; } @@ -572,15 +573,16 @@ static int maweb_request_playbacks(instance* inst){ data->session); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); DBGPF("Poll request: %s", xmit_buffer); - updates_inflight++; + data->updates_inflight++; } - DBGPF("Poll request handling done, %" PRIu64 " updates requested", updates_inflight); + DBGPF("Poll request handling done, %" PRIu64 " updates requested on %s", data->updates_inflight, inst->name); return rv; } static int maweb_handle_message(instance* inst, char* payload, size_t payload_length){ char xmit_buffer[MAWEB_XMIT_CHUNK]; + int64_t session = 0; char* field; maweb_instance_data* data = (maweb_instance_data*) inst->impl; @@ -591,6 +593,11 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le if(json_obj_bool(payload, "result", 0)){ LOG("Login successful"); data->login = 1; + + //initially request playbacks + if(!update_interval){ + maweb_request_playbacks(inst); + } } else{ LOG("Login failed"); @@ -601,21 +608,28 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le if(maweb_process_playbacks(inst, json_obj_int(payload, "iPage", 0), payload, payload_length)){ LOG("Failed to handle/request input data"); } + + //request playbacks again if configured + if(!update_interval && data->login && !data->updates_inflight){ + maweb_request_playbacks(inst); + } return 0; } } DBGPF("Incoming message (%" PRIsize_t "): %s", payload_length, payload); if(json_obj(payload, "session") == JSON_NUMBER){ - data->session = json_obj_int(payload, "session", data->session); - if(data->session < 0){ + session = json_obj_int(payload, "session", data->session); + if(session < 0){ LOG("Login failed"); + data->session = -1; data->login = 0; return 0; } - if(quiet_mode < 2){ - LOGPF("Session id is now %" PRId64, data->session); + if(data->session != session){ + LOGPF("Session ID changed from %" PRId64 " to %" PRId64 "", data->session, session); } + data->session = session; } if(json_obj_bool(payload, "forceLogin", 0)){ @@ -1047,7 +1061,7 @@ static int maweb_handle(size_t num, managed_fd* fds){ last_keepalive = mm_timestamp(); } - if(last_update && mm_timestamp() - last_update >= update_interval){ + if(update_interval && last_update && mm_timestamp() - last_update >= update_interval){ rv |= maweb_poll(); last_update = mm_timestamp(); } diff --git a/backends/maweb.h b/backends/maweb.h index 85ca09d..8efe6a8 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -100,4 +100,6 @@ typedef struct /*_maweb_instance_data*/ { size_t offset; size_t allocated; uint8_t* buffer; + + uint64_t updates_inflight; } maweb_instance_data; diff --git a/backends/maweb.md b/backends/maweb.md index 1547919..6ac2cd1 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -18,7 +18,7 @@ Web Remote. Set a web remote password using the option below the activation sett | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|---------------------------------------------------------------| -| `interval` | `100` | `50` | Query interval for input data polling (in msec). | +| `interval` | `100` | `0` | Query interval for input data polling (in msec). If set to 0 (the default), data is queried again when the previous data request has received an answer. | | `quiet` | `1` | `0` | Turn off some warning messages, for use by experts. | #### Instance configuration -- cgit v1.2.3 From 165fd82294090ea970e90c1b5436a5a98dc58eb9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 17 May 2020 18:37:47 +0200 Subject: Include virtual screen origin offset in mouse normalization --- backends/wininput.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backends/wininput.c b/backends/wininput.c index 0f35364..f044a7c 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "wininput" -//#define DEBUG +#define DEBUG #include #include "wininput.h" @@ -262,9 +262,8 @@ static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ //for some reason, sendinput only takes "normalized absolute coordinates", which are never again used in the API static void wininput_mouse_normalize(long* x, long* y){ - //TODO this needs to take a possible origin offset into account - long normalized_x = (double) (*x) * (65535.0f / (double) cfg.virtual_width); - long normalized_y = (double) (*y) * (65535.0f / (double) cfg.virtual_height); + long normalized_x = (double) (*x + cfg.virtual_x) * (65535.0f / (double) cfg.virtual_width); + long normalized_y = (double) (*y + cfg.virtual_y) * (65535.0f / (double) cfg.virtual_height); *x = normalized_x; *y = normalized_y; -- cgit v1.2.3 From 013718121c768507cef62ec03afa91d715a5ef38 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 23 May 2020 23:18:14 +0200 Subject: Allow unicode characters to pass getline on Windows --- config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.c b/config.c index 11e8a70..b10740c 100644 --- a/config.c +++ b/config.c @@ -76,7 +76,7 @@ static ssize_t getline(char** line, size_t* alloc, FILE* stream){ } //input broken - if(ferror(stream) || c < 0){ + if(ferror(stream)){ return -1; } -- cgit v1.2.3 From 5b4fedbafd476f5d08b55d5d30dc928407304312 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 24 May 2020 00:37:25 +0200 Subject: Implement joystick channel specification, fix normalization --- backends/wininput.c | 56 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/backends/wininput.c b/backends/wininput.c index f044a7c..3175171 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -59,7 +59,7 @@ static key_info keys[] = { static struct { int virtual_x, virtual_y, virtual_width, virtual_height; - uint16_t mouse_x, mouse_y; + long mouse_x, mouse_y; size_t requests; wininput_request* request; uint32_t interval; @@ -236,6 +236,48 @@ static uint64_t wininput_channel_key(instance* inst, char* spec, uint8_t flags){ return 0; } +static uint64_t wininput_channel_joystick(instance* inst, char* spec, uint8_t flags){ + char* token = NULL, *axes = "xyzruvp"; + uint16_t controller = strtoul(spec, &token, 0); + wininput_channel_ident ident = { + .fields.type = joystick + }; + + if(flags & mmchannel_output){ + LOG("Joystick channels can only be mapped as inputs on Windows"); + return 0; + } + + if(!controller || !token || *token != '.'){ + LOGPF("Invalid joystick specification %s", spec); + return 0; + } + + if(strlen(token) == 1 || !strcmp(token, "pov")){ + if(strchr(axes, token[0])){ + ident.fields.channel = position; + ident.fields.control = (controller << 8) | token[0]; + return ident.label; + } + + LOGPF("Unknown joystick axis specification %s", token); + return 0; + } + + if(!strncmp(token, "button", 6)){ + ident.fields.control = strtoul(token + 6, NULL, 10); + if(!ident.fields.control || ident.fields.control > 32){ + LOGPF("Button index out of range for specification %s", token); + return 0; + } + ident.fields.control |= (controller << 8); + return ident.label; + } + + printf("Invalid joystick control %s", spec); + return 0; +} + static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ channel* chan = NULL; uint64_t label = 0; @@ -246,6 +288,9 @@ static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ else if(!strncmp(spec, "key.", 4)){ label = wininput_channel_key(inst, spec + 4, flags); } + else if(!strncmp(spec, "joy", 3)){ + label = wininput_channel_joystick(inst, spec + 3, flags); + } else{ LOGPF("Unknown channel spec type %s", spec); } @@ -262,8 +307,8 @@ static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){ //for some reason, sendinput only takes "normalized absolute coordinates", which are never again used in the API static void wininput_mouse_normalize(long* x, long* y){ - long normalized_x = (double) (*x + cfg.virtual_x) * (65535.0f / (double) cfg.virtual_width); - long normalized_y = (double) (*y + cfg.virtual_y) * (65535.0f / (double) cfg.virtual_height); + long normalized_x = (double) (*x - cfg.virtual_x) * (65535.0f / (double) cfg.virtual_width); + long normalized_y = (double) (*y - cfg.virtual_y) * (65535.0f / (double) cfg.virtual_height); *x = normalized_x; *y = normalized_y; @@ -411,7 +456,8 @@ static int wininput_handle(size_t num, managed_fd* fds){ push_event = 1; } } - else{ + else if(cfg.request[u].ident.fields.type == keyboard + || cfg.request[u].ident.fields.type == mouse){ //check key state key_state = GetAsyncKeyState(cfg.request[u].ident.fields.control); if(key_state == 1){ @@ -463,7 +509,7 @@ static int wininput_start(size_t n, instance** inst){ joy_info.dwSize = sizeof(joy_info); joy_info.dwFlags = 0; if(joyGetPosEx(u, &joy_info) == JOYERR_NOERROR){ - LOGPF("Joystick %" PRIsize_t " is available for input", u); + LOGPF("Joystick %" PRIsize_t " is available for input", u + 1); } } -- cgit v1.2.3 From 9dc10f402022cd4812a3644761d8af91d0852a0e Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 25 May 2020 22:16:42 +0200 Subject: Sort wininput requests, implement basic joystick requests --- backends/wininput.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/backends/wininput.c b/backends/wininput.c index 3175171..71e9855 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -61,6 +61,7 @@ static struct { int virtual_x, virtual_y, virtual_width, virtual_height; long mouse_x, mouse_y; size_t requests; + //sorted in _start wininput_request* request; uint32_t interval; } cfg = { @@ -95,6 +96,24 @@ MM_PLUGIN_API int init(){ return 0; } +static int request_comparator(const void * raw_a, const void * raw_b){ + wininput_request* a = (wininput_request*) raw_a, *b = (wininput_request*) raw_b; + + //sort by type first + if(a->ident.fields.type != b->ident.fields.type){ + return a->ident.fields.type - b->ident.fields.type; + } + + //joysticks need to be sorted by controller id first so we can query them once + if(a->ident.fields.type == joystick){ + //joystick id is in the upper bits of control and we dont actually care about anything else + return a->ident.fields.control - b->ident.fields.control; + } + + //the rest doesnt actually need to be sorted at all + return 0; +} + static uint32_t wininput_interval(){ return cfg.interval; } @@ -252,6 +271,7 @@ static uint64_t wininput_channel_joystick(instance* inst, char* spec, uint8_t fl LOGPF("Invalid joystick specification %s", spec); return 0; } + token++; if(strlen(token) == 1 || !strcmp(token, "pov")){ if(strchr(axes, token[0])){ @@ -270,11 +290,12 @@ static uint64_t wininput_channel_joystick(instance* inst, char* spec, uint8_t fl LOGPF("Button index out of range for specification %s", token); return 0; } + ident.fields.channel = button; ident.fields.control |= (controller << 8); return ident.label; } - printf("Invalid joystick control %s", spec); + LOGPF("Invalid joystick control %s", spec); return 0; } @@ -419,10 +440,11 @@ static int wininput_handle(size_t num, managed_fd* fds){ channel_value val = { .normalised = 0 }; - uint8_t mouse_updated = 0, synthesize_off = 0, push_event = 0; + uint8_t mouse_updated = 0, synthesize_off = 0, push_event = 0, current_joystick = 0; uint16_t key_state = 0; - POINT cursor_position; size_t u = 0, n; + POINT cursor_position; + JOYINFOEX joy_info; for(u = 0; u < cfg.requests; u++){ synthesize_off = 0; @@ -473,6 +495,32 @@ static int wininput_handle(size_t num, managed_fd* fds){ push_event = 1; } } + else if(cfg.request[u].ident.fields.type == joystick){ + if(cfg.request[u].ident.fields.control >> 8 != current_joystick){ + joy_info.dwSize = sizeof(joy_info); + joy_info.dwFlags = JOY_RETURNALL; + if(joyGetPosEx((cfg.request[u].ident.fields.control >> 8) - 1, &joy_info) != JOYERR_NOERROR){ + LOGPF("Failed to query joystick %d", cfg.request[u].ident.fields.control >> 8); + //early exit because other joystick probably won't be connected either (though this may be wrong) + //else we would need to think of a way to mark the data invalid for subsequent requests on the same joystick + return 0; + } + current_joystick = cfg.request[u].ident.fields.control >> 8; + } + + if(cfg.request[u].ident.fields.channel == button){ + //button query + if(joy_info.dwFlags & JOY_RETURNBUTTONS){ + //TODO handle button requests + } + else{ + LOGPF("No button data received for joystick %d", cfg.request[u].ident.fields.control >> 8); + } + } + else{ + //TODO handle axis requests + } + } if(push_event){ //push current value to all channels @@ -498,6 +546,7 @@ static int wininput_start(size_t n, instance** inst){ size_t u; POINT cursor_position; JOYINFOEX joy_info; + JOYCAPS joy_caps; //if no input requested, don't request polling if(!cfg.requests){ @@ -509,7 +558,12 @@ static int wininput_start(size_t n, instance** inst){ joy_info.dwSize = sizeof(joy_info); joy_info.dwFlags = 0; if(joyGetPosEx(u, &joy_info) == JOYERR_NOERROR){ - LOGPF("Joystick %" PRIsize_t " is available for input", u + 1); + if(joyGetDevCaps(u, &joy_caps, sizeof(joy_caps)) == JOYERR_NOERROR){ + LOGPF("Joystick %" PRIsize_t " (%s) is available for input", u + 1, joy_caps.szPname ? joy_caps.szPname : "unknown model"); + } + else{ + LOGPF("Joystick %" PRIsize_t " available for input, but no capabilities reported", u + 1); + } } } @@ -520,6 +574,9 @@ static int wininput_start(size_t n, instance** inst){ cfg.virtual_y = GetSystemMetrics(SM_YVIRTUALSCREEN); DBGPF("Virtual screen is %dx%d with offset %dx%d", cfg.virtual_width, cfg.virtual_height, cfg.virtual_x, cfg.virtual_y); + //sort requests to allow querying each joystick only once + qsort(cfg.request, cfg.requests, sizeof(wininput_request), request_comparator); + //initialize mouse position if(!GetCursorPos(&cursor_position)){ LOG("Failed to read initial mouse position"); -- cgit v1.2.3 From 51b281340ba319eaa8399d507b00eaefa6c6a469 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 30 May 2020 12:23:32 +0200 Subject: maweb: Try next host (if configured) on login failure --- backends/maweb.c | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 980a914..297fb5f 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -593,15 +593,24 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le if(json_obj_bool(payload, "result", 0)){ LOG("Login successful"); data->login = 1; - + //initially request playbacks if(!update_interval){ maweb_request_playbacks(inst); } } else{ - LOG("Login failed"); data->login = 0; + + if(data->hosts > 1){ + LOGPF("Console login failed on %s, will try again with the next host", inst->name); + //mark as closed to reconnect + data->state = ws_closed; + } + else{ + LOGPF("Console login failed on %s", inst->name); + } + return 0; } } if(!strncmp(field, "playbacks", 9)){ @@ -621,10 +630,13 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le if(json_obj(payload, "session") == JSON_NUMBER){ session = json_obj_int(payload, "session", data->session); if(session < 0){ - LOG("Login failed"); - data->session = -1; - data->login = 0; - return 0; + LOG("Invalid session ID received, closing connection"); + data->session = -1; + data->login = 0; + + //this should be enough to mark the socket for the next keepalive/establish run + data->state = ws_closed; + return 0; } if(data->session != session){ LOGPF("Session ID changed from %" PRId64 " to %" PRId64 "", data->session, session); -- cgit v1.2.3 From 257ce41f43837dc1f1195d6ead397151c309e59b Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 2 Jun 2020 21:28:59 +0200 Subject: Change maweb session identifier message (Fixes #62) --- backends/maweb.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 297fb5f..1f8fcf0 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -630,7 +630,7 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le if(json_obj(payload, "session") == JSON_NUMBER){ session = json_obj_int(payload, "session", data->session); if(session < 0){ - LOG("Invalid session ID received, closing connection"); + LOG("Invalid web remote session identifier received, closing connection"); data->session = -1; data->login = 0; @@ -639,7 +639,7 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le return 0; } if(data->session != session){ - LOGPF("Session ID changed from %" PRId64 " to %" PRId64 "", data->session, session); + LOGPF("Web remote session ID changed from %" PRId64 " to %" PRId64 "", data->session, session); } data->session = session; } -- cgit v1.2.3 From e52c67ce0da7c3847d2583c8df6d5f1bd1fecb9e Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 5 Jun 2020 23:42:57 +0200 Subject: Log out of maweb session on disconnect (Fixes #61) --- backends/maweb.c | 56 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 1f8fcf0..7829ec4 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -16,6 +16,8 @@ #define WS_FLAG_FIN 0x80 #define WS_FLAG_MASK 0x80 +static void maweb_disconnect(instance* inst); + static uint64_t last_keepalive = 0; static uint64_t update_interval = 0; static uint64_t last_update = 0; @@ -351,8 +353,7 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload if(mmbackend_send(data->fd, frame_header, header_bytes) || mmbackend_send(data->fd, payload, len)){ LOGPF("Failed to send on instance %s, assuming connection failure", inst->name); - data->state = ws_closed; - data->login = 0; + maweb_disconnect(inst); return 1; } @@ -604,8 +605,7 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le if(data->hosts > 1){ LOGPF("Console login failed on %s, will try again with the next host", inst->name); - //mark as closed to reconnect - data->state = ws_closed; + maweb_disconnect(inst); } else{ LOGPF("Console login failed on %s", inst->name); @@ -631,11 +631,7 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le session = json_obj_int(payload, "session", data->session); if(session < 0){ LOG("Invalid web remote session identifier received, closing connection"); - data->session = -1; - data->login = 0; - - //this should be enough to mark the socket for the next keepalive/establish run - data->state = ws_closed; + maweb_disconnect(inst); return 0; } if(data->session != session){ @@ -668,6 +664,30 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le return 0; } +static void maweb_disconnect(instance* inst){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + char xmit_buffer[MAWEB_XMIT_CHUNK]; + + if(data->fd){ + //close the session if one is active + if(data->session > 0){ + snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"requestType\":\"close\",\"session\":%" PRIu64 "}", data->session); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + } + + mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL); + close(data->fd); + } + + data->fd = -1; + data->state = ws_closed; + data->login = 0; + data->session = -1; + data->peer_type = peer_unidentified; + data->offset = 0; + data->updates_inflight = 0; +} + static int maweb_connect(instance* inst){ int rv = 1; maweb_instance_data* data = (maweb_instance_data*) inst->impl; @@ -676,14 +696,8 @@ static int maweb_connect(instance* inst){ goto bail; } - //unregister old fd from core - if(data->fd >= 0){ - mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL); - close(data->fd); - data->fd = -1; - } - data->state = ws_closed; - data->login = 0; + //close old connection and reset state + maweb_disconnect(inst); LOGPF("Connecting to host %" PRIsize_t " of %" PRIsize_t " on %s", data->next_host + 1, data->hosts, inst->name); @@ -1140,14 +1154,10 @@ static int maweb_shutdown(size_t n, instance** inst){ free(data->pass); data->pass = NULL; - close(data->fd); - data->fd = -1; - + maweb_disconnect(inst[u]); free(data->buffer); data->buffer = NULL; - - data->offset = data->allocated = 0; - data->state = ws_closed; + data->allocated = 0; free(data->channel); data->channel = NULL; -- cgit v1.2.3 From 5e4c3fe5a81f548243aab01a42973333b559004f Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 11 Jun 2020 12:27:57 +0200 Subject: Query joystick buttons --- backends/maweb.c | 2 +- backends/wininput.c | 87 ++++++++++++++++++++++++++++++++++++++++++++-------- backends/wininput.h | 5 ++- backends/wininput.md | 5 ++- 4 files changed, 84 insertions(+), 15 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 7829ec4..33d2b7e 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -669,7 +669,7 @@ static void maweb_disconnect(instance* inst){ char xmit_buffer[MAWEB_XMIT_CHUNK]; if(data->fd){ - //close the session if one is active + //close the session if one is active if(data->session > 0){ snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"requestType\":\"close\",\"session\":%" PRIu64 "}", data->session); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); diff --git a/backends/wininput.c b/backends/wininput.c index 71e9855..627891e 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "wininput" -#define DEBUG +//#define DEBUG #include #include "wininput.h" @@ -7,6 +7,7 @@ #include //TODO check whether feedback elimination is required +//TODO might want to store virtual desktop extents in request->limit static key_info keys[] = { {VK_LBUTTON, "lmb", button}, {VK_RBUTTON, "rmb", button}, {VK_MBUTTON, "mmb", button}, @@ -57,6 +58,17 @@ static key_info keys[] = { {VK_ZOOM, "zoom"} }; +//this monstrosity is necessary because not only are the buttons not a simple bitmask, the bits are also partially reused. +//i get why they replaced this heap of trash API, but the replacement would require me to jump through even more hoops. +static uint32_t button_masks[32] = {JOY_BUTTON1, JOY_BUTTON2, JOY_BUTTON3, JOY_BUTTON4, + JOY_BUTTON5, JOY_BUTTON6, JOY_BUTTON7, JOY_BUTTON8, + JOY_BUTTON9, JOY_BUTTON10, JOY_BUTTON11, JOY_BUTTON12, + JOY_BUTTON13, JOY_BUTTON14, JOY_BUTTON15, JOY_BUTTON16, + JOY_BUTTON17, JOY_BUTTON18, JOY_BUTTON19, JOY_BUTTON20, + JOY_BUTTON21, JOY_BUTTON22, JOY_BUTTON23, JOY_BUTTON24, + JOY_BUTTON25, JOY_BUTTON26, JOY_BUTTON27, JOY_BUTTON28, + JOY_BUTTON29, JOY_BUTTON30, JOY_BUTTON31, JOY_BUTTON32}; + static struct { int virtual_x, virtual_y, virtual_width, virtual_height; long mouse_x, mouse_y; @@ -159,7 +171,7 @@ static int wininput_subscribe(uint64_t ident, channel* chan){ cfg.request[u].ident.label = ident; cfg.request[u].channels = 0; cfg.request[u].channel = NULL; - cfg.request[u].state = 0; + cfg.request[u].state = cfg.request[u].min = cfg.request[u].max = 0; cfg.requests++; } @@ -276,7 +288,7 @@ static uint64_t wininput_channel_joystick(instance* inst, char* spec, uint8_t fl if(strlen(token) == 1 || !strcmp(token, "pov")){ if(strchr(axes, token[0])){ ident.fields.channel = position; - ident.fields.control = (controller << 8) | token[0]; + ident.fields.control = ((controller - 1) << 8) | token[0]; return ident.label; } @@ -511,13 +523,25 @@ static int wininput_handle(size_t num, managed_fd* fds){ if(cfg.request[u].ident.fields.channel == button){ //button query if(joy_info.dwFlags & JOY_RETURNBUTTONS){ - //TODO handle button requests + key_state = (joy_info.dwButtons & button_masks[(cfg.request[u].ident.fields.control & 0xFF)]) > 0 ? 1 : 0; + if(key_state != cfg.request[u].state){ + if(key_state){ + val.normalised = 1.0; + } + cfg.request[u].state = key_state; + push_event = 1; + DBGPF("Joystick %d button %d: %d", + cfg.request[u].ident.fields.control >> 8, + cfg.request[u].ident.fields.control & 0xFF, + key_state); + } } else{ LOGPF("No button data received for joystick %d", cfg.request[u].ident.fields.control >> 8); } } else{ + //TODO handle axis requests } } @@ -542,17 +566,11 @@ static int wininput_handle(size_t num, managed_fd* fds){ return 0; } -static int wininput_start(size_t n, instance** inst){ - size_t u; - POINT cursor_position; +static void wininput_start_joystick(){ + size_t u, p; JOYINFOEX joy_info; JOYCAPS joy_caps; - //if no input requested, don't request polling - if(!cfg.requests){ - cfg.interval = 0; - } - DBGPF("This system supports a maximum of %u joysticks", joyGetNumDevs()); for(u = 0; u < joyGetNumDevs(); u++){ joy_info.dwSize = sizeof(joy_info); @@ -560,12 +578,57 @@ static int wininput_start(size_t n, instance** inst){ if(joyGetPosEx(u, &joy_info) == JOYERR_NOERROR){ if(joyGetDevCaps(u, &joy_caps, sizeof(joy_caps)) == JOYERR_NOERROR){ LOGPF("Joystick %" PRIsize_t " (%s) is available for input", u + 1, joy_caps.szPname ? joy_caps.szPname : "unknown model"); + for(p = 0; p < cfg.requests; p++){ + if(cfg.request[p].ident.fields.type == joystick + && cfg.request[p].ident.fields.channel == position + && (cfg.request[p].ident.fields.control >> 8) == u){ + //this looks really dumb, but the structure is defined in a way that prevents us from doing anything clever here + switch(cfg.request[p].ident.fields.control & 0xFF){ + case 'x': + cfg.request[p].min = joy_caps.wXmin; + cfg.request[p].max = joy_caps.wXmax; + break; + case 'y': + cfg.request[p].min = joy_caps.wYmin; + cfg.request[p].max = joy_caps.wYmax; + break; + case 'z': + cfg.request[p].min = joy_caps.wZmin; + cfg.request[p].max = joy_caps.wZmax; + break; + case 'r': + cfg.request[p].min = joy_caps.wRmin; + cfg.request[p].max = joy_caps.wRmax; + break; + case 'u': + cfg.request[p].min = joy_caps.wUmin; + cfg.request[p].max = joy_caps.wUmax; + break; + case 'v': + cfg.request[p].min = joy_caps.wVmin; + cfg.request[p].max = joy_caps.wVmax; + break; + } + DBGPF("Updated limits on request %" PRIsize_t " to %" PRIu32 " / %" PRIu32, p, cfg.request[p].min, cfg.request[p].max); + } + } } else{ LOGPF("Joystick %" PRIsize_t " available for input, but no capabilities reported", u + 1); } } } +} + +static int wininput_start(size_t n, instance** inst){ + POINT cursor_position; + + //if no input requested, don't request polling + if(!cfg.requests){ + cfg.interval = 0; + } + + wininput_start_joystick(); //read virtual desktop extents for later normalization cfg.virtual_width = GetSystemMetrics(SM_CXVIRTUALSCREEN); diff --git a/backends/wininput.h b/backends/wininput.h index 0318724..6ec9f46 100644 --- a/backends/wininput.h +++ b/backends/wininput.h @@ -47,5 +47,8 @@ typedef struct /*_input_request*/ { wininput_channel_ident ident; size_t channels; channel** channel; - uint16_t state; + uint32_t state; + + //used for jostick axes + uint32_t min, max; } wininput_request; diff --git a/backends/wininput.md b/backends/wininput.md index 6ead158..6e665bb 100644 --- a/backends/wininput.md +++ b/backends/wininput.md @@ -79,8 +79,11 @@ input.X > wi1.key.escape #### Known bugs / problems +Joysticks can only be used as input to the MIDIMonster, as Windows does not provide a method to emulate +Joystick input from user space. This is unlikely to change. + Keyboard and mouse input is subject to UIPI. You can not send input to applications that run at a higher -privilege level than the MIDIMonster. +privilege level than the MIDIMonster. This limitation is by design and will not change. Due to inconsistencies in the Windows API, mouse position input and output may differ for the same cursor location. This may be correlated with the use and arrangement of multi-monitor desktops. If you encounter problems with either -- cgit v1.2.3 From 7b3245de94d5e9453ec1f817aa810f6bd3e64c28 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 13 Jun 2020 10:32:43 +0200 Subject: Implement joystick axis input --- backends/wininput.c | 57 ++++++++++++++++++++++++++++++++++++++++++++++++---- backends/wininput.md | 27 +++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/backends/wininput.c b/backends/wininput.c index 627891e..876e276 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -510,7 +510,7 @@ static int wininput_handle(size_t num, managed_fd* fds){ else if(cfg.request[u].ident.fields.type == joystick){ if(cfg.request[u].ident.fields.control >> 8 != current_joystick){ joy_info.dwSize = sizeof(joy_info); - joy_info.dwFlags = JOY_RETURNALL; + joy_info.dwFlags = JOY_RETURNALL | JOY_RETURNPOVCTS; if(joyGetPosEx((cfg.request[u].ident.fields.control >> 8) - 1, &joy_info) != JOYERR_NOERROR){ LOGPF("Failed to query joystick %d", cfg.request[u].ident.fields.control >> 8); //early exit because other joystick probably won't be connected either (though this may be wrong) @@ -523,7 +523,7 @@ static int wininput_handle(size_t num, managed_fd* fds){ if(cfg.request[u].ident.fields.channel == button){ //button query if(joy_info.dwFlags & JOY_RETURNBUTTONS){ - key_state = (joy_info.dwButtons & button_masks[(cfg.request[u].ident.fields.control & 0xFF)]) > 0 ? 1 : 0; + key_state = (joy_info.dwButtons & button_masks[(cfg.request[u].ident.fields.control & 0xFF) - 1]) > 0 ? 1 : 0; if(key_state != cfg.request[u].state){ if(key_state){ val.normalised = 1.0; @@ -541,12 +541,61 @@ static int wininput_handle(size_t num, managed_fd* fds){ } } else{ + if(!cfg.request[u].max){ + cfg.request[u].max = 0xFFFF; + } + val.raw.u64 = cfg.request[u].state; - //TODO handle axis requests + //axis requests, every single access to these structures is stupid. + switch(cfg.request[u].ident.fields.control & 0xFF){ + case 'x': + if(joy_info.dwFlags & JOY_RETURNX){ + val.raw.u64 = joy_info.dwXpos; + } + break; + case 'y': + if(joy_info.dwFlags & JOY_RETURNY){ + val.raw.u64 = joy_info.dwYpos; + } + break; + case 'z': + if(joy_info.dwFlags & JOY_RETURNZ){ + val.raw.u64 = joy_info.dwZpos; + } + break; + case 'r': + if(joy_info.dwFlags & JOY_RETURNR){ + val.raw.u64 = joy_info.dwRpos; + } + break; + case 'u': + if(joy_info.dwFlags & JOY_RETURNU){ + val.raw.u64 = joy_info.dwUpos; + } + break; + case 'v': + if(joy_info.dwFlags & JOY_RETURNV){ + val.raw.u64 = joy_info.dwVpos; + } + break; + case 'p': + if(joy_info.dwFlags & (JOY_RETURNPOV | JOY_RETURNPOVCTS)){ + val.raw.u64 = joy_info.dwPOV; + } + break; + } + + if(val.raw.u64 != cfg.request[u].state){ + val.normalised = (double) (val.raw.u64 - cfg.request[u].min) / (double) (cfg.request[u].max - cfg.request[u].min); + cfg.request[u].state = val.raw.u64; + push_event = 1; + } } } if(push_event){ + //clamp value just to be safe + val.normalised = clamp(val.normalised, 1.0, 0.0); //push current value to all channels DBGPF("Pushing event %f on request %" PRIsize_t, val.normalised, u); for(n = 0; n < cfg.request[u].channels; n++){ @@ -609,7 +658,7 @@ static void wininput_start_joystick(){ cfg.request[p].max = joy_caps.wVmax; break; } - DBGPF("Updated limits on request %" PRIsize_t " to %" PRIu32 " / %" PRIu32, p, cfg.request[p].min, cfg.request[p].max); + DBGPF("Updated limits on request %" PRIsize_t " (%c) to %" PRIu32 " / %" PRIu32, p, cfg.request[p].ident.fields.control & 0xFF, cfg.request[p].min, cfg.request[p].max); } } } diff --git a/backends/wininput.md b/backends/wininput.md index 6e665bb..bcf6a1b 100644 --- a/backends/wininput.md +++ b/backends/wininput.md @@ -77,6 +77,33 @@ input.a > wi1.key.a input.X > wi1.key.escape ``` +Joystick and gamepad controllers with up to 32 buttons and 6 axes plus POV hat can be mapped as inputs to the +MIDIMonster. When starting up, the MIDIMonster will output a list of all connected and usable game controllers. + +Controllers can be mapped using the syntax + +* `joy.` for axes, where `` is the ID of the controller and `` is one of + * `x`, `y`: Main joystick / analog controller axes + * `z`: Third axis / joystick rotation + * `r`: Fourth axis / Rudder controller / Slider + * `u`, `v`: non-specific fifth/sixth axis +* `joy.button` for buttons, with `` again being the controller ID and `b` being the button number between + 1 and 32 (the maximum supported by Windows) + +Use the Windows game controller input calibration and configuration tool to identify the axes and button IDs +relevant to your controller. + +For button channels, the channel value will either be `0` or `1.0`, for axis channels it will be the normalized +value of the axis (with calibration offsets applied), with the exception of the POV axis, where the channel value +will be in some way correlated with the direction of view. + +Example mappings: +``` +input.joy1.x > movinghead.pan +input.joy1.y > movinghead.tilt +input.joy1.button1 > movinghead.dim +``` + #### Known bugs / problems Joysticks can only be used as input to the MIDIMonster, as Windows does not provide a method to emulate -- cgit v1.2.3 From d23dc2086f4467e7c439f6ddee022e48cbc0dfe1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 27 Jun 2020 18:59:41 +0200 Subject: Fix wininput joystick button calculation --- backends/wininput.c | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/backends/wininput.c b/backends/wininput.c index 876e276..8926782 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -58,17 +58,6 @@ static key_info keys[] = { {VK_ZOOM, "zoom"} }; -//this monstrosity is necessary because not only are the buttons not a simple bitmask, the bits are also partially reused. -//i get why they replaced this heap of trash API, but the replacement would require me to jump through even more hoops. -static uint32_t button_masks[32] = {JOY_BUTTON1, JOY_BUTTON2, JOY_BUTTON3, JOY_BUTTON4, - JOY_BUTTON5, JOY_BUTTON6, JOY_BUTTON7, JOY_BUTTON8, - JOY_BUTTON9, JOY_BUTTON10, JOY_BUTTON11, JOY_BUTTON12, - JOY_BUTTON13, JOY_BUTTON14, JOY_BUTTON15, JOY_BUTTON16, - JOY_BUTTON17, JOY_BUTTON18, JOY_BUTTON19, JOY_BUTTON20, - JOY_BUTTON21, JOY_BUTTON22, JOY_BUTTON23, JOY_BUTTON24, - JOY_BUTTON25, JOY_BUTTON26, JOY_BUTTON27, JOY_BUTTON28, - JOY_BUTTON29, JOY_BUTTON30, JOY_BUTTON31, JOY_BUTTON32}; - static struct { int virtual_x, virtual_y, virtual_width, virtual_height; long mouse_x, mouse_y; @@ -523,7 +512,7 @@ static int wininput_handle(size_t num, managed_fd* fds){ if(cfg.request[u].ident.fields.channel == button){ //button query if(joy_info.dwFlags & JOY_RETURNBUTTONS){ - key_state = (joy_info.dwButtons & button_masks[(cfg.request[u].ident.fields.control & 0xFF) - 1]) > 0 ? 1 : 0; + key_state = (joy_info.dwButtons & (1 << ((cfg.request[u].ident.fields.control & 0xFF) - 1))) > 0 ? 1 : 0; if(key_state != cfg.request[u].state){ if(key_state){ val.normalised = 1.0; -- cgit v1.2.3 From 4e5cc42d2eb245bfd2ac616ca6898e4855ec5363 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 27 Jun 2020 19:26:26 +0200 Subject: Remove manually built backends with clean target --- backends/Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backends/Makefile b/backends/Makefile index dbb9f55..09f5b96 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -3,6 +3,7 @@ LINUX_BACKENDS = midi.so evdev.so WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll wininput.dll BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.so OPTIONAL_BACKENDS = ola.so +MANUAL_BACKENDS = lua.dll BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) @@ -97,4 +98,4 @@ windows: ../libmmapi.a $(BACKEND_LIB) $(WINDOWS_BACKENDS) full: $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) clean: - $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) $(WINDOWS_BACKENDS) + $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) $(WINDOWS_BACKENDS) $(MANUAL_BACKENDS) -- cgit v1.2.3 From 1fd6f2bf7cd243ad3e2c126ebce723d57000c7b4 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 9 Jul 2020 22:56:31 +0200 Subject: Restructure CI --- .ci.sh | 131 +++++++++++++++++++++++++++++++++++++++++++ .travis-ci.sh | 116 -------------------------------------- .travis.yml | 17 ++---- backends/openpixelcontrol.md | 4 +- 4 files changed, 137 insertions(+), 131 deletions(-) create mode 100755 .ci.sh delete mode 100644 .travis-ci.sh diff --git a/.ci.sh b/.ci.sh new file mode 100755 index 0000000..087bf11 --- /dev/null +++ b/.ci.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +# Check for Travis and use the provided fold method if detected +if declare -f travis_fold > /dev/null; then + ci_fold(){ + travis_fold "$1" "$2" + } +else + ci_fold(){ + printf -- "-- %s stage %s --\n" "$1" "$2" + } +fi + +if [ -z "$OS" ]; then + OS="linux" +fi + +if [ "$TASK" = "spellcheck" ]; then + result=0 + # Create list of files to be spellchecked + spellcheck_files=$(find . -type f | grep -v ".git/") + + # Run spellintian to find spelling errors + sl_results=$(xargs spellintian 2>&1 <<< "$spellcheck_files") + + sl_errors=$(wc -l <<< "$sl_results") + sl_errors_dups=$((grep "\(duplicate word\)" | wc -l) <<< "$sl_results") + sl_errors_nodups=$((grep -v "\(duplicate word\)" | wc -l) <<< "$sl_results") + + if [ "$sl_errors" -ne 0 ]; then + printf "Spellintian found %s errors (%s spelling, %s duplicate words):\n\n" "$sl_errors" "$sl_errors_nodups" "$sl_errors_dups" + printf "%s\n\n" "$sl_results" + result=1 + else + printf "Spellintian reports no errors\n" + fi + + # Run codespell to find some more + cs_results=$(xargs codespell --quiet 2 <<< "$spellcheck_files" 2>&1) + cs_errors=$(wc -l <<< "$cs_results") + if [ "$cs_errors" -ne 0 ]; then + printf "Codespell found %s errors:\n\n" "$cs_errors" + printf "%s\n\n" "$cs_results" + result=1 + else + printf "Codespell reports no errors\n" + fi + exit "$result" +elif [ "$TASK" = "codesmell" ]; then + result=0 + + if [ -z "$(which lizard)" ]; then + printf "Installing lizard...\n" + pip3 install lizard + fi + + # Run shellcheck for all shell scripts + printf "Running shellcheck...\n" + shell_files="$(find . -type f -iname \*.sh)" + xargs shellcheck -Cnever -s bash <<< "$shell_files" + if [ "$?" -ne "0" ]; then + result=1 + fi + + # Run cloc for some stats + printf "Code statistics:\n\n" + cloc ./ + + # Run lizard for the project + printf "Running lizard for code complexity analysis\n" + lizard ./ + if [ "$?" -ne "0" ]; then + result=1 + fi + + exit "$result" +elif [ "$TASK" = "sanitize" ]; then + # Run sanitized compile + ci_fold start "make_sanitize" + if ! make sanitize; then + exit "$?" + fi + ci_fold end "make_sanitize" +elif [ "$TASK" = "windows" ]; then + ci_fold start "make_windows" + if ! make windows; then + exit "$?" + fi + make -C backends lua.dll + ci_fold end "make_windows" + if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then + ci_fold start "deploy_windows" + mkdir ./deployment + mkdir ./deployment/backends + mkdir ./deployment/docs + # Strip the Windows binaries as they become huge quickly + strip midimonster.exe backends/*.dll + cp ./midimonster.exe ./deployment/ + cp ./backends/*.dll ./deployment/backends/ + cp ./backends/*.dll.disabled ./deployment/backends/ + cp ./monster.cfg ./deployment/monster.cfg + cp ./backends/*.md ./deployment/docs/ + cp -r ./configs ./deployment/ + cd ./deployment + zip -r "./midimonster-$(git describe)-windows.zip" "./" + find . ! -iname '*.zip' -delete + ci_fold end "deploy_windows" + fi +else + # Otherwise compile as normal + ci_fold start "make" + if ! make full; then + exit "$?" + fi + ci_fold end "make" + if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then + ci_fold start "deploy_unix" + mkdir ./deployment + mkdir ./deployment/backends + mkdir ./deployment/docs + cp ./midimonster ./deployment/ + cp ./backends/*.so ./deployment/backends/ + cp ./monster.cfg ./deployment/monster.cfg + cp ./backends/*.md ./deployment/docs/ + cp -r ./configs ./deployment/ + cd ./deployment + tar czf "midimonster-$(git describe)-$OS.tgz" "./" + find . ! -iname '*.tgz' -delete + ci_fold end "deploy_unix" + fi +fi diff --git a/.travis-ci.sh b/.travis-ci.sh deleted file mode 100644 index c72070e..0000000 --- a/.travis-ci.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/bin/bash - -if [ "$TASK" = "spellcheck" ]; then - result=0 - # Create list of files to be spellchecked - spellcheck_files=$(find . -type f | grep -v ".git/") - - # Run spellintian to find spelling errors - sl_results=$(xargs spellintian 2>&1 <<< "$spellcheck_files") - - sl_errors=$(wc -l <<< "$sl_results") - sl_errors_dups=$((grep "\(duplicate word\)" | wc -l) <<< "$sl_results") - sl_errors_nodups=$((grep -v "\(duplicate word\)" | wc -l) <<< "$sl_results") - - if [ "$sl_errors" -ne 0 ]; then - printf "Spellintian found %s errors (%s spelling, %s duplicate words):\n\n" "$sl_errors" "$sl_errors_nodups" "$sl_errors_dups" - printf "%s\n\n" "$sl_results" - result=1 - else - printf "Spellintian reports no errors\n" - fi - - # Run codespell to find some more - cs_results=$(xargs codespell --quiet 2 <<< "$spellcheck_files" 2>&1) - cs_errors=$(wc -l <<< "$cs_results") - if [ "$cs_errors" -ne 0 ]; then - printf "Codespell found %s errors:\n\n" "$cs_errors" - printf "%s\n\n" "$cs_results" - result=1 - else - printf "Codespell reports no errors\n" - fi - exit "$result" -elif [ "$TASK" = "codesmell" ]; then - result=0 - - if [ -z "$(which lizard)" ]; then - printf "Installing lizard...\n" - pip3 install lizard - fi - - # Run shellcheck for all shell scripts - printf "Running shellcheck...\n" - shell_files="$(find . -type f -iname \*.sh)" - xargs shellcheck -Cnever -s bash <<< "$shell_files" - if [ "$?" -ne "0" ]; then - result=1 - fi - - # Run cloc for some stats - printf "Code statistics:\n\n" - cloc ./ - - # Run lizard for the project - printf "Running lizard for code complexity analysis\n" - lizard ./ - if [ "$?" -ne "0" ]; then - result=1 - fi - - exit "$result" -elif [ "$TASK" = "sanitize" ]; then - # Run sanitized compile - travis_fold start "make_sanitize" - if ! make sanitize; then - exit "$?" - fi - travis_fold end "make_sanitize" -elif [ "$TASK" = "windows" ]; then - travis_fold start "make_windows" - if ! make windows; then - exit "$?" - fi - make -C backends lua.dll - travis_fold end "make_windows" - if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then - travis_fold start "deploy_windows" - mkdir ./deployment - mkdir ./deployment/backends - mkdir ./deployment/docs - # Strip the Windows binaries as they become huge quickly - strip midimonster.exe backends/*.dll - cp ./midimonster.exe ./deployment/ - cp ./backends/*.dll ./deployment/backends/ - cp ./backends/*.dll.disabled ./deployment/backends/ - cp ./monster.cfg ./deployment/monster.cfg - cp ./backends/*.md ./deployment/docs/ - cp -r ./configs ./deployment/ - cd ./deployment - zip -r "./midimonster-$(git describe)-windows.zip" "./" - find . ! -iname '*.zip' -delete - travis_fold end "deploy_windows" - fi -else - # Otherwise compile as normal - travis_fold start "make" - if ! make full; then - exit "$?" - fi - travis_fold end "make" - if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then - travis_fold start "deploy_unix" - mkdir ./deployment - mkdir ./deployment/backends - mkdir ./deployment/docs - cp ./midimonster ./deployment/ - cp ./backends/*.so ./deployment/backends/ - cp ./monster.cfg ./deployment/monster.cfg - cp ./backends/*.md ./deployment/docs/ - cp -r ./configs ./deployment/ - cd ./deployment - tar czf "midimonster-$(git describe)-$TRAVIS_OS_NAME.tgz" ./ - find . ! -iname '*.tgz' -delete - travis_fold end "deploy_unix" - fi -fi diff --git a/.travis.yml b/.travis.yml index 4e14339..5f60e59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,17 +5,15 @@ dist: bionic before_script: - export -f travis_fold + - export OS="$TRAVIS_OS_NAME" script: - - "bash .travis-ci.sh" + - "bash .ci.sh" addons: apt: - packages: &base_build - - ccache packages: &core_build # This is all the bits we need to enable all options - - *base_build - libasound2-dev - libevdev-dev - libola-dev @@ -125,11 +123,6 @@ env: cache: apt: true - directories: - - $HOME/.ccache # ccache cache - -before_cache: - - ccache -s # see how many hits ccache got before_install: # Travis clones with --branch, which omits tags. Since we use them for the version string at build time, fetch them @@ -138,21 +131,19 @@ before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi # 'brew install' sometimes returns non-zero for some arcane reason. Executing 'true' resets the exit code and allows Travis to continue building... # Travis seems to have Python 2.7 installed by default, which for some reason prevents pkg-config from reading python3.pc - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl jack python3; brew link --overwrite python; true; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ola lua openssl jack python3; brew link --overwrite python; true; fi # OpenSSL is not a proper install due to some Apple bull, so provide additional locations via the environment... # Additionally, newer versions of this "recipe" seem to use the name 'openssl@1.1' instead of plain 'openssl' and there seems to be # no way to programmatically get the link and include paths. Genius! Hardcoding the new version for the time being... - export CFLAGS="$CFLAGS -I/usr/local/opt/openssl@1.1/include" - export LDFLAGS="$LDFLAGS -L/usr/local/opt/openssl@1.1/lib" - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then PATH=/usr/local/opt/ccache/libexec:$PATH; fi # Use ccache on Mac too - - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) -a "$CC" = "gcc" ]; then export CC="ccache gcc-8"; export CXX="ccache g++-8"; fi #Use the latest clang if we're compiling with clang - if [ "$TRAVIS_OS_NAME" == "linux" -a "$CC" = "clang" ]; then export CC="clang-6.0"; export CXX="clang-6.0"; fi #Report the compiler versions - $CC --version #OS X uses something other than $CXX variable - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) ]; then $CXX --version; fi -# Download libraries to link with on Windows +# Download libraries to link with for Windows - if [ "$TASK" == "windows" ]; then wget "https://downloads.sourceforge.net/project/luabinaries/5.3.5/Windows%20Libraries/Dynamic/lua-5.3.5_Win64_dllw6_lib.zip" -O lua53.zip; unzip lua53.zip lua53.dll; fi notifications: diff --git a/backends/openpixelcontrol.md b/backends/openpixelcontrol.md index d09d412..af5e811 100644 --- a/backends/openpixelcontrol.md +++ b/backends/openpixelcontrol.md @@ -35,12 +35,12 @@ Channels can be specified by their sequential index (one-based). Example mapping (data from Strip 2 LED 66's green component is mapped to the blue component of LED 2 on strip 1): ``` -strip1.channel6 < strip2.channel200 +op1.strip1.channel6 < op1.strip2.channel200 ``` Additionally, channels may be referred to by their color component and LED index: ``` -strip1.blue2 < strip2.green66 +op1.strip1.blue2 < op2.strip2.green66 ``` #### Known bugs / problems -- cgit v1.2.3 From 5c9c43e634aa4712b56da27cb418034506853c1c Mon Sep 17 00:00:00 2001 From: Spacelord Date: Mon, 20 Jul 2020 19:46:22 +0200 Subject: Implement small fixes to the installer script. --- installer.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/installer.sh b/installer.sh index 66eef99..b18a7ef 100755 --- a/installer.sh +++ b/installer.sh @@ -116,9 +116,9 @@ install_dependencies(){ printf "\n" } -ask_questions(){ +ask_questions(){ # Only say if necessary - if [ -n "$VAR_PREFIX" ] || [ -n "$VAR_PLUGINS" ] || [ -n "$VAR_DEFAULT_CFG" ] || [ -n "$VAR_EXAMPLE_CFGS" ]; then + 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" fi @@ -147,21 +147,22 @@ ask_questions(){ prepare_repo(){ printf "Cloning the repository\n" git clone "https://github.com/cbdevnet/midimonster.git" "$tmp_path" + printf "\n" # 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 case "$magic" in y|Y) - printf "OK! You´re a risky person ;D\n" + printf "OK! You´re a risky person ;D\n\n" NIGHTLY=1 ;; n|N) - printf "That´s OK - installing the latest stable version for you ;-)\n" + printf "That´s OK - installing the latest stable version for you ;-)\n\n" NIGHTLY=0 ;; *) - printf "${bold}Invalid input -- INSTALLING LATEST STABLE VERSION!${normal}\n" + printf "${bold}Invalid input -- INSTALLING LATEST STABLE VERSION!${normal}\n\n" NIGHTLY=0 ;; esac @@ -175,6 +176,7 @@ prepare_repo(){ printf "Checking out %s...\n" "$last_tag" git checkout -f -q "$last_tag" fi + printf "\n" } # Build and install the software @@ -237,7 +239,7 @@ fi # Check if we can download the sources if [ "$(wget -q --spider http://github.com)" ]; then - printf "The installer/updater requires internet connectivity to download the midimonster sources\n" + printf "The installer/updater requires internet connectivity to download the midimonster sources and dependencies\n" exit 1 fi -- cgit v1.2.3 From 7734eb3947df0ba586ff6c3ee7e1098b04f53bab Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 24 Jul 2020 01:10:56 +0200 Subject: Remove OSX gcc build --- .travis.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5f60e59..9fbe236 100644 --- a/.travis.yml +++ b/.travis.yml @@ -82,11 +82,6 @@ jobs: compiler: clang env: - TASK='compile' - - os: osx - osx_image: xcode10.2 - compiler: gcc - env: - - TASK='compile' - os: osx osx_image: xcode10.2 compiler: clang @@ -119,7 +114,7 @@ env: # No colours in terminal (to reduce log file size) - TERM=dumb # Parallel make build - - MAKEFLAGS="-j 2" + - MAKEFLAGS="-j 4" cache: apt: true @@ -139,10 +134,6 @@ before_install: - export LDFLAGS="$LDFLAGS -L/usr/local/opt/openssl@1.1/lib" #Use the latest clang if we're compiling with clang - if [ "$TRAVIS_OS_NAME" == "linux" -a "$CC" = "clang" ]; then export CC="clang-6.0"; export CXX="clang-6.0"; fi -#Report the compiler versions - - $CC --version -#OS X uses something other than $CXX variable - - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) ]; then $CXX --version; fi # Download libraries to link with for Windows - if [ "$TASK" == "windows" ]; then wget "https://downloads.sourceforge.net/project/luabinaries/5.3.5/Windows%20Libraries/Dynamic/lua-5.3.5_Win64_dllw6_lib.zip" -O lua53.zip; unzip lua53.zip lua53.dll; fi -- cgit v1.2.3 From 4e7acb39c73bd2108e55e4075350ddb82629fa26 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 24 Jul 2020 07:46:14 +0200 Subject: Select multicast interface for sACN where possible (#69) --- backends/sacn.c | 51 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/backends/sacn.c b/backends/sacn.c index 5858f16..170769f 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -604,6 +604,45 @@ static int sacn_handle(size_t num, managed_fd* fds){ return 0; } +static int sacn_start_multicast(instance* inst){ + sacn_instance_data* data = (sacn_instance_data*) inst->impl; + struct sockaddr_storage bound_name = { + 0 + }; + #ifdef _WIN32 + struct ip_mreq mcast_req = { + .imr_interface.s_addr = INADDR_ANY, + #else + struct ip_mreqn mcast_req = { + .imr_address.s_addr = INADDR_ANY, + #endif + .imr_multiaddr.s_addr = htobe32(((uint32_t) 0xefff0000) | ((uint32_t) data->uni)) + }; + socklen_t bound_length = sizeof(bound_name); + + //select the specific interface to join the mcast group on based on the bind address + if(getsockname(global_cfg.fd[data->fd_index].fd, (struct sockaddr*) &bound_name, &bound_length)){ + LOGPF("Failed to read back local bind address on socket %" PRIsize_t, data->fd_index); + return 1; + } + else if(bound_name.ss_family != AF_INET || !((struct sockaddr_in*) &bound_name)->sin_addr.s_addr){ + LOGPF("Socket %" PRIsize_t " not bound to a specific IPv4 address, joining multicast input group for instance %s (universe %u) on default interface", data->fd_index, inst->name, data->uni); + } + else{ + #ifdef _WIN32 + mcast_req.imr_interface = ((struct sockaddr_in*) &bound_name)->sin_addr; + #else + mcast_req.imr_address = ((struct sockaddr_in*) &bound_name)->sin_addr; + #endif + } + + if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ + LOGPF("Failed to join Multicast group for universe %u on instance %s: %s", data->uni, inst->name, mmbackend_socket_strerror(errno)); + } + + return 0; +} + static int sacn_start(size_t n, instance** inst){ size_t u, p; int rv = 1; @@ -611,9 +650,6 @@ static int sacn_start(size_t n, instance** inst){ sacn_instance_id id = { .label = 0 }; - struct ip_mreq mcast_req = { - .imr_interface.s_addr = INADDR_ANY - }; struct sockaddr_in* dest_v4 = NULL; if(!global_cfg.fds){ @@ -641,11 +677,8 @@ static int sacn_start(size_t n, instance** inst){ } } - if(!data->unicast_input){ - mcast_req.imr_multiaddr.s_addr = htobe32(((uint32_t) 0xefff0000) | ((uint32_t) data->uni)); - if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ - LOGPF("Failed to join Multicast group for universe %u on instance %s: %s", data->uni, inst[u]->name, mmbackend_socket_strerror(errno)); - } + if(!data->unicast_input && sacn_start_multicast(inst[u])){ + return 1; } if(data->xmit_prio){ @@ -667,7 +700,7 @@ static int sacn_start(size_t n, instance** inst){ dest_v4 = (struct sockaddr_in*) (&data->dest_addr); dest_v4->sin_family = AF_INET; dest_v4->sin_port = htobe16(strtoul(SACN_PORT, NULL, 10)); - dest_v4->sin_addr = mcast_req.imr_multiaddr; + dest_v4->sin_addr.s_addr = htobe32(((uint32_t) 0xefff0000) | ((uint32_t) data->uni)); } } } -- cgit v1.2.3 From 897e6f2e84e80f2109c5cb553b5fe15e7ce4ad50 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 25 Jul 2020 12:35:18 +0200 Subject: Output version information during normal startup --- midimonster.c | 1 + 1 file changed, 1 insertion(+) diff --git a/midimonster.c b/midimonster.c index b418711..3cc9719 100644 --- a/midimonster.c +++ b/midimonster.c @@ -472,6 +472,7 @@ int main(int argc, char** argv){ return EXIT_FAILURE; } + version(); if(platform_initialize()){ fprintf(stderr, "Failed to perform platform-specific initialization\n"); return EXIT_FAILURE; -- cgit v1.2.3 From 6291031a98539bdf51262329b0dc20604c2bad70 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 26 Jul 2020 23:01:35 +0200 Subject: Implement rudimentary detect mode for sACN (#70) --- backends/sacn.c | 39 ++++++++++++++++++++++++++++++++++++--- backends/sacn.h | 1 + backends/sacn.md | 1 + configs/launchctl-sacn.cfg | 3 ++- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/backends/sacn.c b/backends/sacn.c index 170769f..247dfc7 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "sacn" +#define DEBUG #include #include @@ -29,13 +30,15 @@ static struct /*_sacn_global_config*/ { sacn_fd* fd; uint64_t last_announce; uint32_t next_frame; + uint8_t detect; } global_cfg = { .source_name = "MIDIMonster", .cid = {'M', 'I', 'D', 'I', 'M', 'o', 'n', 's', 't', 'e', 'r'}, .fds = 0, .fd = NULL, .last_announce = 0, - .next_frame = 0 + .next_frame = 0, + .detect = 0 }; MM_PLUGIN_API int init(){ @@ -130,6 +133,16 @@ static int sacn_configure(char* option, char* value){ global_cfg.cid[u] = (strtoul(next, &next, 0) & 0xFF); } } + else if(!strcmp(option, "detect")){ + global_cfg.detect = 0; + if(!strcmp(value, "on")){ + global_cfg.detect = 1; + } + else if(!strcmp(value, "verbose")){ + global_cfg.detect = 2; + } + return 0; + } else if(!strcmp(option, "bind")){ mmbackend_parse_hostspec(value, &host, &port, &next); @@ -138,8 +151,13 @@ static int sacn_configure(char* option, char* value){ return 1; } - if(next && !strncmp(next, "local", 5)){ - flags = mcast_loop; + //parse additional socket options + if(next){ + for(next = strtok(next, " "); next; next = strtok(NULL, " ")){ + if(!strcmp(next, "local")){ + flags |= mcast_loop; + } + } } if(sacn_listener(host, port ? port : SACN_PORT, flags)){ @@ -401,6 +419,9 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame //source filtering if(inst_data->filter_enabled && memcmp(inst_data->cid_filter, frame->sender_cid, 16)){ + if(global_cfg.detect > 1){ + LOGPF("Discarding data for instance %s due to source filter rule", inst->name); + } return 0; } @@ -418,11 +439,19 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame //handle source priority (currently a 1-bit counter) if(inst_data->data.last_priority > data->priority){ + if(global_cfg.detect > 1){ + LOGPF("Ignoring lower-priority (%d) source on %s, current source is %d", data->priority, inst->name, inst_data->data.last_priority); + } inst_data->data.last_priority = data->priority; return 0; } inst_data->data.last_priority = data->priority; + if(!inst_data->last_input && global_cfg.detect){ + LOGPF("Valid data on instance %s (Universe %u): Source name %.*s, priority %d", inst->name, inst_data->uni, 64, data->source_name, data->priority); + } + inst_data->last_input = mm_timestamp(); + //read data (except start code), mark changed channels for(u = 1; u < be16toh(data->channels); u++){ if(IS_ACTIVE(inst_data->data.map[u - 1]) @@ -583,6 +612,10 @@ static int sacn_handle(size_t num, managed_fd* fds){ if(inst && sacn_process_frame(inst, frame, data)){ LOG("Failed to process frame"); } + else if(!inst && global_cfg.detect > 1){ + //this will only happen with unicast input + LOGPF("Received data for unconfigured universe %d on descriptor %" PRIsize_t, be16toh(data->universe), ((uint64_t) fds[u].impl) & 0xFFFF); + } } } } while(bytes_read > 0); diff --git a/backends/sacn.h b/backends/sacn.h index 4138f45..0c44ebc 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -40,6 +40,7 @@ typedef struct /*_sacn_universe_model*/ { } sacn_universe; typedef struct /*_sacn_instance_model*/ { + uint32_t last_input; uint16_t uni; uint8_t realtime; uint8_t xmit_prio; diff --git a/backends/sacn.md b/backends/sacn.md index 598f430..3bc5b72 100644 --- a/backends/sacn.md +++ b/backends/sacn.md @@ -11,6 +11,7 @@ containing all write-enabled universes. | `name` | `sACN source` | `MIDIMonster` | sACN source name | | `cid` | `0xAA 0xBB 0xCC` ... | `MIDIMonster` | Source CID (16 bytes) | | `bind` | `0.0.0.0 5568` | none | Binds a network address to listen for data. This option may be set multiple times, with each descriptor being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one descriptor is required for operation. | +| `detect` | `on`, `verbose` | `off` | Output additional information on incoming and outgoing data packets to help with configuring complex scenarios. | The `bind` configuration value can be extended by the keyword `local` to allow software on the local host to process the sACN output frames from the MIDIMonster (e.g. `bind = 0.0.0.0 5568 local`). diff --git a/configs/launchctl-sacn.cfg b/configs/launchctl-sacn.cfg index 10a736a..612ac25 100644 --- a/configs/launchctl-sacn.cfg +++ b/configs/launchctl-sacn.cfg @@ -7,7 +7,8 @@ name = MIDIMonster [backend sacn] -bind = 0.0.0.0 5568 local +bind = 0.0.0.0 5568 +detect = verbose [midi lc] read = Launch Control -- cgit v1.2.3 From 7f2f36ddaa9c8311404248c69cc686bac6264ceb Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 1 Aug 2020 20:04:25 +0200 Subject: Allow initial maweb event push even for zero values (#74) --- backends/maweb.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/maweb.c b/backends/maweb.c index 33d2b7e..39ef7a6 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -249,7 +249,7 @@ static int maweb_instance(instance* inst){ static channel* maweb_channel(instance* inst, char* spec, uint8_t flags){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; maweb_channel_data chan = { - 0 + .in = -1 //this hack allows the initial data request to push events even for zero'ed channels }; char* next_token = NULL; channel* channel_ref = NULL; -- cgit v1.2.3 From 39dfd02d5daa8ce7cf749f6235cf6450b2171214 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 7 Aug 2020 22:34:31 +0200 Subject: Implement detect mode for artnet (#70) --- backends/artnet.c | 128 ++++++++++++++++++++++++++++++++--------------------- backends/artnet.h | 1 + backends/artnet.md | 3 +- backends/sacn.c | 1 - backends/sacn.h | 2 +- backends/sacn.md | 2 +- 6 files changed, 82 insertions(+), 55 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index e07ea52..4a7907d 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -9,14 +9,19 @@ #define MAX_FDS 255 -static uint32_t next_frame = 0; -static uint8_t default_net = 0; -static size_t artnet_fds = 0; -static artnet_descriptor* artnet_fd = NULL; +static struct { + uint32_t next_frame; + uint8_t default_net; + size_t fds; + artnet_descriptor* fd; + uint8_t detect; +} global_cfg = { + 0 +}; static int artnet_listener(char* host, char* port){ int fd; - if(artnet_fds >= MAX_FDS){ + if(global_cfg.fds >= MAX_FDS){ LOG("Backend descriptor limit reached"); return -1; } @@ -27,18 +32,19 @@ static int artnet_listener(char* host, char* port){ } //store fd - artnet_fd = realloc(artnet_fd, (artnet_fds + 1) * sizeof(artnet_descriptor)); - if(!artnet_fd){ + global_cfg.fd = realloc(global_cfg.fd, (global_cfg.fds + 1) * sizeof(artnet_descriptor)); + if(!global_cfg.fd){ close(fd); + global_cfg.fds = 0; LOG("Failed to allocate memory"); return -1; } - LOGPF("Interface %" PRIsize_t " bound to %s port %s", artnet_fds, host, port); - artnet_fd[artnet_fds].fd = fd; - artnet_fd[artnet_fds].output_instances = 0; - artnet_fd[artnet_fds].output_instance = NULL; - artnet_fds++; + LOGPF("Interface %" PRIsize_t " bound to %s port %s", global_cfg.fds, host, port); + global_cfg.fd[global_cfg.fds].fd = fd; + global_cfg.fd[global_cfg.fds].output_instances = 0; + global_cfg.fd[global_cfg.fds].output_instance = NULL; + global_cfg.fds++; return 0; } @@ -70,8 +76,8 @@ MM_PLUGIN_API int init(){ } static uint32_t artnet_interval(){ - if(next_frame){ - return next_frame; + if(global_cfg.next_frame){ + return global_cfg.next_frame; } return ARTNET_KEEPALIVE_INTERVAL; } @@ -80,7 +86,7 @@ static int artnet_configure(char* option, char* value){ char* host = NULL, *port = NULL, *fd_opts = NULL; if(!strcmp(option, "net")){ //configure default net - default_net = strtoul(value, NULL, 0); + global_cfg.default_net = strtoul(value, NULL, 0); return 0; } else if(!strcmp(option, "bind")){ @@ -97,6 +103,16 @@ static int artnet_configure(char* option, char* value){ } return 0; } + else if(!strcmp(option, "detect")){ + global_cfg.detect = 0; + if(!strcmp(value, "on")){ + global_cfg.detect = 1; + } + else if(!strcmp(value, "verbose")){ + global_cfg.detect = 2; + } + return 0; + } LOGPF("Unknown backend option %s", option); return 1; @@ -111,7 +127,7 @@ static int artnet_instance(instance* inst){ return 1; } - data->net = default_net; + data->net = global_cfg.default_net; for(u = 0; u < sizeof(data->data.channel) / sizeof(channel); u++){ data->data.channel[u].ident = u; data->data.channel[u].instance = inst; @@ -136,7 +152,7 @@ static int artnet_configure_instance(instance* inst, char* option, char* value){ else if(!strcmp(option, "iface") || !strcmp(option, "interface")){ data->fd_index = strtoul(value, NULL, 0); - if(data->fd_index >= artnet_fds){ + if(data->fd_index >= global_cfg.fds){ LOGPF("Invalid interface configured for instance %s", inst->name); return 1; } @@ -223,7 +239,7 @@ static int artnet_transmit(instance* inst, artnet_output_universe* output){ }; memcpy(frame.data, data->data.out, 512); - if(sendto(artnet_fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ + if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ #ifdef _WIN32 if(WSAGetLastError() != WSAEWOULDBLOCK){ #else @@ -234,8 +250,8 @@ static int artnet_transmit(instance* inst, artnet_output_universe* output){ } //reschedule frame output output->mark = 1; - if(!next_frame || next_frame > ARTNET_SYNTHESIZE_MARGIN){ - next_frame = ARTNET_SYNTHESIZE_MARGIN; + if(!global_cfg.next_frame || global_cfg.next_frame > ARTNET_SYNTHESIZE_MARGIN){ + global_cfg.next_frame = ARTNET_SYNTHESIZE_MARGIN; } return 0; } @@ -279,22 +295,22 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) if(mark){ //find last frame time - for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){ - if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){ + for(u = 0; u < global_cfg.fd[data->fd_index].output_instances; u++){ + if(global_cfg.fd[data->fd_index].output_instance[u].label == inst->ident){ break; } } - frame_delta = mm_timestamp() - artnet_fd[data->fd_index].output_instance[u].last_frame; + frame_delta = mm_timestamp() - global_cfg.fd[data->fd_index].output_instance[u].last_frame; //check output rate limit, request next frame if(frame_delta < ARTNET_FRAME_TIMEOUT){ - artnet_fd[data->fd_index].output_instance[u].mark = 1; - if(!next_frame || next_frame > (ARTNET_FRAME_TIMEOUT - frame_delta)){ - next_frame = (ARTNET_FRAME_TIMEOUT - frame_delta); + global_cfg.fd[data->fd_index].output_instance[u].mark = 1; + if(!global_cfg.next_frame || global_cfg.next_frame > (ARTNET_FRAME_TIMEOUT - frame_delta)){ + global_cfg.next_frame = (ARTNET_FRAME_TIMEOUT - frame_delta); } return 0; } - return artnet_transmit(inst, artnet_fd[data->fd_index].output_instance + u); + return artnet_transmit(inst, global_cfg.fd[data->fd_index].output_instance + u); } return 0; @@ -307,6 +323,11 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ channel_value val; artnet_instance_data* data = (artnet_instance_data*) inst->impl; + if(!data->last_input && global_cfg.detect){ + LOGPF("Valid data on instance %s (Net %d Universe %d): %d channels", inst->name, data->net, data->uni, be16toh(frame->length)); + } + data->last_input = mm_timestamp(); + if(be16toh(frame->length) > 512){ LOGPF("Invalid frame channel count: %d", be16toh(frame->length)); return 1; @@ -366,23 +387,23 @@ static int artnet_handle(size_t num, managed_fd* fds){ artnet_pkt* frame = (artnet_pkt*) recv_buf; //transmit keepalive & synthesized frames - next_frame = 0; - for(u = 0; u < artnet_fds; u++){ - for(c = 0; c < artnet_fd[u].output_instances; c++){ - synthesize_delta = timestamp - artnet_fd[u].output_instance[c].last_frame; - if((artnet_fd[u].output_instance[c].mark + global_cfg.next_frame = 0; + for(u = 0; u < global_cfg.fds; u++){ + for(c = 0; c < global_cfg.fd[u].output_instances; c++){ + synthesize_delta = timestamp - global_cfg.fd[u].output_instance[c].last_frame; + if((global_cfg.fd[u].output_instance[c].mark && synthesize_delta >= ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN) //synthesize next frame || synthesize_delta >= ARTNET_KEEPALIVE_INTERVAL){ //keepalive timeout - inst = mm_instance_find(BACKEND_NAME, artnet_fd[u].output_instance[c].label); + inst = mm_instance_find(BACKEND_NAME, global_cfg.fd[u].output_instance[c].label); if(inst){ - artnet_transmit(inst, artnet_fd[u].output_instance + c); + artnet_transmit(inst, global_cfg.fd[u].output_instance + c); } } //update next_frame - if(artnet_fd[u].output_instance[c].mark - && (!next_frame || next_frame > ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta)){ - next_frame = ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta; + if(global_cfg.fd[u].output_instance[c].mark + && (!global_cfg.next_frame || global_cfg.next_frame > ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta)){ + global_cfg.next_frame = ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta; } } } @@ -400,6 +421,9 @@ static int artnet_handle(size_t num, managed_fd* fds){ if(inst && artnet_process_frame(inst, frame)){ LOG("Failed to process frame"); } + else if(!inst && global_cfg.detect > 1){ + LOGPF("Received data for unconfigured universe %d (net %d) on descriptor %" PRIsize_t, frame->universe, frame->net, (((uint64_t) fds[u].impl) & 0xFF)); + } } } } while(bytes_read > 0); @@ -429,7 +453,7 @@ static int artnet_start(size_t n, instance** inst){ .label = 0 }; - if(!artnet_fds){ + if(!global_cfg.fds){ LOG("Failed to start backend: no descriptors bound"); return 1; } @@ -452,23 +476,23 @@ static int artnet_start(size_t n, instance** inst){ //if enabled for output, add to keepalive tracking if(data->dest_len){ - artnet_fd[data->fd_index].output_instance = realloc(artnet_fd[data->fd_index].output_instance, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(artnet_output_universe)); + global_cfg.fd[data->fd_index].output_instance = realloc(global_cfg.fd[data->fd_index].output_instance, (global_cfg.fd[data->fd_index].output_instances + 1) * sizeof(artnet_output_universe)); - if(!artnet_fd[data->fd_index].output_instance){ + if(!global_cfg.fd[data->fd_index].output_instance){ LOG("Failed to allocate memory"); goto bail; } - artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].label = id.label; - artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].last_frame = 0; - artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].mark = 0; + global_cfg.fd[data->fd_index].output_instance[global_cfg.fd[data->fd_index].output_instances].label = id.label; + global_cfg.fd[data->fd_index].output_instance[global_cfg.fd[data->fd_index].output_instances].last_frame = 0; + global_cfg.fd[data->fd_index].output_instance[global_cfg.fd[data->fd_index].output_instances].mark = 0; - artnet_fd[data->fd_index].output_instances++; + global_cfg.fd[data->fd_index].output_instances++; } } - LOGPF("Registering %" PRIsize_t " descriptors to core", artnet_fds); - for(u = 0; u < artnet_fds; u++){ - if(mm_manage_fd(artnet_fd[u].fd, BACKEND_NAME, 1, (void*) u)){ + LOGPF("Registering %" PRIsize_t " descriptors to core", global_cfg.fds); + for(u = 0; u < global_cfg.fds; u++){ + if(mm_manage_fd(global_cfg.fd[u].fd, BACKEND_NAME, 1, (void*) u)){ goto bail; } } @@ -485,11 +509,13 @@ static int artnet_shutdown(size_t n, instance** inst){ free(inst[p]->impl); } - for(p = 0; p < artnet_fds; p++){ - close(artnet_fd[p].fd); - free(artnet_fd[p].output_instance); + for(p = 0; p < global_cfg.fds; p++){ + close(global_cfg.fd[p].fd); + free(global_cfg.fd[p].output_instance); } - free(artnet_fd); + free(global_cfg.fd); + global_cfg.fd = NULL; + global_cfg.fds = 0; LOG("Backend shut down"); return 0; diff --git a/backends/artnet.h b/backends/artnet.h index a517aa0..ecd775e 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -47,6 +47,7 @@ typedef struct /*_artnet_instance_model*/ { socklen_t dest_len; artnet_universe data; size_t fd_index; + uint64_t last_input; } artnet_instance_data; typedef union /*_artnet_instance_id*/ { diff --git a/backends/artnet.md b/backends/artnet.md index 383203d..73f598a 100644 --- a/backends/artnet.md +++ b/backends/artnet.md @@ -9,8 +9,9 @@ Art-Net™ Designed by and Copyright Artistic Licence Holdings Ltd. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `bind` | `127.0.0.1 6454` | none | Binds a network address to listen for data. This option may be set multiple times, with each interface being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one interface is required for transmission. | +| `bind` | `127.0.0.1 6454` | none | Binds a network address to listen for data. This option may be set multiple times, with each interface being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one interface is required for transmission. | | `net` | `0` | `0` | The default net to use | +| `detect` | `on`, `verbose` | `off` | Output additional information on received data packets to help with configuring complex scenarios | #### Instance configuration diff --git a/backends/sacn.c b/backends/sacn.c index 247dfc7..0ea7b58 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -1,5 +1,4 @@ #define BACKEND_NAME "sacn" -#define DEBUG #include #include diff --git a/backends/sacn.h b/backends/sacn.h index 0c44ebc..0f24538 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -40,7 +40,7 @@ typedef struct /*_sacn_universe_model*/ { } sacn_universe; typedef struct /*_sacn_instance_model*/ { - uint32_t last_input; + uint64_t last_input; uint16_t uni; uint8_t realtime; uint8_t xmit_prio; diff --git a/backends/sacn.md b/backends/sacn.md index 3bc5b72..b7686e0 100644 --- a/backends/sacn.md +++ b/backends/sacn.md @@ -11,7 +11,7 @@ containing all write-enabled universes. | `name` | `sACN source` | `MIDIMonster` | sACN source name | | `cid` | `0xAA 0xBB 0xCC` ... | `MIDIMonster` | Source CID (16 bytes) | | `bind` | `0.0.0.0 5568` | none | Binds a network address to listen for data. This option may be set multiple times, with each descriptor being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one descriptor is required for operation. | -| `detect` | `on`, `verbose` | `off` | Output additional information on incoming and outgoing data packets to help with configuring complex scenarios. | +| `detect` | `on`, `verbose` | `off` | Output additional information on received data packets to help with configuring complex scenarios. | The `bind` configuration value can be extended by the keyword `local` to allow software on the local host to process the sACN output frames from the MIDIMonster (e.g. `bind = 0.0.0.0 5568 local`). -- cgit v1.2.3 From b6ea5bbddf9db836ba206127b6eb77ca21cb8fa4 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 8 Aug 2020 09:58:44 +0200 Subject: Lua channel introspection (#68) --- backends/lua.c | 40 ++++++++++++++++++++++++++++++++++++++-- backends/lua.md | 5 +++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index 98ce369..cf59f8f 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -137,6 +137,8 @@ static int lua_update_timerfd(){ } static void lua_thread_resume(size_t current_thread){ + int thread_status = 0; + //push coroutine reference lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD); lua_pushnumber(thread[current_thread].thread, current_thread); @@ -144,9 +146,17 @@ static void lua_thread_resume(size_t current_thread){ //call thread main DBGPF("Resuming thread %" PRIsize_t " on %s", current_thread, thread[current_thread].instance->name); - if(lua_resume(thread[current_thread].thread, NULL, 0) != LUA_YIELD){ - DBGPF("Thread %" PRIsize_t " on %s terminated", current_thread, thread[current_thread].instance->name); + thread_status = lua_resume(thread[current_thread].thread, NULL, 0); + + if(thread_status == LUA_YIELD){ + DBGPF("Thread %" PRIsize_t " on %s yielded execution", current_thread, thread[current_thread].instance->name); + } + else{ thread[current_thread].timeout = 0; + LOGPF("Thread %" PRIsize_t " on %s terminated", current_thread, thread[current_thread].instance->name); + if(thread_status){ + LOGPF("Last error message: %s", lua_tostring(thread[current_thread].thread, -1)); + } } //remove coroutine reference @@ -166,6 +176,30 @@ static instance* lua_fetch_instance(lua_State* interpreter){ return inst; } +static int lua_callback_channels(lua_State* interpreter){ + size_t u; + instance* inst = lua_fetch_instance(interpreter); + lua_instance_data* data = (lua_instance_data*) inst->impl; + + if(!last_timestamp){ + LOG("The channels() API will not return usable results before the configuration has been read completely"); + } + + //create a table for the return array + lua_createtable(interpreter, data->channels, 0); + + for(u = 0; u < data->channels; u++){ + //push the key + lua_pushnumber(interpreter, u + 1); + //push the value + lua_pushstring(interpreter, data->channel[u].name); + //settable pops key and value, leaving the table + lua_settable(interpreter, -3); + } + + return 1; +} + static int lua_callback_thread(lua_State* interpreter){ instance* inst = lua_fetch_instance(interpreter); size_t u = threads; @@ -467,6 +501,7 @@ static int lua_instance(instance* inst){ lua_register(data->interpreter, "thread", lua_callback_thread); lua_register(data->interpreter, "sleep", lua_callback_sleep); lua_register(data->interpreter, "cleanup_handler", lua_callback_cleanup_handler); + lua_register(data->interpreter, "channels", lua_callback_channels); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -604,6 +639,7 @@ static int lua_resolve_symbol(lua_State* interpreter, char* symbol){ || !strcmp(symbol, "input_channel") || !strcmp(symbol, "timestamp") || !strcmp(symbol, "cleanup_handler") + || !strcmp(symbol, "channels") || !strcmp(symbol, "interval")){ return LUA_NOREF; } diff --git a/backends/lua.md b/backends/lua.md index b2f40e0..026c945 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -6,8 +6,8 @@ and manipulate events using the Lua scripting language. Every instance has its own interpreter state which can be loaded with custom scripts. To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist) -with the value (as a Lua `number` type) as parameter. Alternatively, a designated default channel handler -which will receive events for all incoming channels may be supplied in the configuration. +with the normalized event value (as a Lua `number` type) as parameter. Alternatively, a designated +default channel handler which will receive events for all incoming channels may be set in the configuration. The backend can also call Lua functions repeatedly using a timer, allowing users to implement time-based functionality (such as evaluating a fixed mathematical function or outputting periodic updates). @@ -25,6 +25,7 @@ The following functions are provided within the Lua interpreter for interaction | `timestamp()` | `print(timestamp())` | Returns the core timestamp for this iteration with millisecond resolution. This is not a performance timer, but intended for timeouting, etc | | `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) | | `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds | +| `channels()` | `chans = channels()` | Fetch an array of all currently known channels on the instance. Note that this function only works properly after the configuration has been read completely, i.e. any time after startup | While a channel handler executes, calling `input_value` for that channel returns the previous value. The stored value is updated once the handler returns. -- cgit v1.2.3 From 7a00b8fda337ad38cfba4689dd5fc07686783158 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 8 Aug 2020 15:38:48 +0200 Subject: Python channel introspection (#68) --- backends/python.c | 19 +++++++++++++++++++ backends/python.md | 1 + 2 files changed, 20 insertions(+) diff --git a/backends/python.c b/backends/python.c index bd73a20..b9b6518 100644 --- a/backends/python.c +++ b/backends/python.c @@ -112,6 +112,24 @@ static int python_prepend_str(PyObject* list, char* str){ return 0; } +static PyObject* mmpy_channels(PyObject* self, PyObject* args){ + size_t u = 0; + PyObject* list = NULL; + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + + if(!last_timestamp){ + LOG("The channels() API will not return usable results before the configuration has been read completely"); + } + + list = PyList_New(data->channels); + for(u = 0; u < data->channels; u++){ + PyList_SET_ITEM(list, u, PyUnicode_FromString(data->channel[u].name)); + } + + return list; +} + static PyObject* mmpy_output(PyObject* self, PyObject* args){ instance* inst = *((instance**) PyModule_GetState(self)); python_instance_data* data = (python_instance_data*) inst->impl; @@ -432,6 +450,7 @@ static PyObject* mmpy_init(){ {"timestamp", mmpy_timestamp, METH_VARARGS, "Get the core timestamp (in milliseconds)"}, {"manage", mmpy_manage_fd, METH_VARARGS, "(Un-)register a socket or file descriptor for notifications"}, {"interval", mmpy_interval, METH_VARARGS, "Register or update an interval handler"}, + {"channels", mmpy_channels, METH_VARARGS, "List currently registered instance channels"}, {"cleanup_handler", mmpy_cleanup_handler, METH_VARARGS, "Register or update the instances cleanup handler"}, {0} }; diff --git a/backends/python.md b/backends/python.md index a78d972..2114a08 100644 --- a/backends/python.md +++ b/backends/python.md @@ -24,6 +24,7 @@ The `midimonster` module provides the following functions: | `timestamp()` | `print(midimonster.timestamp())` | Get the internal core timestamp (in milliseconds) | | `interval(function, long)` | `midimonster.interval(toggle, 100)` | Register a function to be called periodically. Interval is specified in milliseconds (accurate to 10msec). Calling `interval` with the same function again updates the interval. Specifying the interval as `0` cancels the interval | | `manage(function, socket)` | `midimonster.manage(handler, socket)` | Register a (connected/listening) socket to the MIDIMonster core. Calls `function(socket)` when the socket is ready to read. Calling this method with `None` as the function argument unregisters the socket. A socket may only have one associated handler | +| `channels()` | `midimonster.channels()` | Fetch a list of all currently known channels on the instance. Note that this function only returns useful data after the configuration has been read completely, i.e. any time after initial startup | | `cleanup_handler(function)` | `midimonster.cleanup_handler(save_all)`| Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | When a channel handler executes, calling `midimonster.inputvalue()` for that exact channel returns the previous value, -- cgit v1.2.3 From ee055791d1430187ec175c3f065398460a5acf6b Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 9 Aug 2020 14:16:03 +0200 Subject: Implement list-type multichannel specification (Fixes #67) --- README.md | 19 +++++++---- config.c | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++----------- config.h | 9 +++++ 3 files changed, 116 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 161fcaa..ea079bf 100644 --- a/README.md +++ b/README.md @@ -122,18 +122,25 @@ The last line is a shorter way to create a bi-directional mapping. ### Multi-channel mapping -To make mapping large contiguous sets of channels easier, channel names may contain -expressions of the form `{..}`, with *start* and *end* being positive integers -delimiting a range of channels. Multiple such expressions may be used in one channel -specification, with the rightmost expression being incremented (or decremented) first for -evaluation. +To make mapping large contiguous sets of channels easier, channel names may contain certain +types of expressions specifying multiple channels at once. + +Expressions of the form `{..}`, with *start* and *end* being positive integers, +expand to a range of channels, with the expression replaced by the incrementing or decrementing +value. + +Expressions of the form `{value1,value2,value3}` (with any number of values separated by commas) +are replaced with each of the specified values in sequence. + +Multiple such expressions may be used in one channel specification, with the rightmost expression +being evaluated first. Both sides of a multi-channel assignment need to have the same number of channels, or one side must have exactly one channel. Example multi-channel mapping: ``` -instance-a.channel{1..10} > instance-b.{10..1} +instance-a.channel{1..5} > instance-b.{1,2,3,4,5} ``` ## Backend documentation diff --git a/config.c b/config.c index b10740c..b939f4e 100644 --- a/config.c +++ b/config.c @@ -7,6 +7,7 @@ #endif #define BACKEND_NAME "core/cfg" +#define DEBUG #include "midimonster.h" #include "config.h" #include "backend.h" @@ -99,9 +100,10 @@ static char* config_trim_line(char* in){ return in; } -static int config_glob_parse(channel_glob* glob, char* spec, size_t length){ - char* parse_offset = NULL; +static int config_glob_parse_range(channel_glob* glob, char* spec, size_t length){ //FIXME might want to allow negative delimiters at some point + char* parse_offset = NULL; + glob->type = glob_range; //first interval member glob->limits.u64[0] = strtoul(spec, &parse_offset, 10); @@ -130,6 +132,39 @@ static int config_glob_parse(channel_glob* glob, char* spec, size_t length){ return 0; } +static int config_glob_parse_list(channel_glob* glob, char* spec, size_t length){ + size_t u = 0; + glob->type = glob_list; + glob->values = 1; + + //count number of values in list + for(u = 0; u < length; u++){ + if(spec[u] == ','){ + glob->values++; + } + } + return 0; +} + +static int config_glob_parse(channel_glob* glob, char* spec, size_t length){ + size_t u = 0; + + //detect glob type + for(u = 0; u < length; u++){ + if(length - u > 2 && !strncmp(spec + u, "..", 2)){ + DBGPF("Detected glob %.*s as range type", (int) length, spec); + return config_glob_parse_range(glob, spec, length); + } + else if(spec[u] == ','){ + DBGPF("Detected glob %.*s as list type", (int) length, spec); + return config_glob_parse_list(glob, spec, length); + } + } + + LOGPF("Failed to detect glob type for spec %.*s", (int) length, spec); + return 1; +} + static int config_glob_scan(instance* inst, channel_spec* spec){ char* glob_start = spec->spec, *glob_end = NULL; size_t u; @@ -182,50 +217,89 @@ static int config_glob_scan(instance* inst, channel_spec* spec){ return 0; } +static ssize_t config_glob_resolve_range(char* spec, size_t length, channel_glob* glob, uint64_t n){ + uint64_t current_value = glob->limits.u64[0] + (n % glob->values); + //if counting down + if(glob->limits.u64[0] > glob->limits.u64[1]){ + current_value = glob->limits.u64[0] - (n % glob->values); + } + + //write out value + return snprintf(spec, length, "%" PRIu64, current_value); +} + +static ssize_t config_glob_resolve_list(char* spec, size_t length, channel_glob* glob, uint64_t n){ + uint64_t current_replacement = 0; + size_t replacement_length = 0; + char* source = spec + 1; + n %= glob->values; + + //find start of replacement value + DBGPF("Searching instance %" PRIu64 " of spec %.*s", n, (int) length, spec); + for(current_replacement = 0; current_replacement < n; current_replacement++){ + for(; source[0] != ','; source++){ + } + source++; + } + + //calculate replacement length + for(; source[replacement_length] != ',' && source[replacement_length] != '}'; replacement_length++){ + } + + //write out new value + memmove(spec, source, replacement_length); + return replacement_length; +} + static channel* config_glob_resolve(instance* inst, channel_spec* spec, uint64_t n, uint8_t map_direction){ size_t glob = 0, glob_length; ssize_t bytes = 0; - uint64_t current_value = 0; channel* result = NULL; char* resolved_spec = strdup(spec->spec); if(!resolved_spec){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); return NULL; } //TODO if not internal, try to resolve externally - //iterate and resolve globs for(glob = spec->globs; glob > 0; glob--){ - current_value = spec->glob[glob - 1].limits.u64[0] + (n % spec->glob[glob - 1].values); - if(spec->glob[glob - 1].limits.u64[0] > spec->glob[glob - 1].limits.u64[1]){ - current_value = spec->glob[glob - 1].limits.u64[0] - (n % spec->glob[glob - 1].values); - } glob_length = spec->glob[glob - 1].offset[1] - spec->glob[glob - 1].offset[0]; - n /= spec->glob[glob - 1].values; - //write out value - bytes = snprintf(resolved_spec + spec->glob[glob - 1].offset[0], - glob_length, - "%" PRIu64, - current_value); - if(bytes > glob_length){ - fprintf(stderr, "Internal error resolving glob %s\n", spec->spec); - goto bail; + switch(spec->glob[glob - 1].type){ + case glob_range: + bytes = config_glob_resolve_range(resolved_spec + spec->glob[glob - 1].offset[0], + glob_length, + spec->glob + (glob - 1), + n); + break; + case glob_list: + bytes = config_glob_resolve_list(resolved_spec + spec->glob[glob - 1].offset[0], + glob_length, + spec->glob + (glob - 1), + n); + break; } + n /= spec->glob[glob - 1].values; + //move trailing data - if(bytes < glob_length){ + if(bytes > 0 && bytes < glob_length){ memmove(resolved_spec + spec->glob[glob - 1].offset[0] + bytes, resolved_spec + spec->glob[glob - 1].offset[1] + 1, strlen(spec->spec) - spec->glob[glob - 1].offset[1]); } + else{ + LOGPF("Failure parsing glob spec %s", resolved_spec); + goto bail; + } } + DBGPF("Resolved spec %s to %s", spec->spec, resolved_spec); result = inst->backend->channel(inst, resolved_spec, map_direction); if(spec->globs && !result){ - fprintf(stderr, "Failed to match multichannel evaluation %s to a channel\n", resolved_spec); + LOGPF("Failed to match multichannel evaluation %s to a channel", resolved_spec); } bail: diff --git a/config.h b/config.h index d15aed2..b96a866 100644 --- a/config.h +++ b/config.h @@ -1,3 +1,11 @@ +/* + * Channel glob type + */ +enum /*_mm_channel_glob_type */ { + glob_range, + glob_list +}; + /* * Channel specification glob */ @@ -7,6 +15,7 @@ typedef struct /*_mm_channel_glob*/ { void* impl; uint64_t u64[2]; } limits; + uint8_t type; uint64_t values; } channel_glob; -- cgit v1.2.3 From f23ff5277386fc5658b349f0d35d8850a3752f0a Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 9 Aug 2020 14:18:03 +0200 Subject: Don't build config parser in debug mode --- config.c | 1 - 1 file changed, 1 deletion(-) diff --git a/config.c b/config.c index b939f4e..9945319 100644 --- a/config.c +++ b/config.c @@ -7,7 +7,6 @@ #endif #define BACKEND_NAME "core/cfg" -#define DEBUG #include "midimonster.h" #include "config.h" #include "backend.h" -- cgit v1.2.3 From 5716802da4778c3c7507e401bba09686e23ceb60 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 14 Aug 2020 23:14:03 +0200 Subject: Initial VISCA backend implementation --- backends/visca.c | 222 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/visca.h | 57 ++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 backends/visca.c create mode 100644 backends/visca.h diff --git a/backends/visca.c b/backends/visca.c new file mode 100644 index 0000000..f8bdae1 --- /dev/null +++ b/backends/visca.c @@ -0,0 +1,222 @@ +#define BACKEND_NAME "visca" +#define DEBUG + +#include +#include "visca.h" +#include "libmmbackend.h" + +/* TODO + * VISCA server + */ + +MM_PLUGIN_API int init(){ + backend ptz = { + .name = BACKEND_NAME, + .conf = ptz_configure, + .create = ptz_instance, + .conf_instance = ptz_configure_instance, + .channel = ptz_channel, + .handle = ptz_set, + .process = ptz_handle, + .start = ptz_start, + .shutdown = ptz_shutdown + }; + + //register backend + if(mm_backend_register(ptz)){ + LOG("Failed to register backend"); + return 1; + } + return 0; +} + +static int ptz_configure(char* option, char* value){ + LOG("No backend configuration possible"); + return 1; +} + +static int ptz_configure_instance(instance* inst, char* option, char* value){ + char* host = NULL, *port = NULL, *options = NULL; + ptz_instance_data* data = (ptz_instance_data*) inst->impl; + uint8_t mode = 0; + + if(!strcmp(option, "id")){ + data->cam_address = strtoul(value, NULL, 10); + return 0; + } + if(!strcmp(option, "connect")){ + if(data->fd >= 0){ + LOGPF("Instance %s already connected", inst->name); + return 1; + } + + mmbackend_parse_hostspec(value, &host, &port, &options); + if(!host || !port){ + LOGPF("Invalid destination address specified for instance %s", inst->name); + return 1; + } + + if(options && !strcmp(options, "udp")){ + mode = 1; + } + + data->fd = mmbackend_socket(host, port, mode ? SOCK_DGRAM : SOCK_STREAM, 0, 0, 1); + if(data->fd < 0){ + LOGPF("Failed to connect instance %s", inst->name); + return 1; + } + return 0; + } + + LOGPF("Unknown instance configuration parameter %s for instance %s", option, inst->name); + return 1; +} + +static int ptz_instance(instance* inst){ + ptz_instance_data* data = calloc(1, sizeof(ptz_instance_data)); + if(!data){ + LOG("Failed to allocate memory"); + return 1; + } + + data->fd = -1; + data->cam_address = 1; + //start with maximum speeds + data->panspeed = ptz_channels[panspeed].max; + data->tiltspeed = ptz_channels[tiltspeed].max; + + inst->impl = data; + return 0; +} + +static channel* ptz_channel(instance* inst, char* spec, uint8_t flags){ + uint64_t ident = pan; + size_t command = 0; + + if(flags & mmchannel_input){ + LOG("This backend currently only supports output channels"); + return NULL; + } + + for(command = 0; command < sentinel; command++){ + if(!strncmp(spec, ptz_channels[command].name, strlen(ptz_channels[command].name))){ + ident = command; + } + } + + if(ident == sentinel){ + LOGPF("Unknown channel spec %s", spec); + return NULL; + } + + if(ident == call){ + ident |= (strtoul(spec + strlen(ptz_channels[call].name), NULL, 10) << 8); + } + + return mm_channel(inst, ident, 1); +} + +static size_t ptz_set_pantilt(instance* inst, channel* c, channel_value* v, uint8_t* msg){ + ptz_instance_data* data = (ptz_instance_data*) inst->impl; + uint32_t* x = (uint32_t*) msg + 6; + uint32_t* y = (uint32_t*) msg + 10; + + if(c->ident == pan){ + data->x = ((ptz_channels[pan].max - ptz_channels[pan].min) * v->normalised) + ptz_channels[pan].min; + } + else{ + data->y = ((ptz_channels[tilt].max - ptz_channels[tilt].min) * v->normalised) + ptz_channels[tilt].min; + } + + msg[4] = data->panspeed; + msg[5] = data->tiltspeed; + *x = htobe32(data->x); + *y = htobe32(data->y); + + return ptz_channels[pan].bytes; +} + +static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint8_t* msg){ + ptz_instance_data* data = (ptz_instance_data*) inst->impl; + if(c->ident == panspeed){ + data->panspeed = ((ptz_channels[panspeed].max - ptz_channels[panspeed].min) * v->normalised) + ptz_channels[panspeed].min; + } + else{ + data->tiltspeed = ((ptz_channels[tiltspeed].max - ptz_channels[tiltspeed].min) * v->normalised) + ptz_channels[tiltspeed].min; + } + return 0; +} + +static size_t ptz_set_zoom(instance* inst, channel* c, channel_value* v, uint8_t* msg){ + uint32_t* position = (uint32_t*) msg + 4; + *position = htobe32(((ptz_channels[zoom].max - ptz_channels[zoom].min) * v->normalised) + ptz_channels[zoom].min); + return ptz_channels[zoom].bytes; +} + +static size_t ptz_set_focus(instance* inst, channel* c, channel_value* v, uint8_t* msg){ + uint32_t* position = (uint32_t*) msg + 4; + *position = htobe32(((ptz_channels[focus].max - ptz_channels[focus].min) * v->normalised) + ptz_channels[focus].min); + return ptz_channels[focus].bytes; +} + +static size_t ptz_set_memory(instance* inst, channel* c, channel_value* v, uint8_t* msg){ + if(v->normalised < 0.9){ + return 0; + } + + msg[5] = (c->ident >> 8); + return ptz_channels[call].bytes; +} + +static int ptz_set(instance* inst, size_t num, channel** c, channel_value* v){ + ptz_instance_data* data = (ptz_instance_data*) inst->impl; + size_t n = 0, bytes = 0; + uint8_t tx[VISCA_BUFFER_LENGTH] = ""; + uint8_t command = 0; + + for(n = 0; n < num; n++){ + bytes = 0; + command = c[n]->ident & 0xFF; + + if(ptz_channels[command].bytes){ + memcpy(tx, ptz_channels[command].pattern, ptz_channels[command].bytes); + } + tx[0] = 0x80 | (data->cam_address & 0xF); + + if(ptz_channels[command].set){ + bytes = ptz_channels[command].set(inst, c[n], v + n, tx); + } + + if(bytes && mmbackend_send(data->fd, tx, bytes)){ + LOGPF("Failed to push %s command on instance %s", ptz_channels[command].name, inst->name); + } + } + return 0; +} + +static int ptz_handle(size_t num, managed_fd* fds){ + //no events generated here + return 0; +} + +static int ptz_start(size_t n, instance** inst){ + //no startup needed yet + return 0; +} + +static int ptz_shutdown(size_t n, instance** inst){ + size_t u; + ptz_instance_data* data = NULL; + + for(u = 0; u < n; u++){ + data = (ptz_instance_data*) inst[u]->impl; + if(data->fd >= 0){ + close(data->fd); + } + free(data); + inst[u]->impl = NULL; + } + + LOG("Backend shut down"); + return 0; +} diff --git a/backends/visca.h b/backends/visca.h new file mode 100644 index 0000000..54c80f7 --- /dev/null +++ b/backends/visca.h @@ -0,0 +1,57 @@ +#include "midimonster.h" + +MM_PLUGIN_API int init(); +static int ptz_configure(char* option, char* value); +static int ptz_configure_instance(instance* inst, char* option, char* value); +static int ptz_instance(instance* inst); +static channel* ptz_channel(instance* inst, char* spec, uint8_t flags); +static int ptz_set(instance* inst, size_t num, channel** c, channel_value* v); +static int ptz_handle(size_t num, managed_fd* fds); +static int ptz_start(size_t n, instance** inst); +static int ptz_shutdown(size_t n, instance** inst); + +#define VISCA_BUFFER_LENGTH 50 + +typedef struct /*_ptz_instance_data*/ { + int fd; + uint8_t cam_address; + uint32_t x; + uint32_t y; + uint8_t panspeed; + uint8_t tiltspeed; +} ptz_instance_data; + +enum /*ptz_channels*/ { + pan = 0, + tilt, + panspeed, + tiltspeed, + zoom, + focus, + call, + sentinel +}; + +typedef size_t (*ptz_channel_set)(instance*, channel*, channel_value*, uint8_t* msg); +static size_t ptz_set_pantilt(instance* inst, channel* c, channel_value* v, uint8_t* msg); +static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint8_t* msg); +static size_t ptz_set_zoom(instance* inst, channel* c, channel_value* v, uint8_t* msg); +static size_t ptz_set_focus(instance* inst, channel* c, channel_value* v, uint8_t* msg); +static size_t ptz_set_memory(instance* inst, channel* c, channel_value* v, uint8_t* msg); + +static struct { + char* name; + size_t bytes; + uint8_t pattern[VISCA_BUFFER_LENGTH]; + size_t min; + size_t max; + ptz_channel_set set; +} ptz_channels[] = { + [pan] = {"pan", 14, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 4000, ptz_set_pantilt}, + [tilt] = {"tilt", 14, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 4000, ptz_set_pantilt}, + [panspeed] = {"panspeed", 0, {0}, 0x01, 0x18, ptz_set_ptspeed}, + [tiltspeed] = {"tiltspeed", 0, {0}, 0x01, 0x14, ptz_set_ptspeed}, + [zoom] = {"zoom", 9, {0x80, 0x01, 0x04, 0x47, 0, 0, 0, 0, 0xFF}, 0, 4000, ptz_set_zoom}, + [focus] = {"focus", 9, {0x80, 0x01, 0x04, 0x48, 0, 0, 0, 0, 0xFF}, 0, 4000, ptz_set_focus}, + [call] = {"memory", 7, {0x80, 0x01, 0x04, 0x3F, 0x02, 0, 0xFF}, 0, 254, ptz_set_memory} +}; -- cgit v1.2.3 From 690aec061db4cfab50b998822628f732e115e11e Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 15 Aug 2020 11:15:01 +0200 Subject: Improve some documentation --- README.md | 2 +- backends/Makefile | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ea079bf..3a3fcbd 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ side must have exactly one channel. Example multi-channel mapping: ``` -instance-a.channel{1..5} > instance-b.{1,2,3,4,5} +instance-a.channel{1..5} > instance-b.{a,b,c,d,e} ``` ## Backend documentation diff --git a/backends/Makefile b/backends/Makefile index 09f5b96..4f89b43 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,17 +1,26 @@ .PHONY: all clean full +# Backends that can only be built on Linux LINUX_BACKENDS = midi.so evdev.so -WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll wininput.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.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 +# 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 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) MANUAL_BACKENDS = lua.dll + +# The backend library, providing platform-independent abstractions for common things BACKEND_LIB = libmmbackend.o +# Evaluate which system we are on SYSTEM := $(shell uname -s) # Generate debug symbols unless overridden CFLAGS ?= -g CPPFLAGS ?= -g +# All backends are shared libraries CFLAGS += -fPIC -I../ -Wall -Wpedantic CPPFLAGS += -fPIC -I../ LDFLAGS += -shared @@ -25,6 +34,7 @@ ifeq ($(SYSTEM),Darwin) LDFLAGS += -undefined dynamic_lookup endif +# Most of these next few backends just pull in the backend lib, some set additional flags artnet.so: ADDITIONAL_OBJS += $(BACKEND_LIB) artnet.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) artnet.dll: LDLIBS += -lws2_32 @@ -37,6 +47,10 @@ sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: LDLIBS += -lws2_32 +visca.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +visca.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +visca.dll: LDLIBS += -lws2_32 + openpixelcontrol.so: ADDITIONAL_OBJS += $(BACKEND_LIB) openpixelcontrol.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) openpixelcontrol.dll: LDLIBS += -lws2_32 @@ -73,6 +87,7 @@ lua.dll: LDLIBS += -L../ -llua53 python.so: CFLAGS += $(shell pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") python.so: CFLAGS += $(shell pkg-config --libs python3 || pkg-config --libs python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +# Generic rules on how to build .SO/.DLL's from C and CPP sources %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) @@ -82,11 +97,14 @@ python.so: CFLAGS += $(shell pkg-config --libs python3 || pkg-config --libs pyth %.so :: %.cpp %.h $(CXX) $(CPPFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) +# This is the actual first named target, and thus the default all: $(BACKEND_LIB) $(BACKENDS) +# Build an import lib for the windows build if it's not already there ../libmmapi.a: $(MAKE) -C ../ midimonster.exe +# Override a bunch of stuff for the windows target and it's DLL dependencies %.dll: export CC = x86_64-w64-mingw32-gcc %.dll: LDLIBS += -lmmapi %.dll: LDFLAGS += -L../ @@ -95,7 +113,9 @@ windows: CFLAGS += -Wno-format -Wno-pointer-sign windows: export CC = x86_64-w64-mingw32-gcc windows: ../libmmapi.a $(BACKEND_LIB) $(WINDOWS_BACKENDS) +# Optional target including the backends that require large dependencies full: $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) +# Clean up all generated files clean: $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) $(WINDOWS_BACKENDS) $(MANUAL_BACKENDS) -- cgit v1.2.3 From 15826f63185211ff3974b29370d04b8082be9c53 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 15 Aug 2020 13:45:23 +0200 Subject: Add VISCA documentation, fix some issues --- backends/visca.c | 30 +++++++++++++++++++++++++----- backends/visca.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 backends/visca.md diff --git a/backends/visca.c b/backends/visca.c index f8bdae1..ae586a7 100644 --- a/backends/visca.c +++ b/backends/visca.c @@ -44,7 +44,7 @@ static int ptz_configure_instance(instance* inst, char* option, char* value){ data->cam_address = strtoul(value, NULL, 10); return 0; } - if(!strcmp(option, "connect")){ + else if(!strcmp(option, "connect")){ if(data->fd >= 0){ LOGPF("Instance %s already connected", inst->name); return 1; @@ -67,6 +67,24 @@ static int ptz_configure_instance(instance* inst, char* option, char* value){ } return 0; } + else if(!strcmp(option, "device")){ + if(data->fd >= 0){ + LOGPF("Instance %s already connected", inst->name); + return 1; + } + + #ifdef _WIN32 + LOGPF("Direct device connections are not possible on Windows"); + return 1; + #else + data->fd = open(value, O_NONBLOCK); + if(data->fd < 0){ + LOGPF("Failed to connect instance %s to device %s", inst->name, value); + return 1; + } + return 0; + #endif + } LOGPF("Unknown instance configuration parameter %s for instance %s", option, inst->name); return 1; @@ -109,6 +127,7 @@ static channel* ptz_channel(instance* inst, char* spec, uint8_t flags){ return NULL; } + //store the memory to be called above the command type if(ident == call){ ident |= (strtoul(spec + strlen(ptz_channels[call].name), NULL, 10) << 8); } @@ -118,8 +137,8 @@ static channel* ptz_channel(instance* inst, char* spec, uint8_t flags){ static size_t ptz_set_pantilt(instance* inst, channel* c, channel_value* v, uint8_t* msg){ ptz_instance_data* data = (ptz_instance_data*) inst->impl; - uint32_t* x = (uint32_t*) msg + 6; - uint32_t* y = (uint32_t*) msg + 10; + uint32_t* x = (uint32_t*) (msg + 6); + uint32_t* y = (uint32_t*) (msg + 10); if(c->ident == pan){ data->x = ((ptz_channels[pan].max - ptz_channels[pan].min) * v->normalised) + ptz_channels[pan].min; @@ -144,17 +163,18 @@ static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint else{ data->tiltspeed = ((ptz_channels[tiltspeed].max - ptz_channels[tiltspeed].min) * v->normalised) + ptz_channels[tiltspeed].min; } + return 0; } static size_t ptz_set_zoom(instance* inst, channel* c, channel_value* v, uint8_t* msg){ - uint32_t* position = (uint32_t*) msg + 4; + uint32_t* position = (uint32_t*) (msg + 4); *position = htobe32(((ptz_channels[zoom].max - ptz_channels[zoom].min) * v->normalised) + ptz_channels[zoom].min); return ptz_channels[zoom].bytes; } static size_t ptz_set_focus(instance* inst, channel* c, channel_value* v, uint8_t* msg){ - uint32_t* position = (uint32_t*) msg + 4; + uint32_t* position = (uint32_t*) (msg + 4); *position = htobe32(((ptz_channels[focus].max - ptz_channels[focus].min) * v->normalised) + ptz_channels[focus].min); return ptz_channels[focus].bytes; } diff --git a/backends/visca.md b/backends/visca.md new file mode 100644 index 0000000..26e523a --- /dev/null +++ b/backends/visca.md @@ -0,0 +1,50 @@ +### The `visca` backend + +The VISCA backend provides control of compatible PTZ (Pan, Tilt, Zoom) controllable cameras +via the network. This protocol has, with some variations, been implemented by multiple manufacturers +in their camera equipment. There may be some specific limits on the command set depending on make +and model of your equpipment. + +This backend can connect to both UDP and TCP based cameras. + +#### Global configuration + +The `visca` backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|---------------------------------------------------------------| +| `id` | `5` | `1` | VISCA Camera address (normally 1 for network communication | +| `connect` | `10.10.10.1 5678` | none | Camera network address and port. Default connection is TCP, when optionally suffixed with the `udp` keyword, connection will be UDP | +| `device` | `/dev/ttyUSB0` | none | (Linux only) Device node for a serial port adapter connecting to the camera | + +#### Channel specification + +Each instance exposes the following channels + +* `pan`: Pan axis +* `tilt`: Tilt axis +* `panspeed`: Pan speed +* `tiltspeed`: Tilt speed +* `zoom`: Zoom position +* `focus`: Focus position +* `memory`: Call memory (if incoming event value is greater than 0.9) + +Example mappings: + +``` +control.pan > visca.pan +control.tilt > visca.tilt +control.btn1 > visca.memory1 +``` + +#### Known bugs / problems + +Value readback / Inquiry is not yet implemented. This backend currently only does output. + +Some manufacturers use VISCA, but require special framing for command flow control. This may be implemented +in the future if there is sufficient interest. + +Please file a ticket if you can confirm this backend working/nonworking with a new make or model +of camera so we can add it to the compatability list! -- cgit v1.2.3 From 5bf8fcd4cd4e5ccabb46e830dffa19fd6f7a2f11 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 15 Aug 2020 22:19:22 +0200 Subject: Update visca documentation --- backends/visca.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backends/visca.md b/backends/visca.md index 26e523a..6d978e2 100644 --- a/backends/visca.md +++ b/backends/visca.md @@ -1,11 +1,11 @@ ### The `visca` backend -The VISCA backend provides control of compatible PTZ (Pan, Tilt, Zoom) controllable cameras -via the network. This protocol has, with some variations, been implemented by multiple manufacturers -in their camera equipment. There may be some specific limits on the command set depending on make -and model of your equpipment. +The `visca` backend provides control of compatible PTZ (Pan, Tilt, Zoom) controllable cameras +via the network. The VISCA protocol has, with some variations, been implemented by multiple manufacturers +in their camera equipment. There may be some specific limits on the command set depending on the make +and model of your equipment. -This backend can connect to both UDP and TCP based cameras. +This backend can connect to both UDP and TCP based camera control interfaces. #### Global configuration -- cgit v1.2.3 From ca861ca42bdbf0e8cf14737e81cd70665c5b557d Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 16 Aug 2020 19:28:48 +0200 Subject: Fix CI returns --- .ci.sh | 9 ++++++--- backends/evdev.c | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.ci.sh b/.ci.sh index 087bf11..4a646a9 100755 --- a/.ci.sh +++ b/.ci.sh @@ -78,13 +78,15 @@ elif [ "$TASK" = "sanitize" ]; then # Run sanitized compile ci_fold start "make_sanitize" if ! make sanitize; then - exit "$?" + printf "Failed to build\n" + exit 1 fi ci_fold end "make_sanitize" elif [ "$TASK" = "windows" ]; then ci_fold start "make_windows" if ! make windows; then - exit "$?" + printf "Failed to build\n" + exit 1 fi make -C backends lua.dll ci_fold end "make_windows" @@ -110,7 +112,8 @@ else # Otherwise compile as normal ci_fold start "make" if ! make full; then - exit "$?" + printf "Failed to build\n" + exit 1 fi ci_fold end "make" if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then diff --git a/backends/evdev.c b/backends/evdev.c index 8a14200..4c734f9 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -206,6 +206,7 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { else if(data->relative_axis[data->relative_axes].max == 0){ LOGPF("Relative axis configuration for %s.%s has invalid range", inst->name, option + 8); } + //this does not crash on single-integer `value`s because strtoll sets `next_token` to the terminator data->relative_axis[data->relative_axes].current = strtoul(next_token, NULL, 0); if(data->relative_axis[data->relative_axes].code < 0){ LOGPF("Failed to configure relative axis extents for %s.%s", inst->name, option + 8); -- cgit v1.2.3 From 4d7db4128985229ebdbaae66be25b2710360f464 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 16 Aug 2020 19:37:48 +0200 Subject: Fix VISCA windows build --- backends/visca.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/visca.c b/backends/visca.c index ae586a7..619211d 100644 --- a/backends/visca.c +++ b/backends/visca.c @@ -74,7 +74,7 @@ static int ptz_configure_instance(instance* inst, char* option, char* value){ } #ifdef _WIN32 - LOGPF("Direct device connections are not possible on Windows"); + LOG("Direct device connections are not possible on Windows"); return 1; #else data->fd = open(value, O_NONBLOCK); -- cgit v1.2.3 From e131992bbe0893a3e5b79cf9423830ea90b4a4d7 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 18 Aug 2020 06:37:01 +0200 Subject: Implement wininput mouse wheel control (Fixes #65) --- backends/wininput.c | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++-- backends/wininput.h | 2 +- backends/wininput.md | 15 ++++++++++++++ config.c | 2 +- 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/backends/wininput.c b/backends/wininput.c index 8926782..6d21a76 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -65,9 +65,13 @@ static struct { //sorted in _start wininput_request* request; uint32_t interval; + uint64_t wheel, wheel_max, wheel_delta; + uint8_t wheel_inverted; } cfg = { .requests = 0, - .interval = 50 + .interval = 50, + .wheel_max = 0xFFFF, + .wheel_delta = 1 }; MM_PLUGIN_API int init(){ @@ -120,10 +124,38 @@ static uint32_t wininput_interval(){ } static int wininput_configure(char* option, char* value){ + int64_t parameter = 0; + char* next_token = NULL; + if(!strcmp(option, "interval")){ cfg.interval = strtoul(value, NULL, 0); return 0; } + else if(!strcmp(option, "wheel")){ + parameter = strtoll(value, &next_token, 0); + + cfg.wheel_max = parameter; + if(parameter < 0){ + LOG("Inverting mouse wheel data"); + cfg.wheel_max = -parameter; + cfg.wheel_inverted = 1; + } + else if(!parameter){ + LOGPF("Invalid mouse wheel configuration %s", value); + return 1; + } + + if(next_token && *next_token){ + cfg.wheel = strtoul(next_token, NULL, 0); + } + + return 0; + } + else if(!strcmp(option, "wheeldelta")){ + cfg.wheel_delta = strtoul(value, NULL, 0); + return 0; + } + LOGPF("Unknown backend configuration option %s", option); return 1; @@ -196,6 +228,13 @@ static uint64_t wininput_channel_mouse(instance* inst, char* spec, uint8_t flags ident.fields.channel = position; ident.fields.control = 1; } + else if(!strcmp(spec, "wheel")){ + ident.fields.channel = wheel; + if(flags & mmchannel_input){ + LOG("The mouse wheel can only be used as an output channel"); + return 0; + } + } else{ //check the buttons for(u = 0; u < sizeof(keys) / sizeof(keys[0]); u++){ @@ -354,7 +393,7 @@ static INPUT wininput_event_mouse(uint8_t channel, uint8_t control, double value ev.mi.dx = cfg.mouse_x; ev.mi.dy = cfg.mouse_y; } - if(channel == button){ + else if(channel == button){ switch(control){ case VK_LBUTTON: flags_up |= MOUSEEVENTF_LEFTUP; @@ -383,6 +422,15 @@ static INPUT wininput_event_mouse(uint8_t channel, uint8_t control, double value ev.mi.dwFlags |= flags_up; } } + else if(channel == wheel){ + ev.mi.dwFlags |= MOUSEEVENTF_WHEEL; + ev.mi.mouseData = ((value * cfg.wheel_max) - cfg.wheel) * cfg.wheel_delta; + if(cfg.wheel_inverted){ + ev.mi.mouseData *= -1; + } + DBGPF("Moving wheel %d (invert %d) with delta %d: %d", (value * cfg.wheel_max) - cfg.wheel, cfg.wheel_inverted, cfg.wheel_delta, ev.mi.mouseData); + cfg.wheel = (value * cfg.wheel_max); + } return ev; } @@ -479,6 +527,10 @@ static int wininput_handle(size_t num, managed_fd* fds){ push_event = 1; } } + else if(cfg.request[u].ident.fields.type == mouse + && cfg.request[u].ident.fields.channel == wheel){ + //ignore wheel requests, can't read that + } else if(cfg.request[u].ident.fields.type == keyboard || cfg.request[u].ident.fields.type == mouse){ //check key state diff --git a/backends/wininput.h b/backends/wininput.h index 6ec9f46..0939cc3 100644 --- a/backends/wininput.h +++ b/backends/wininput.h @@ -22,7 +22,7 @@ enum /*wininput_control_channel*/ { keypress = 0, button, position, - //wheel, /*relative*/ + wheel, key_unicode }; diff --git a/backends/wininput.md b/backends/wininput.md index bcf6a1b..87e9321 100644 --- a/backends/wininput.md +++ b/backends/wininput.md @@ -11,6 +11,12 @@ access (as is available under Linux) is possible. This backend does not take any global configuration. +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|---------------------------------------| +| `interval` | `100` | `50` | Data polling interval in milliseconds. Lower intervals lead to higher CPU load. This value should normally not be changed. | +| `wheel` | `4000 2000` | `65535 0` | Mouse wheel range and optional initial value. To invert the mouse wheel control, specify the range as a negative integer. As the mouse wheel is a relative control, we need to specify a range incoming absolute values are mapped to. This can be used control the wheel resolution and travel size. | +| `wheeldelta` | `20` | `1` | Multiplier for wheel travel | + #### Instance configuration This backend does not take any instance-specific configuration. @@ -30,6 +36,11 @@ as well as one channel per mouse button * `mouse.xmb1`: Extra mouse button 1 * `mouse.xmb2`: Extra mouse button 2 +The (vertical) mouse wheel can be controlled from the MIDIMonster using the `mouse.wheel` channel, but it can not be used +as an input channel due to limitations in the Windows API. All instances share one wheel control (see the section on known +bugs below). The mouse wheel sensitivity can be controlled by adjusting the absolute travel range, its initial value and +a wheel delta multiplier. + All keys that have an [assigned virtual keycode](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes) are mappable as MIDIMonster channels using the syntax `key.`, with *keyname* being one of the following specifiers: @@ -120,3 +131,7 @@ Some antivirus applications may detect this backend as problematic because it us interfaces to read keyboard and mouse input as any malicious application would. While it is definitely possible to configure the MIDIMonster to do malicious things, the code itself does not log anything. You can verify this by reading the backend code yourself. + +Since the Windows input system merges all keyboard/mouse input data into one data stream, using multiple +instances of this backend is not necessary or useful. It is still supported for technical reasons. +There may be unexpected side effects when mapping the mouse wheel in multiple instances. diff --git a/config.c b/config.c index 9945319..c1c3124 100644 --- a/config.c +++ b/config.c @@ -545,7 +545,7 @@ static int config_line(char* line){ //find separator separator = strchr(line, '='); if(!separator){ - fprintf(stderr, "Not an assignment: %s\n", line); + fprintf(stderr, "Not an assignment (currently expecting %s configuration): %s\n", line, (parser_state == backend_cfg) ? "backend" : "instance"); return 1; } -- cgit v1.2.3 From 436bcf59c7dcf4acf42f5f812d0e6c4839326757 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 18 Aug 2020 06:40:01 +0200 Subject: Update wininput documentation --- backends/wininput.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/backends/wininput.md b/backends/wininput.md index 87e9321..bffa000 100644 --- a/backends/wininput.md +++ b/backends/wininput.md @@ -9,8 +9,6 @@ access (as is available under Linux) is possible. #### Global configuration -This backend does not take any global configuration. - | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|---------------------------------------| | `interval` | `100` | `50` | Data polling interval in milliseconds. Lower intervals lead to higher CPU load. This value should normally not be changed. | -- cgit v1.2.3 From dc7303cc07565a725eec19aeff46f64ad4275c21 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 19 Aug 2020 12:46:31 +0200 Subject: wininput sanity check and minor documentation update --- backends/wininput.c | 5 +++++ backends/wininput.md | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backends/wininput.c b/backends/wininput.c index 6d21a76..1d1c85b 100644 --- a/backends/wininput.c +++ b/backends/wininput.c @@ -149,6 +149,11 @@ static int wininput_configure(char* option, char* value){ cfg.wheel = strtoul(next_token, NULL, 0); } + if(cfg.wheel > cfg.wheel_max){ + LOG("Mouse wheel initial value out of range"); + return 1; + } + return 0; } else if(!strcmp(option, "wheeldelta")){ diff --git a/backends/wininput.md b/backends/wininput.md index bffa000..797d879 100644 --- a/backends/wininput.md +++ b/backends/wininput.md @@ -12,7 +12,7 @@ access (as is available under Linux) is possible. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|---------------------------------------| | `interval` | `100` | `50` | Data polling interval in milliseconds. Lower intervals lead to higher CPU load. This value should normally not be changed. | -| `wheel` | `4000 2000` | `65535 0` | Mouse wheel range and optional initial value. To invert the mouse wheel control, specify the range as a negative integer. As the mouse wheel is a relative control, we need to specify a range incoming absolute values are mapped to. This can be used control the wheel resolution and travel size. | +| `wheel` | `-4000 2000` | `65535 0` | Mouse wheel range and optional initial value. To invert the mouse wheel control, specify the range as a negative integer. As the mouse wheel is a relative control, we need to specify a range incoming absolute values are mapped to. This can be used control the wheel resolution and travel size. | | `wheeldelta` | `20` | `1` | Multiplier for wheel travel | #### Instance configuration @@ -35,11 +35,11 @@ as well as one channel per mouse button * `mouse.xmb2`: Extra mouse button 2 The (vertical) mouse wheel can be controlled from the MIDIMonster using the `mouse.wheel` channel, but it can not be used -as an input channel due to limitations in the Windows API. All instances share one wheel control (see the section on known +as an input channel due to limitations in the Windows API. All instances share one `wheel` control (see the section on known bugs below). The mouse wheel sensitivity can be controlled by adjusting the absolute travel range, its initial value and a wheel delta multiplier. -All keys that have an [assigned virtual keycode](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes) +All keyboard keys that have an [assigned virtual keycode](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes) are mappable as MIDIMonster channels using the syntax `key.`, with *keyname* being one of the following specifiers: * One of the keynames listed below (e.g., `key.enter`) -- cgit v1.2.3 From fec26ba3f193f3e31e08b55dfb3c60aa30eed63f Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 6 Sep 2020 22:45:20 +0200 Subject: Add rate limiting notice to sACN and ArtNet --- TODO | 1 + backend.c | 6 ++++-- backends/artnet.md | 3 +++ backends/sacn.md | 6 +++++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/TODO b/TODO index befa5e6..44336d4 100644 --- a/TODO +++ b/TODO @@ -4,3 +4,4 @@ udp backends may ignore MTU make event collectors threadsafe to stop marshalling data... collect & check backend API version move all connection establishment to _start to be able to hot-stop/start all backends +event deduplication in core? diff --git a/backend.c b/backend.c index 0fb6679..16e095c 100644 --- a/backend.c +++ b/backend.c @@ -94,16 +94,18 @@ 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); 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); if(channels.entry[bucket][u]->instance == inst && channels.entry[bucket][u]->ident == ident){ - DBGPF("Requested channel %" PRIu64 " on instance %s already exists, reusing (%" PRIsize_t " search steps)\n", ident, inst->name, u); + DBGPF("Requested channel %" PRIu64 " on instance %s already exists, reusing (bucket %" PRIsize_t ", %" PRIsize_t " search steps)\n", ident, inst->name, bucket, u); return channels.entry[bucket][u]; } } if(!create){ - DBGPF("Requested unknown channel %" PRIu64 " on instance %s\n", ident, inst->name); + DBGPF("Requested unknown channel %" PRIu64 " (bucket %" PRIsize_t ") on instance %s\n", ident, bucket, inst->name); return NULL; } diff --git a/backends/artnet.md b/backends/artnet.md index 73f598a..bd26014 100644 --- a/backends/artnet.md +++ b/backends/artnet.md @@ -39,3 +39,6 @@ net1.1+2 > net2.5+123 A normal channel that is part of a wide channel can not be mapped individually. #### Known bugs / problems + +When using this backend for output with a fast event source, some events may appear to be lost due to the packet output rate limiting +mandated by the [ArtNet specification](https://artisticlicence.com/WebSiteMaster/User%20Guides/art-net.pdf) (Section `Refresh rate`). diff --git a/backends/sacn.md b/backends/sacn.md index b7686e0..244b4c4 100644 --- a/backends/sacn.md +++ b/backends/sacn.md @@ -11,7 +11,7 @@ containing all write-enabled universes. | `name` | `sACN source` | `MIDIMonster` | sACN source name | | `cid` | `0xAA 0xBB 0xCC` ... | `MIDIMonster` | Source CID (16 bytes) | | `bind` | `0.0.0.0 5568` | none | Binds a network address to listen for data. This option may be set multiple times, with each descriptor being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one descriptor is required for operation. | -| `detect` | `on`, `verbose` | `off` | Output additional information on received data packets to help with configuring complex scenarios. | +| `detect` | `on`, `verbose` | `off` | Output additional information on received data packets to help with configuring complex scenarios | The `bind` configuration value can be extended by the keyword `local` to allow software on the local host to process the sACN output frames from the MIDIMonster (e.g. `bind = 0.0.0.0 5568 local`). @@ -59,3 +59,7 @@ To use multicast input, all networking hardware in the path must support the IGM The Linux kernel limits the number of multicast groups an interface may join to 20. An instance configured for input automatically joins the multicast group for its universe, unless configured in `unicast` mode. This limit can be raised by changing the kernel option in `/proc/sys/net/ipv4/igmp_max_memberships`. + +When using this backend for output with a fast event source, some events may appear to be lost due to the packet output rate limiting +mandated by the E1.31 specification (Section `6.6.1 Transmission rate`). +The rate limiter can be disabled on a per-instance basis using the `realtime` option. -- cgit v1.2.3 From 9f2deded624e1a934b52ac0d4d33e3612a7fb469 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 12 Sep 2020 19:34:26 +0200 Subject: Fix VISCA dialect --- backends/visca.c | 88 ++++++++++++++++++++++++++++++++++++++++++++----------- backends/visca.h | 24 ++++++++------- backends/visca.md | 1 + 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/backends/visca.c b/backends/visca.c index 619211d..d408b80 100644 --- a/backends/visca.c +++ b/backends/visca.c @@ -7,6 +7,7 @@ /* TODO * VISCA server + * Rate limiting */ MM_PLUGIN_API int init(){ @@ -128,8 +129,8 @@ static channel* ptz_channel(instance* inst, char* spec, uint8_t flags){ } //store the memory to be called above the command type - if(ident == call){ - ident |= (strtoul(spec + strlen(ptz_channels[call].name), NULL, 10) << 8); + if(ident == call || ident == store){ + ident |= (strtoul(spec + strlen(ptz_channels[ident].name), NULL, 10) << 8); } return mm_channel(inst, ident, 1); @@ -137,20 +138,27 @@ static channel* ptz_channel(instance* inst, char* spec, uint8_t flags){ static size_t ptz_set_pantilt(instance* inst, channel* c, channel_value* v, uint8_t* msg){ ptz_instance_data* data = (ptz_instance_data*) inst->impl; - uint32_t* x = (uint32_t*) (msg + 6); - uint32_t* y = (uint32_t*) (msg + 10); - + if(c->ident == pan){ - data->x = ((ptz_channels[pan].max - ptz_channels[pan].min) * v->normalised) + ptz_channels[pan].min; + data->x = ((ptz_channels[pan].max - ptz_channels[pan].min) * v->normalised) + ptz_channels[pan].min - ptz_channels[pan].offset; } else{ - data->y = ((ptz_channels[tilt].max - ptz_channels[tilt].min) * v->normalised) + ptz_channels[tilt].min; + data->y = ((ptz_channels[tilt].max - ptz_channels[tilt].min) * v->normalised) + ptz_channels[tilt].min - ptz_channels[tilt].offset; } msg[4] = data->panspeed; msg[5] = data->tiltspeed; - *x = htobe32(data->x); - *y = htobe32(data->y); + + //either i'm doing this wrong or visca is just weird. + msg[6] = ((data->x & 0xF000) >> 12); + msg[7] = ((data->x & 0x0F00) >> 8); + msg[8] = ((data->x & 0xF0) >> 4); + msg[9] = (data->x & 0x0F); + + msg[10] = ((data->y & 0xF000) >> 12); + msg[11] = ((data->y & 0x0F00) >> 8); + msg[12] = ((data->y & 0xF0) >> 4); + msg[13] = (data->y & 0x0F); return ptz_channels[pan].bytes; } @@ -158,24 +166,30 @@ static size_t ptz_set_pantilt(instance* inst, channel* c, channel_value* v, uint static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint8_t* msg){ ptz_instance_data* data = (ptz_instance_data*) inst->impl; if(c->ident == panspeed){ - data->panspeed = ((ptz_channels[panspeed].max - ptz_channels[panspeed].min) * v->normalised) + ptz_channels[panspeed].min; + data->panspeed = ((ptz_channels[panspeed].max - ptz_channels[panspeed].min) * v->normalised) + ptz_channels[panspeed].min - ptz_channels[panspeed].offset; } else{ - data->tiltspeed = ((ptz_channels[tiltspeed].max - ptz_channels[tiltspeed].min) * v->normalised) + ptz_channels[tiltspeed].min; + data->tiltspeed = ((ptz_channels[tiltspeed].max - ptz_channels[tiltspeed].min) * v->normalised) + ptz_channels[tiltspeed].min - ptz_channels[tiltspeed].offset; } return 0; } static size_t ptz_set_zoom(instance* inst, channel* c, channel_value* v, uint8_t* msg){ - uint32_t* position = (uint32_t*) (msg + 4); - *position = htobe32(((ptz_channels[zoom].max - ptz_channels[zoom].min) * v->normalised) + ptz_channels[zoom].min); + uint16_t position = ((ptz_channels[zoom].max - ptz_channels[zoom].min) * v->normalised) + ptz_channels[zoom].min - ptz_channels[zoom].offset; + msg[4] = ((position & 0xF000) >> 12); + msg[5] = ((position & 0x0F00) >> 8); + msg[6] = ((position & 0xF0) >> 4); + msg[7] = (position & 0x0F); return ptz_channels[zoom].bytes; } static size_t ptz_set_focus(instance* inst, channel* c, channel_value* v, uint8_t* msg){ - uint32_t* position = (uint32_t*) (msg + 4); - *position = htobe32(((ptz_channels[focus].max - ptz_channels[focus].min) * v->normalised) + ptz_channels[focus].min); + uint16_t position = ((ptz_channels[focus].max - ptz_channels[focus].min) * v->normalised) + ptz_channels[focus].min - ptz_channels[focus].offset; + msg[4] = ((position & 0xF000) >> 12); + msg[5] = ((position & 0x0F00) >> 8); + msg[6] = ((position & 0xF0) >> 4); + msg[7] = (position & 0x0F); return ptz_channels[focus].bytes; } @@ -188,6 +202,15 @@ static size_t ptz_set_memory(instance* inst, channel* c, channel_value* v, uint8 return ptz_channels[call].bytes; } +static size_t ptz_set_memory_store(instance* inst, channel* c, channel_value* v, uint8_t* msg){ + if(v->normalised < 0.9){ + return 0; + } + + msg[5] = (c->ident >> 8); + return ptz_channels[store].bytes; +} + static int ptz_set(instance* inst, size_t num, channel** c, channel_value* v){ ptz_instance_data* data = (ptz_instance_data*) inst->impl; size_t n = 0, bytes = 0; @@ -215,12 +238,43 @@ static int ptz_set(instance* inst, size_t num, channel** c, channel_value* v){ } static int ptz_handle(size_t num, managed_fd* fds){ - //no events generated here + uint8_t recv_buf[VISCA_BUFFER_LENGTH]; + size_t u; + ssize_t bytes_read; + instance* inst = NULL; + + //read and ignore any responses for now + for(u = 0; u < num; u++){ + inst = (instance*) fds[u].impl; + bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); + if(bytes_read <= 0){ + LOGPF("Failed to receive on signaled fd for instance %s", inst->name); + //TODO handle failure + } + else{ + DBGPF("Ignored %" PRIsize_t " incoming bytes for instance %s", bytes_read, inst->name); + } + } + return 0; } static int ptz_start(size_t n, instance** inst){ - //no startup needed yet + size_t u, fds = 0; + ptz_instance_data* data = NULL; + + for(u = 0; u < n; u++){ + data = (ptz_instance_data*) inst[u]->impl; + if(data->fd >= 0){ + if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u])){ + LOGPF("Failed to register descriptor for instance %s", inst[u]->name); + return 1; + } + fds++; + } + } + + LOGPF("Registered %" PRIsize_t " descriptors to core", fds); return 0; } diff --git a/backends/visca.h b/backends/visca.h index 54c80f7..09c9057 100644 --- a/backends/visca.h +++ b/backends/visca.h @@ -15,8 +15,8 @@ static int ptz_shutdown(size_t n, instance** inst); typedef struct /*_ptz_instance_data*/ { int fd; uint8_t cam_address; - uint32_t x; - uint32_t y; + uint16_t x; + uint16_t y; uint8_t panspeed; uint8_t tiltspeed; } ptz_instance_data; @@ -29,6 +29,7 @@ enum /*ptz_channels*/ { zoom, focus, call, + store, sentinel }; @@ -38,20 +39,23 @@ static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint static size_t ptz_set_zoom(instance* inst, channel* c, channel_value* v, uint8_t* msg); static size_t ptz_set_focus(instance* inst, channel* c, channel_value* v, uint8_t* msg); static size_t ptz_set_memory(instance* inst, channel* c, channel_value* v, uint8_t* msg); +static size_t ptz_set_memory_store(instance* inst, channel* c, channel_value* v, uint8_t* msg); static struct { char* name; size_t bytes; uint8_t pattern[VISCA_BUFFER_LENGTH]; - size_t min; + size_t min; //channel range = max - min size_t max; + size_t offset; //channel value = normalised * range - offset ptz_channel_set set; } ptz_channels[] = { - [pan] = {"pan", 14, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 4000, ptz_set_pantilt}, - [tilt] = {"tilt", 14, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 4000, ptz_set_pantilt}, - [panspeed] = {"panspeed", 0, {0}, 0x01, 0x18, ptz_set_ptspeed}, - [tiltspeed] = {"tiltspeed", 0, {0}, 0x01, 0x14, ptz_set_ptspeed}, - [zoom] = {"zoom", 9, {0x80, 0x01, 0x04, 0x47, 0, 0, 0, 0, 0xFF}, 0, 4000, ptz_set_zoom}, - [focus] = {"focus", 9, {0x80, 0x01, 0x04, 0x48, 0, 0, 0, 0, 0xFF}, 0, 4000, ptz_set_focus}, - [call] = {"memory", 7, {0x80, 0x01, 0x04, 0x3F, 0x02, 0, 0xFF}, 0, 254, ptz_set_memory} + [pan] = {"pan", 15, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 0x990 * 2, 0x990, ptz_set_pantilt}, + [tilt] = {"tilt", 15, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 0x510 * 2, 0x510, ptz_set_pantilt}, + [panspeed] = {"panspeed", 0, {0}, 0x01, 0x18, 0, ptz_set_ptspeed}, + [tiltspeed] = {"tiltspeed", 0, {0}, 0x01, 0x14, 0, ptz_set_ptspeed}, + [zoom] = {"zoom", 9, {0x80, 0x01, 0x04, 0x47, 0, 0, 0, 0, 0xFF}, 0, 0x4000, 0, ptz_set_zoom}, + [focus] = {"focus", 9, {0x80, 0x01, 0x04, 0x48, 0, 0, 0, 0, 0xFF}, 0, 0x4000, 0, ptz_set_focus}, + [call] = {"memory", 7, {0x80, 0x01, 0x04, 0x3F, 0x02, 0, 0xFF}, 0, 254, 0, ptz_set_memory}, + [store] = {"store", 7, {0x80, 0x01, 0x04, 0x3F, 0x01, 0, 0xFF}, 0, 254, 0, ptz_set_memory_store} }; diff --git a/backends/visca.md b/backends/visca.md index 6d978e2..4fe1933 100644 --- a/backends/visca.md +++ b/backends/visca.md @@ -30,6 +30,7 @@ Each instance exposes the following channels * `zoom`: Zoom position * `focus`: Focus position * `memory`: Call memory (if incoming event value is greater than 0.9) +* `store`: Store current pan/tilt/zoom setup to memory (if incoming event value is greater than 0.9) Example mappings: -- cgit v1.2.3 From d45ca2422fd5bedf48d68a3a537bae924b0cbae7 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 18 Sep 2020 20:12:53 +0200 Subject: Implement additional VISCA channels --- backends/visca.c | 28 ++++++++++++++++++++++++++++ backends/visca.h | 21 ++++++++++++++++++++- backends/visca.md | 16 ++++++++++++++-- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/backends/visca.c b/backends/visca.c index d408b80..32f11c9 100644 --- a/backends/visca.c +++ b/backends/visca.c @@ -8,6 +8,7 @@ /* TODO * VISCA server * Rate limiting + * Inquiry */ MM_PLUGIN_API int init(){ @@ -175,6 +176,13 @@ static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint return 0; } +static size_t ptz_set_stop(instance* inst, channel* c, channel_value* v, uint8_t* msg){ + ptz_instance_data* data = (ptz_instance_data*) inst->impl; + msg[4] = data->panspeed; + msg[5] = data->tiltspeed; + return ptz_channels[stop].bytes; +} + static size_t ptz_set_zoom(instance* inst, channel* c, channel_value* v, uint8_t* msg){ uint16_t position = ((ptz_channels[zoom].max - ptz_channels[zoom].min) * v->normalised) + ptz_channels[zoom].min - ptz_channels[zoom].offset; msg[4] = ((position & 0xF000) >> 12); @@ -193,6 +201,24 @@ static size_t ptz_set_focus(instance* inst, channel* c, channel_value* v, uint8_ return ptz_channels[focus].bytes; } +static size_t ptz_set_focus_mode(instance* inst, channel* c, channel_value* v, uint8_t* msg){ + msg[4] = (v->normalised > 0.9) ? 2 : 3; + return ptz_channels[focus_mode].bytes; +} + +static size_t ptz_set_wb_mode(instance* inst, channel* c, channel_value* v, uint8_t* msg){ + msg[4] = (v->normalised > 0.9) ? 0 : 5; + return ptz_channels[wb_mode].bytes; +} + +static size_t ptz_set_wb(instance* inst, channel* c, channel_value* v, uint8_t* msg){ + uint8_t command = c->ident & 0xFF; + uint8_t value = ((ptz_channels[command].max - ptz_channels[command].min) * v->normalised) + ptz_channels[command].min - ptz_channels[command].offset; + msg[6] = value >> 4; + msg[7] = value & 0x0F; + return ptz_channels[command].bytes; +} + static size_t ptz_set_memory(instance* inst, channel* c, channel_value* v, uint8_t* msg){ if(v->normalised < 0.9){ return 0; @@ -223,6 +249,8 @@ static int ptz_set(instance* inst, size_t num, channel** c, channel_value* v){ if(ptz_channels[command].bytes){ memcpy(tx, ptz_channels[command].pattern, ptz_channels[command].bytes); + //if no handler function set, assume a parameterless command and send verbatim + bytes = ptz_channels[command].bytes; } tx[0] = 0x80 | (data->cam_address & 0xF); diff --git a/backends/visca.h b/backends/visca.h index 09c9057..481f5c7 100644 --- a/backends/visca.h +++ b/backends/visca.h @@ -28,8 +28,14 @@ enum /*ptz_channels*/ { tiltspeed, zoom, focus, + focus_mode, + wb_red, + wb_blue, + wb_mode, call, store, + home, + stop, sentinel }; @@ -38,9 +44,15 @@ static size_t ptz_set_pantilt(instance* inst, channel* c, channel_value* v, uint static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint8_t* msg); static size_t ptz_set_zoom(instance* inst, channel* c, channel_value* v, uint8_t* msg); static size_t ptz_set_focus(instance* inst, channel* c, channel_value* v, uint8_t* msg); +static size_t ptz_set_focus_mode(instance* inst, channel* c, channel_value* v, uint8_t* msg); +static size_t ptz_set_wb_mode(instance* inst, channel* c, channel_value* v, uint8_t* msg); +static size_t ptz_set_wb(instance* inst, channel* c, channel_value* v, uint8_t* msg); static size_t ptz_set_memory(instance* inst, channel* c, channel_value* v, uint8_t* msg); static size_t ptz_set_memory_store(instance* inst, channel* c, channel_value* v, uint8_t* msg); +//relative move test +static size_t ptz_set_stop(instance* inst, channel* c, channel_value* v, uint8_t* msg); + static struct { char* name; size_t bytes; @@ -56,6 +68,13 @@ static struct { [tiltspeed] = {"tiltspeed", 0, {0}, 0x01, 0x14, 0, ptz_set_ptspeed}, [zoom] = {"zoom", 9, {0x80, 0x01, 0x04, 0x47, 0, 0, 0, 0, 0xFF}, 0, 0x4000, 0, ptz_set_zoom}, [focus] = {"focus", 9, {0x80, 0x01, 0x04, 0x48, 0, 0, 0, 0, 0xFF}, 0, 0x4000, 0, ptz_set_focus}, + [focus_mode] = {"autofocus", 6, {0x80, 0x01, 0x04, 0x38, 0, 0xFF}, 0, 1, 0, ptz_set_focus_mode}, + [wb_mode] = {"wb.auto", 6, {0x80, 0x01, 0x04, 0x35, 0, 0xFF}, 0, 1, 0, ptz_set_wb_mode}, + [wb_red] = {"wb.red", 9, {0x80, 0x01, 0x04, 0x43, 0x00, 0x00, 0, 0, 0xFF}, 0, 255, 0, ptz_set_wb}, + [wb_blue] = {"wb.blue", 9, {0x80, 0x01, 0x04, 0x44, 0x00, 0x00, 0, 0, 0xFF}, 0, 255, 0, ptz_set_wb}, [call] = {"memory", 7, {0x80, 0x01, 0x04, 0x3F, 0x02, 0, 0xFF}, 0, 254, 0, ptz_set_memory}, - [store] = {"store", 7, {0x80, 0x01, 0x04, 0x3F, 0x01, 0, 0xFF}, 0, 254, 0, ptz_set_memory_store} + [store] = {"store", 7, {0x80, 0x01, 0x04, 0x3F, 0x01, 0, 0xFF}, 0, 254, 0, ptz_set_memory_store}, + [home] = {"home", 5, {0x80, 0x01, 0x06, 0x04, 0xFF}, 0, 0, 0, NULL}, + //relative move test + [stop] = {"stop", 9, {0x80, 0x01, 0x06, 0x01, 0, 0, 0x03, 0x03, 0xFF}, 0, 0, 0, ptz_set_stop} }; diff --git a/backends/visca.md b/backends/visca.md index 4fe1933..d4fc432 100644 --- a/backends/visca.md +++ b/backends/visca.md @@ -29,6 +29,10 @@ Each instance exposes the following channels * `tiltspeed`: Tilt speed * `zoom`: Zoom position * `focus`: Focus position +* `autofocus`: Switch between autofocus (events > 0.9) and manual focus drive mode +* `wb.auto`: Switch between automatic white balance mode (events > 0.9) and manual white balance mode +* `wb.red`, `wb.blue`: Red/Blue channel white balance gain values +* `home`: Return to home position * `memory`: Call memory (if incoming event value is greater than 0.9) * `store`: Store current pan/tilt/zoom setup to memory (if incoming event value is greater than 0.9) @@ -40,12 +44,20 @@ control.tilt > visca.tilt control.btn1 > visca.memory1 ``` +#### Compatability list + +| Manufacturer | Exact model(s) tested | Compatible models | Result / Notes | +|---------------|-------------------------------|-----------------------------------------------|-------------------------------------------------------| +| ValueHD | VHD-V61 | Probably all ValueHD Visca-capable devices | Everything works except for absolute focus control | +| PTZOptics | | Probably all of their PTZ cameras | See ValueHD | + #### Known bugs / problems Value readback / Inquiry is not yet implemented. This backend currently only does output. Some manufacturers use VISCA, but require special framing for command flow control. This may be implemented -in the future if there is sufficient interest. +in the future if there is sufficient interest. Some commands may not work with some manufacturer's cameras due to +different value ranges or command ordering. Please file a ticket if you can confirm this backend working/nonworking with a new make or model -of camera so we can add it to the compatability list! +of camera so we can add it to the compatibility list! -- cgit v1.2.3 From e96ca85d8febf559817b43066ba16c8441ba391b Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 19 Sep 2020 00:20:11 +0200 Subject: Add local serial note for VISCA --- backends/visca.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backends/visca.md b/backends/visca.md index d4fc432..1dd516e 100644 --- a/backends/visca.md +++ b/backends/visca.md @@ -5,7 +5,8 @@ via the network. The VISCA protocol has, with some variations, been implemented in their camera equipment. There may be some specific limits on the command set depending on the make and model of your equipment. -This backend can connect to both UDP and TCP based camera control interfaces. +This backend can connect to both UDP and TCP based camera control interfaces. On Linux, it can also control +devices attached to a serial/RS485 adapter. #### Global configuration -- cgit v1.2.3 From 7d3bde27e412ebb6783c2c94f52a08f0e8d8ba49 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 19 Sep 2020 01:09:43 +0200 Subject: Fix VISCA channel parser --- backends/visca.c | 15 ++++++++------- backends/visca.h | 12 +++++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/backends/visca.c b/backends/visca.c index 32f11c9..8465c14 100644 --- a/backends/visca.c +++ b/backends/visca.c @@ -110,8 +110,7 @@ static int ptz_instance(instance* inst){ } static channel* ptz_channel(instance* inst, char* spec, uint8_t flags){ - uint64_t ident = pan; - size_t command = 0; + uint64_t command = 0; if(flags & mmchannel_input){ LOG("This backend currently only supports output channels"); @@ -120,21 +119,23 @@ static channel* ptz_channel(instance* inst, char* spec, uint8_t flags){ for(command = 0; command < sentinel; command++){ if(!strncmp(spec, ptz_channels[command].name, strlen(ptz_channels[command].name))){ - ident = command; + break; } } - if(ident == sentinel){ + DBGPF("Matched spec %s as %s", spec, ptz_channels[command].name ? ptz_channels[command].name : "sentinel"); + + if(command == sentinel){ LOGPF("Unknown channel spec %s", spec); return NULL; } //store the memory to be called above the command type - if(ident == call || ident == store){ - ident |= (strtoul(spec + strlen(ptz_channels[ident].name), NULL, 10) << 8); + if(command == call || command == store){ + command |= (strtoul(spec + strlen(ptz_channels[command].name), NULL, 10) << 8); } - return mm_channel(inst, ident, 1); + return mm_channel(inst, command, 1); } static size_t ptz_set_pantilt(instance* inst, channel* c, channel_value* v, uint8_t* msg){ diff --git a/backends/visca.h b/backends/visca.h index 481f5c7..160398d 100644 --- a/backends/visca.h +++ b/backends/visca.h @@ -22,10 +22,12 @@ typedef struct /*_ptz_instance_data*/ { } ptz_instance_data; enum /*ptz_channels*/ { - pan = 0, - tilt, - panspeed, + //channels with a name that includes another channels as prefix + //go first so the channel matching logic works + panspeed = 0, tiltspeed, + pan, + tilt, zoom, focus, focus_mode, @@ -62,10 +64,10 @@ static struct { size_t offset; //channel value = normalised * range - offset ptz_channel_set set; } ptz_channels[] = { - [pan] = {"pan", 15, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 0x990 * 2, 0x990, ptz_set_pantilt}, - [tilt] = {"tilt", 15, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 0x510 * 2, 0x510, ptz_set_pantilt}, [panspeed] = {"panspeed", 0, {0}, 0x01, 0x18, 0, ptz_set_ptspeed}, [tiltspeed] = {"tiltspeed", 0, {0}, 0x01, 0x14, 0, ptz_set_ptspeed}, + [pan] = {"pan", 15, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 0x990 * 2, 0x990, ptz_set_pantilt}, + [tilt] = {"tilt", 15, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 0x510 * 2, 0x510, ptz_set_pantilt}, [zoom] = {"zoom", 9, {0x80, 0x01, 0x04, 0x47, 0, 0, 0, 0, 0xFF}, 0, 0x4000, 0, ptz_set_zoom}, [focus] = {"focus", 9, {0x80, 0x01, 0x04, 0x48, 0, 0, 0, 0, 0xFF}, 0, 0x4000, 0, ptz_set_focus}, [focus_mode] = {"autofocus", 6, {0x80, 0x01, 0x04, 0x38, 0, 0xFF}, 0, 1, 0, ptz_set_focus_mode}, -- cgit v1.2.3 From 50a73b4dc7032660e0912c91b197b73190c12240 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 25 Sep 2020 21:30:21 +0200 Subject: Add some new example configuration --- .gitignore | 1 + configs/midi-gamepad.cfg | 25 +++++++++++++++++++++++++ configs/visca.cfg | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 configs/midi-gamepad.cfg create mode 100644 configs/visca.cfg diff --git a/.gitignore b/.gitignore index 4396a38..ccb500c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ libmmapi.a *.o *.so *.dll +__pycache__ diff --git a/configs/midi-gamepad.cfg b/configs/midi-gamepad.cfg new file mode 100644 index 0000000..f91ed4f --- /dev/null +++ b/configs/midi-gamepad.cfg @@ -0,0 +1,25 @@ +; Play games using a MIDI controller! See https://kb.midimonster.net/usecases/DolphinController.html +; This configuration will create a new input device (a joystick), which is controlled by a MIDI input. +; It could, for example, be used to play games in an emulator. +; This will only work on Linux. + +; This evdev instance will provide the new joystick device +[evdev controller] +; Set up the axis constraints - see the evdev backend documentation for what the parameters mean +axis.ABS_X = 34300 0 65536 255 4095 +axis.ABS_Y = 34300 0 65536 255 4095 +relaxis.REL_X = 65535 +; Set the output device name and enable the instance for output +output = MIDIMonster + +; This midi instance will be used as input to control the new joystick +[midi lc] +read = Launch Control + +[map] +; Use two CC's/rotaries to control the main joystick +lc.ch0.cc0 > controller.EV_ABS.ABS_X +lc.ch0.cc1 > controller.EV_ABS.ABS_Y +; Use two buttons to control the joystick buttons +lc.ch0.note0 > controller.EV_KEY.BTN_A +lc.ch0.note1 > controller.EV_KEY.BTN_B diff --git a/configs/visca.cfg b/configs/visca.cfg new file mode 100644 index 0000000..ac4b7a3 --- /dev/null +++ b/configs/visca.cfg @@ -0,0 +1,34 @@ +; This configuration controls a simple VISCA-capable Pan/Tilt/Zoom (PTZ) +; camera over the network via OSC (For example the "Mix 16" layout shipped +; with TouchOSC). + +[backend osc] +; Turn on the detect option to see what comes in +detect = on + +[visca cam] +; This is where we can reach the camera control server +connect = 10.23.23.127 5678 + +[osc touch] +; Bind a local OSC server to which TouchOSC will connect +bind = 0.0.0.0 8000 + +[map] +; Map the XY-pad to camera pan and tilt +touch./1/xy > cam.tilt +touch./1/xy:1 > cam.pan + +; Map control speeds +touch./1/fader1 > cam.panspeed +touch./1/fader2 > cam.tiltspeed + +; Map zoom to a fader +touch./1/fader4 > cam.zoom + +; Map some presets +touch./1/push1 > cam.home +touch./1/push2 > cam.memory1 +touch./1/push3 > cam.memory2 +touch./1/push4 > cam.store1 +touch./1/push5 > cam.store2 -- cgit v1.2.3 From 890111a0183623641ea564c3adc439169af2a6d6 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 25 Sep 2020 21:38:46 +0200 Subject: Publish VISCA backend --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3a3fcbd..26a8c90 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Currently, the MIDIMonster supports the following protocols: | Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | | MA Lighting Web Remote | Linux, Windows, OSX | GrandMA2 and dot2 (incl. OnPC) | [`maweb`](backends/maweb.md) | | JACK/LV2 Control Voltage (CV) | Linux, OSX | | [`jack`](backends/jack.md) | +| VISCA | Linux, Windows, OSX | PTZ Camera control over TCP/UDP | [`visca`](backends/visca.md) | | Lua Scripting | Linux, Windows, OSX | | [`lua`](backends/lua.md) | | Python Scripting | Linux, OSX | | [`python`](backends/python.md) | | Loopback | Linux, Windows, OSX | | [`loopback`](backends/loopback.md) | -- cgit v1.2.3 From 588503226ff61c2a440f19e050691af93cef0f5f Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 9 Oct 2020 23:17:41 +0200 Subject: Implement VISCA relative movement --- backends/visca.c | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- backends/visca.h | 20 ++++++++++---- backends/visca.md | 6 +++-- 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/backends/visca.c b/backends/visca.c index 8465c14..2e82515 100644 --- a/backends/visca.c +++ b/backends/visca.c @@ -2,6 +2,7 @@ #define DEBUG #include +#include #include "visca.h" #include "libmmbackend.h" @@ -87,6 +88,10 @@ static int ptz_configure_instance(instance* inst, char* option, char* value){ return 0; #endif } + else if(!strcmp(option, "deadzone")){ + data->deadzone = strtod(value, NULL); + return 0; + } LOGPF("Unknown instance configuration parameter %s for instance %s", option, inst->name); return 1; @@ -104,6 +109,8 @@ static int ptz_instance(instance* inst){ //start with maximum speeds data->panspeed = ptz_channels[panspeed].max; data->tiltspeed = ptz_channels[tiltspeed].max; + //start with a bit of slack/deadzone in relative movement axes + data->deadzone = 0.1; inst->impl = data; return 0; @@ -135,6 +142,32 @@ static channel* ptz_channel(instance* inst, char* spec, uint8_t flags){ command |= (strtoul(spec + strlen(ptz_channels[command].name), NULL, 10) << 8); } + //store relative move direction + else if(command == relmove){ + if(!strcmp(spec + strlen(ptz_channels[relmove].name), ".up") + || !strcmp(spec + strlen(ptz_channels[relmove].name), ".y")){ + command |= (rel_up << 8); + } + else if(!strcmp(spec + strlen(ptz_channels[relmove].name), ".left") + || !strcmp(spec + strlen(ptz_channels[relmove].name), ".x")){ + command |= (rel_left << 8); + } + + if(!strcmp(spec + strlen(ptz_channels[relmove].name), ".down") + || !strcmp(spec + strlen(ptz_channels[relmove].name), ".y")){ + command |= (rel_down << 8); + } + else if(!strcmp(spec + strlen(ptz_channels[relmove].name), ".right") + || !strcmp(spec + strlen(ptz_channels[relmove].name), ".x")){ + command |= (rel_right << 8); + } + + if(command >> 8 == 0){ + LOGPF("Could not parse relative movement command %s", spec); + return NULL; + } + } + return mm_channel(inst, command, 1); } @@ -177,11 +210,53 @@ static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint return 0; } -static size_t ptz_set_stop(instance* inst, channel* c, channel_value* v, uint8_t* msg){ +static size_t ptz_set_relmove(instance* inst, channel* c, channel_value* v, uint8_t* msg){ ptz_instance_data* data = (ptz_instance_data*) inst->impl; + + uint8_t direction = c->ident >> 8; + double speed_factor = v->normalised; + + if(direction == rel_x + || direction == rel_y){ + //select only one move event + direction &= (speed_factor > 0.5) ? (rel_up | rel_left) : (rel_down | rel_right); + + //scale event value to full axis + speed_factor = fabs((speed_factor - 0.5) * 2); + + //clamp to deadzone + speed_factor = (speed_factor < 2 * data->deadzone) ? 0 : speed_factor; + } + + //clear modified axis + if(direction & rel_x){ + data->relative_movement &= ~rel_x; + } + else{ + data->relative_movement &= ~rel_y; + } + + if(speed_factor){ + data->relative_movement |= direction; + } + + //set stored axis speed msg[4] = data->panspeed; msg[5] = data->tiltspeed; - return ptz_channels[stop].bytes; + + //update motor control from movement data + msg[6] |= (data->relative_movement & (rel_left | rel_right)) >> 2; + msg[7] |= data->relative_movement & (rel_up | rel_down); + + //stop motors if unset + msg[6] = msg[6] ? msg[6] : 3; + msg[7] = msg[7] ? msg[7] : 3; + + DBGPF("Moving axis %d with factor %f, total movement now %02X, commanding %d / %d, %d / %d", + direction, speed_factor, data->relative_movement, + msg[6], msg[4], msg[7], msg[5]); + + return ptz_channels[relmove].bytes; } static size_t ptz_set_zoom(instance* inst, channel* c, channel_value* v, uint8_t* msg){ diff --git a/backends/visca.h b/backends/visca.h index 160398d..47ada19 100644 --- a/backends/visca.h +++ b/backends/visca.h @@ -12,6 +12,15 @@ static int ptz_shutdown(size_t n, instance** inst); #define VISCA_BUFFER_LENGTH 50 +enum /*_ptz_relmove_channel */ { + rel_up = 1, + rel_down = 2, + rel_left = 4, + rel_right = 8, + rel_x = 3, + rel_y = 12 +}; + typedef struct /*_ptz_instance_data*/ { int fd; uint8_t cam_address; @@ -19,6 +28,8 @@ typedef struct /*_ptz_instance_data*/ { uint16_t y; uint8_t panspeed; uint8_t tiltspeed; + uint8_t relative_movement; + double deadzone; } ptz_instance_data; enum /*ptz_channels*/ { @@ -38,6 +49,7 @@ enum /*ptz_channels*/ { store, home, stop, + relmove, sentinel }; @@ -51,9 +63,7 @@ static size_t ptz_set_wb_mode(instance* inst, channel* c, channel_value* v, uint static size_t ptz_set_wb(instance* inst, channel* c, channel_value* v, uint8_t* msg); static size_t ptz_set_memory(instance* inst, channel* c, channel_value* v, uint8_t* msg); static size_t ptz_set_memory_store(instance* inst, channel* c, channel_value* v, uint8_t* msg); - -//relative move test -static size_t ptz_set_stop(instance* inst, channel* c, channel_value* v, uint8_t* msg); +static size_t ptz_set_relmove(instance* inst, channel* c, channel_value* v, uint8_t* msg); static struct { char* name; @@ -77,6 +87,6 @@ static struct { [call] = {"memory", 7, {0x80, 0x01, 0x04, 0x3F, 0x02, 0, 0xFF}, 0, 254, 0, ptz_set_memory}, [store] = {"store", 7, {0x80, 0x01, 0x04, 0x3F, 0x01, 0, 0xFF}, 0, 254, 0, ptz_set_memory_store}, [home] = {"home", 5, {0x80, 0x01, 0x06, 0x04, 0xFF}, 0, 0, 0, NULL}, - //relative move test - [stop] = {"stop", 9, {0x80, 0x01, 0x06, 0x01, 0, 0, 0x03, 0x03, 0xFF}, 0, 0, 0, ptz_set_stop} + [relmove] = {"move", 9, {0x80, 0x01, 0x06, 0x01, 0, 0, 0, 0, 0xFF}, 0, 1, 0, ptz_set_relmove}, + [stop] = {"stop", 9, {0x80, 0x01, 0x06, 0x01, 0, 0, 0x03, 0x03, 0xFF}, 0, 0, 0, ptz_set_relmove} }; diff --git a/backends/visca.md b/backends/visca.md index 1dd516e..cf5906d 100644 --- a/backends/visca.md +++ b/backends/visca.md @@ -19,13 +19,14 @@ The `visca` backend does not take any global configuration. | `id` | `5` | `1` | VISCA Camera address (normally 1 for network communication | | `connect` | `10.10.10.1 5678` | none | Camera network address and port. Default connection is TCP, when optionally suffixed with the `udp` keyword, connection will be UDP | | `device` | `/dev/ttyUSB0` | none | (Linux only) Device node for a serial port adapter connecting to the camera | +| `deadzone` | `0.1` | `0.1` | Amount of event value variation to be ignored for relative movement commands | #### Channel specification Each instance exposes the following channels -* `pan`: Pan axis -* `tilt`: Tilt axis +* `pan`: Pan axis (absolute) +* `tilt`: Tilt axis (absolute) * `panspeed`: Pan speed * `tiltspeed`: Tilt speed * `zoom`: Zoom position @@ -36,6 +37,7 @@ Each instance exposes the following channels * `home`: Return to home position * `memory`: Call memory (if incoming event value is greater than 0.9) * `store`: Store current pan/tilt/zoom setup to memory (if incoming event value is greater than 0.9) +* `move.left`, `move.right`, `move.up`, `move.down`: Relative movement with the currently set `panspeed` and `tiltspeed` Example mappings: -- cgit v1.2.3 From 70f1ca5bbb3156d9d2b90a115ba88495e5151b55 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 10 Oct 2020 17:30:15 +0200 Subject: Update some comments --- backends/sacn.c | 2 +- backends/visca.c | 1 + midimonster.c | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backends/sacn.c b/backends/sacn.c index 0ea7b58..e395ae2 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -385,7 +385,7 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ //send packet if required if(mark){ - //find output instance data + //find output control data for the instance for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){ if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){ break; diff --git a/backends/visca.c b/backends/visca.c index 2e82515..9f398a2 100644 --- a/backends/visca.c +++ b/backends/visca.c @@ -241,6 +241,7 @@ static size_t ptz_set_relmove(instance* inst, channel* c, channel_value* v, uint } //set stored axis speed + //TODO find a way to do relative axis speed via speed_factor, without overwriting a set absolute speed msg[4] = data->panspeed; msg[5] = data->tiltspeed; diff --git a/midimonster.c b/midimonster.c index 3cc9719..3849953 100644 --- a/midimonster.c +++ b/midimonster.c @@ -4,10 +4,10 @@ #include #include #ifndef _WIN32 -#include -#define MM_API __attribute__((visibility("default"))) + #include + #define MM_API __attribute__((visibility("default"))) #else -#define MM_API __attribute__((dllexport)) + #define MM_API __attribute__((dllexport)) #endif #define BACKEND_NAME "core" #define MM_SWAP_LIMIT 20 -- cgit v1.2.3 From 9a5eb9c9d6592b269ba0e39f9cee22ef3313f685 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 23 Oct 2020 19:58:58 +0200 Subject: Implement ArtNet realtime option --- backends/artnet.c | 23 +++++++++++++++-------- backends/artnet.h | 1 + 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index 4a7907d..dae9ba3 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -168,6 +168,10 @@ static int artnet_configure_instance(instance* inst, char* option, char* value){ return mmbackend_parse_sockaddr(host, port ? port : ARTNET_PORT, &data->dest_addr, &data->dest_len); } + else if(!strcmp(option, "realtime")){ + data->realtime = strtoul(value, NULL, 10); + return 0; + } LOGPF("Unknown instance option %s for instance %s", option, inst->name); return 1; @@ -294,21 +298,24 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) } if(mark){ - //find last frame time + //find output control data for the instance for(u = 0; u < global_cfg.fd[data->fd_index].output_instances; u++){ if(global_cfg.fd[data->fd_index].output_instance[u].label == inst->ident){ break; } } - frame_delta = mm_timestamp() - global_cfg.fd[data->fd_index].output_instance[u].last_frame; - //check output rate limit, request next frame - if(frame_delta < ARTNET_FRAME_TIMEOUT){ - global_cfg.fd[data->fd_index].output_instance[u].mark = 1; - if(!global_cfg.next_frame || global_cfg.next_frame > (ARTNET_FRAME_TIMEOUT - frame_delta)){ - global_cfg.next_frame = (ARTNET_FRAME_TIMEOUT - frame_delta); + if(!data->realtime){ + frame_delta = mm_timestamp() - global_cfg.fd[data->fd_index].output_instance[u].last_frame; + + //check output rate limit, request next frame + if(frame_delta < ARTNET_FRAME_TIMEOUT){ + global_cfg.fd[data->fd_index].output_instance[u].mark = 1; + if(!global_cfg.next_frame || global_cfg.next_frame > (ARTNET_FRAME_TIMEOUT - frame_delta)){ + global_cfg.next_frame = (ARTNET_FRAME_TIMEOUT - frame_delta); + } + return 0; } - return 0; } return artnet_transmit(inst, global_cfg.fd[data->fd_index].output_instance + u); } diff --git a/backends/artnet.h b/backends/artnet.h index ecd775e..b42646d 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -48,6 +48,7 @@ typedef struct /*_artnet_instance_model*/ { artnet_universe data; size_t fd_index; uint64_t last_input; + uint8_t realtime; } artnet_instance_data; typedef union /*_artnet_instance_id*/ { -- cgit v1.2.3 From 2d881202dafa5c8593fce2b21b47f63fe1820d96 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 24 Oct 2020 11:23:49 +0200 Subject: Update ArtNet documentation (Fixes #77) --- backends/artnet.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backends/artnet.md b/backends/artnet.md index bd26014..f035ad7 100644 --- a/backends/artnet.md +++ b/backends/artnet.md @@ -21,6 +21,7 @@ Art-Net™ Designed by and Copyright Artistic Licence Holdings Ltd. | `universe` | `0` | `0` | Universe identifier | | `destination` | `10.2.2.2` | none | Destination address for sent ArtNet frames. Setting this enables the universe for output | | `interface` | `1` | `0` | The bound address to use for data input/output | +| `realtime` | `1` | `0` | Disable the recommended rate-limiting (approx. 44 packets per second) for this instance | #### Channel specification @@ -42,3 +43,4 @@ A normal channel that is part of a wide channel can not be mapped individually. When using this backend for output with a fast event source, some events may appear to be lost due to the packet output rate limiting mandated by the [ArtNet specification](https://artisticlicence.com/WebSiteMaster/User%20Guides/art-net.pdf) (Section `Refresh rate`). +This limit can be disabled on a per-instance basis using the `realtime` instance option. -- cgit v1.2.3 From bb122923379f3439c62241652b82ed403ad835f7 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 24 Oct 2020 12:31:27 +0200 Subject: Add generic lua pkg-config data --- TODO | 4 ++++ backends/Makefile | 6 +++--- backends/visca.c | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/TODO b/TODO index 44336d4..5662479 100644 --- a/TODO +++ b/TODO @@ -5,3 +5,7 @@ make event collectors threadsafe to stop marshalling data... collect & check backend API version move all connection establishment to _start to be able to hot-stop/start all backends event deduplication in core? + +per-channel filters + * invert + * edge detection diff --git a/backends/Makefile b/backends/Makefile index 4f89b43..d815f84 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -79,9 +79,9 @@ ola.so: CPPFLAGS += -Wno-write-strings # The pkg-config name for liblua5.3 is subject to discussion. I prefer 'lua5.3' (which works on Debian and OSX), # but Arch requires 'lua53' which works on Debian, too, but breaks on OSX. -lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") -lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") -lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") +lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || pkg-config --cflags lua || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") +lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 || pkg-config --libs lua || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") +lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || pkg-config --cflags lua || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") lua.dll: LDLIBS += -L../ -llua53 python.so: CFLAGS += $(shell pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") diff --git a/backends/visca.c b/backends/visca.c index 9f398a2..ba81f8d 100644 --- a/backends/visca.c +++ b/backends/visca.c @@ -8,8 +8,9 @@ /* TODO * VISCA server - * Rate limiting + * Command output rate limiting / deduplication * Inquiry + * Reconnect on connection close */ MM_PLUGIN_API int init(){ -- cgit v1.2.3 From c85ba5aff2d670791589553073e4c519ef1d8434 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 30 Oct 2020 19:45:11 +0100 Subject: Update lua backend to 5.4 API changes --- backends/lua.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backends/lua.c b/backends/lua.c index cf59f8f..0a638f7 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -146,7 +146,13 @@ static void lua_thread_resume(size_t current_thread){ //call thread main DBGPF("Resuming thread %" PRIsize_t " on %s", current_thread, thread[current_thread].instance->name); + //the lua_resume API has changed with lua5.4 + #if LUA_VERSION_NUM > 503 + int results = 0; + thread_status = lua_resume(thread[current_thread].thread, NULL, 0, &results); + #else thread_status = lua_resume(thread[current_thread].thread, NULL, 0); + #endif if(thread_status == LUA_YIELD){ DBGPF("Thread %" PRIsize_t " on %s yielded execution", current_thread, thread[current_thread].instance->name); @@ -658,6 +664,10 @@ static int lua_start(size_t n, instance** inst){ int default_handler; channel_value v; + #ifdef LUA_VERSION_NUM + DBGPF("Lua backend built with %s (%d)", LUA_VERSION, LUA_VERSION_NUM); + #endif + //resolve channels to their handler functions for(u = 0; u < n; u++){ data = (lua_instance_data*) inst[u]->impl; -- cgit v1.2.3 From a59c42e5caa76a5ea1d5e79b7820a1a012f9a3a4 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 14 Nov 2020 15:21:53 +0100 Subject: MQTT backend skeleton --- backends/mqtt.c | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/mqtt.h | 48 +++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 backends/mqtt.c create mode 100644 backends/mqtt.h diff --git a/backends/mqtt.c b/backends/mqtt.c new file mode 100644 index 0000000..4f56aa6 --- /dev/null +++ b/backends/mqtt.c @@ -0,0 +1,108 @@ +#define BACKEND_NAME "mqtt" + +#include +#include "mqtt.h" + +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 size_t mqtt_varint_decode(uint8_t* buffer, uint32_t* result){ + //TODO + return 0; +} + +static int mqtt_configure(char* option, char* value){ + LOG("This backend does not take global configuration"); + return 1; +} + +static int mqtt_configure_instance(instance* inst, char* option, char* value){ + mqtt_instance_data* data = (mqtt_instance_data*) inst->impl; + char* token = value; + + if(!strcmp(option, "user")){ + free(data->user); + data->user = strdup(value); + return 0; + } + else if(!strcmp(option, "password")){ + free(data->password); + data->user = strdup(value); + return 0; + } + else if(!strcmp(option, "host")){ + //mqtt url may be of the form + //mqtt[s]://[username][:password]@host.domain[:port] + token = strchr(value, ':'); + //TODO + } + + LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); + return 1; +} + +static int mqtt_instance(instance* inst){ + //TODO + return 0; +} + +static channel* mqtt_channel(instance* inst, char* spec, uint8_t flags){ + //TODO + return NULL; +} + +static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){ + //TODO + return 0; +} + +static int mqtt_handle(size_t num, managed_fd* fds){ + //TODO + return 0; +} + +static int mqtt_start(size_t n, instance** inst){ + //TODO + return 0; +} + +static int mqtt_shutdown(size_t n, instance** inst){ + size_t u, p; + mqtt_instance_data* data = NULL; + + for(u = 0; u < n; u++){ + data = (mqtt_instance_data*) inst[u]->impl; + for(p = 0; p < data->nchannels; p++){ + free(data->channel[p]); + } + free(data->channel); + free(data->host); + free(data->port); + free(data->user); + free(data->password); + + 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..165f2ba --- /dev/null +++ b/backends/mqtt.h @@ -0,0 +1,48 @@ +#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" + +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 = 0x80, + MSG_SUBACK = 0x90, + MSG_UNSUBSCRIBE = 0xA0, + MSG_UNSUBACK = 0xB0, + MSG_PINGREQ = 0xC0, + MSG_PINGRESP = 0xD0, + MSG_DISCONNECT = 0xE0, + MSG_AUTH = 0xF0 +}; + +typedef struct /*_mqtt_instance_data*/ { + uint8_t tls; + char* host; + char* port; + + char* user; + char* password; + + size_t nchannels; + char** channel; +} mqtt_instance_data; + +//per-channel +//qos, subscribe -- cgit v1.2.3 From fdb0bf2e86693f99550f6e21de85023188ac5b03 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 5 Dec 2020 14:27:23 +0100 Subject: Implement basic MQTT connection startup --- backends/mqtt.c | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- backends/mqtt.h | 5 ++ 2 files changed, 206 insertions(+), 12 deletions(-) diff --git a/backends/mqtt.c b/backends/mqtt.c index 4f56aa6..d9cf2d8 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -1,8 +1,14 @@ #define BACKEND_NAME "mqtt" +#define DEBUG #include + +#include "libmmbackend.h" #include "mqtt.h" +//TODO +// * Periodic connection retries + MM_PLUGIN_API int init(){ backend mqtt = { .name = BACKEND_NAME, @@ -24,8 +30,100 @@ MM_PLUGIN_API int init(){ 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 size_t mqtt_varint_decode(uint8_t* buffer, uint32_t* result){ - //TODO + size_t value = 0, offset = 0; + do { + value |= (buffer[offset] & 0x7F) << (7 * offset); + offset++; + } while(buffer[offset - 1] & 0x80); + return 0; +} + +static size_t mqtt_varint_encode(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 void mqtt_disconnect(instance* inst){ + mqtt_instance_data* data = (mqtt_instance_data*) inst->impl; + + //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; + + fixed_header[offset++] = type; + offset += mqtt_varint_encode(vh_length + payload_length, sizeof(fixed_header) - offset, fixed_header + offset); + + if(mmbackend_send(data->fd, fixed_header, offset) + || mmbackend_send(data->fd, vh, vh_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; + } + return 0; } @@ -34,25 +132,91 @@ static int mqtt_configure(char* option, char* value){ return 1; } +static int mqtt_reconnect(instance* inst){ + uint8_t variable_header[MQTT_BUFFER_LENGTH] = {0x00, 0x04, 'M', 'Q', 'T', 'T', 0x05, 0x00 /*flags*/, (MQTT_KEEPALIVE >> 8) & 0xFF, MQTT_KEEPALIVE & 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)", + 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->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 + variable_header[7] = 0x02 /*clean start*/ | (data->user ? 0x80 : 0x00) | (data->user ? 0x40 : 0x00); + //TODO set session expiry interval option + //TODO re-use previos session on reconnect + + //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++] = 7; + //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; + + //push client_id as utf8 + //payload_offset += mqtt_push_utf8(); + if(data->user){ + //push user name as utf8 + } + if(data->password){ + //push password as binary + } + + 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_instance(instance* inst, char* option, char* value){ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl; - char* token = value; if(!strcmp(option, "user")){ - free(data->user); - data->user = strdup(value); + mmbackend_strdup(&(data->user), value); return 0; } else if(!strcmp(option, "password")){ - free(data->password); - data->user = strdup(value); + mmbackend_strdup(&(data->password), value); return 0; } else if(!strcmp(option, "host")){ - //mqtt url may be of the form - //mqtt[s]://[username][:password]@host.domain[:port] - token = strchr(value, ':'); - //TODO + if(mqtt_parse_hostspec(inst, value)){ + return 1; + } + return 0; } LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); @@ -60,7 +224,15 @@ static int mqtt_configure_instance(instance* inst, char* option, char* value){ } static int mqtt_instance(instance* inst){ - //TODO + mqtt_instance_data* data = calloc(1, sizeof(mqtt_instance_data)); + + if(!data){ + LOG("Failed to allocate memory"); + return 1; + } + + data->fd = -1; + inst->impl = data; return 0; } @@ -75,12 +247,27 @@ static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){ } static int mqtt_handle(size_t num, managed_fd* fds){ + LOG("Handling"); //TODO return 0; } static int mqtt_start(size_t n, instance** inst){ - //TODO + size_t u = 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: + break; + } + } + return 0; } @@ -90,6 +277,8 @@ static int mqtt_shutdown(size_t n, instance** inst){ for(u = 0; u < n; u++){ data = (mqtt_instance_data*) inst[u]->impl; + mqtt_disconnect(inst[u]); + for(p = 0; p < data->nchannels; p++){ free(data->channel[p]); } diff --git a/backends/mqtt.h b/backends/mqtt.h index 165f2ba..1c8b47d 100644 --- a/backends/mqtt.h +++ b/backends/mqtt.h @@ -12,6 +12,8 @@ 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 enum { MSG_RESERVED = 0x00, @@ -39,9 +41,12 @@ typedef struct /*_mqtt_instance_data*/ { char* user; char* password; + char* client_id; size_t nchannels; char** channel; + + int fd; } mqtt_instance_data; //per-channel -- cgit v1.2.3 From 378c38aee6dae85c75be924e04f0ba5f90145518 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 6 Dec 2020 17:11:46 +0100 Subject: Add MQTT backend to backend makefile --- backends/Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 -- cgit v1.2.3 From e248f0c5778f73988477ba73174c91b7085c232c Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 18 Dec 2020 22:31:14 +0100 Subject: Implement MQTT connect --- backends/mqtt.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++++-------- backends/mqtt.h | 3 +++ 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/backends/mqtt.c b/backends/mqtt.c index d9cf2d8..8c4a9fd 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -2,10 +2,13 @@ #define DEBUG #include +#include #include "libmmbackend.h" #include "mqtt.h" +static uint64_t last_maintenance = 0; + //TODO // * Periodic connection retries @@ -75,6 +78,14 @@ static int mqtt_parse_hostspec(instance* inst, char* hostspec){ return 0; } +static int mqtt_generate_instanceid(instance* inst){ + mqtt_instance_data* data = (mqtt_instance_data*) inst->impl; + char clientid[23] = ""; + + snprintf(clientid, sizeof(clientid), "MIDIMonster-%d-%s", (uint32_t) time(NULL), inst->name); + return mmbackend_strdup(&(data->client_id), clientid); +} + static size_t mqtt_varint_decode(uint8_t* buffer, uint32_t* result){ size_t value = 0, offset = 0; do { @@ -98,6 +109,24 @@ static size_t mqtt_varint_encode(size_t value, size_t maxlen, uint8_t* buffer){ 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 void mqtt_disconnect(instance* inst){ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl; @@ -133,7 +162,7 @@ static int mqtt_configure(char* option, char* value){ } static int mqtt_reconnect(instance* inst){ - uint8_t variable_header[MQTT_BUFFER_LENGTH] = {0x00, 0x04, 'M', 'Q', 'T', 'T', 0x05, 0x00 /*flags*/, (MQTT_KEEPALIVE >> 8) & 0xFF, MQTT_KEEPALIVE & 0xFF}; + uint8_t variable_header[MQTT_BUFFER_LENGTH] = {0x00, 0x04, 'M', 'Q', 'T', 'T', MQTT_VERSION, 0x00 /*flags*/, (MQTT_KEEPALIVE >> 8) & 0xFF, MQTT_KEEPALIVE & 0xFF}; uint8_t payload[MQTT_BUFFER_LENGTH]; size_t vh_offset = 10, payload_offset = 0; mqtt_instance_data* data = (mqtt_instance_data*) inst->impl; @@ -162,14 +191,15 @@ static int mqtt_reconnect(instance* inst){ return 1; } - //prepare CONNECT message + //prepare CONNECT message flags variable_header[7] = 0x02 /*clean start*/ | (data->user ? 0x80 : 0x00) | (data->user ? 0x40 : 0x00); + //TODO set session expiry interval option //TODO re-use previos session on reconnect - + //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++] = 7; + variable_header[vh_offset++] = 8; //push maximum packet size option variable_header[vh_offset++] = 0x27; variable_header[vh_offset++] = (MQTT_BUFFER_LENGTH >> 24) & 0xFF; @@ -181,13 +211,14 @@ static int mqtt_reconnect(instance* inst){ variable_header[vh_offset++] = 0xFF; variable_header[vh_offset++] = 0xFF; - //push client_id as utf8 - //payload_offset += mqtt_push_utf8(); + //prepare CONNECT payload + //push client id + payload_offset += mqtt_push_utf8(payload + payload_offset, sizeof(payload) - payload_offset, data->client_id); if(data->user){ - //push user name as utf8 + payload_offset += mqtt_push_utf8(payload + payload_offset, sizeof(payload) - payload_offset, data->user); } if(data->password){ - //push password as binary + 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); @@ -218,6 +249,15 @@ static int mqtt_configure_instance(instance* inst, char* option, char* value){ } return 0; } + else if(!strcmp(option, "clientid")){ + if(strlen(value)){ + mmbackend_strdup(&(data->client_id), value); + return 0; + } + else{ + return mqtt_generate_instanceid(inst); + } + } LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); return 1; @@ -233,6 +273,10 @@ static int mqtt_instance(instance* inst){ data->fd = -1; inst->impl = data; + + if(mqtt_generate_instanceid(inst)){ + return 1; + } return 0; } @@ -247,8 +291,17 @@ static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){ } static int mqtt_handle(size_t num, managed_fd* fds){ - LOG("Handling"); - //TODO + size_t n = 0; + + //for(n = 0; n < num; n++){ + //} + + //keepalive/reconnect processing + if(last_maintenance && mm_timestamp() - last_maintenance >= MQTT_KEEPALIVE * 1000){ + //TODO run reconnects + last_maintenance = mm_timestamp(); + } + return 0; } @@ -268,6 +321,8 @@ static int mqtt_start(size_t n, instance** inst){ } } + //initialize maintenance timer + last_maintenance = mm_timestamp(); return 0; } diff --git a/backends/mqtt.h b/backends/mqtt.h index 1c8b47d..df63319 100644 --- a/backends/mqtt.h +++ b/backends/mqtt.h @@ -14,6 +14,7 @@ static int mqtt_shutdown(size_t n, instance** inst); #define MQTT_TLS_PORT "8883" #define MQTT_BUFFER_LENGTH 8192 #define MQTT_KEEPALIVE 10 +#define MQTT_VERSION 0x05 enum { MSG_RESERVED = 0x00, @@ -47,6 +48,8 @@ typedef struct /*_mqtt_instance_data*/ { char** channel; int fd; + uint8_t receive_buffer[MQTT_BUFFER_LENGTH]; + size_t receive_offset; } mqtt_instance_data; //per-channel -- cgit v1.2.3 From 2b0aea70c275e08c1e312db91653eef880b4f725 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 19 Dec 2020 17:25:19 +0100 Subject: Reconnect MQTT instances during maintenance --- backends/mqtt.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/backends/mqtt.c b/backends/mqtt.c index 8c4a9fd..72046df 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -290,15 +290,69 @@ static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){ return 0; } +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; + } + } + } + + free(inst); + 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; + + 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); + + return 0; +} + static int mqtt_handle(size_t num, managed_fd* fds){ size_t n = 0; + int rv = 0; - //for(n = 0; n < num; n++){ - //} + 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){ - //TODO run reconnects + if(mqtt_maintenance()){ + return 1; + } last_maintenance = mm_timestamp(); } -- cgit v1.2.3 From 0175c84ad866e8f33a90d571e6207c6cc120075c Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 20 Dec 2020 13:30:11 +0100 Subject: Basic MQTT message parsing --- backends/mqtt.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/backends/mqtt.c b/backends/mqtt.c index 72046df..d42d1f5 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -86,16 +86,22 @@ static int mqtt_generate_instanceid(instance* inst){ return mmbackend_strdup(&(data->client_id), clientid); } -static size_t mqtt_varint_decode(uint8_t* buffer, uint32_t* result){ +static int 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); - return 0; + + *result = value; + return offset; } -static size_t mqtt_varint_encode(size_t value, size_t maxlen, uint8_t* buffer){ +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 { @@ -142,8 +148,9 @@ static int mqtt_transmit(instance* inst, uint8_t type, size_t vh_length, uint8_t 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_varint_encode(vh_length + payload_length, sizeof(fixed_header) - offset, fixed_header + offset); + offset += mqtt_push_varint(vh_length + payload_length, sizeof(fixed_header) - offset, fixed_header + offset); if(mmbackend_send(data->fd, fixed_header, offset) || mmbackend_send(data->fd, vh, vh_length) @@ -316,9 +323,28 @@ static int mqtt_maintenance(){ return 0; } +static int mqtt_handle_message(instance* inst, uint8_t type, uint8_t* variable_header, size_t length){ + switch(type){ + case MSG_CONNACK: + if(length >= 2){ + if(variable_header[1]){ + LOGPF("Connection on %s was rejected, reason code %d", inst->name, variable_header[1]); + } + else{ + LOGPF("Connection on %s established", inst->name); + } + } + break; + default: + 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){ @@ -333,7 +359,27 @@ static int mqtt_handle_fd(instance* inst){ } DBGPF("Instance %s, offset %" PRIsize_t ", read %" PRIsize_t " bytes", inst->name, data->receive_offset, bytes_read); + data->receive_offset += bytes_read; + + //TODO loop this while at least one unhandled message is in the buffer + //check for complete message + if(data->receive_offset >= 2){ + 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; + } + } + data->receive_offset += bytes_read; return 0; } -- cgit v1.2.3 From adc132b5fc039a185be947de3309bd11f4dee823 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 1 Jan 2021 19:22:01 +0100 Subject: Implement EPN transmission for the midi backend --- backends/midi.c | 90 +++++++++++++++++++++++++++++++++++++++++++------------- backends/midi.h | 8 +++-- backends/midi.md | 10 +++++++ 3 files changed, 85 insertions(+), 23 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index 1f0f2d5..8a8887a 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -13,7 +13,9 @@ enum /*_midi_channel_type*/ { cc, pressure, aftertouch, - pitchbend + pitchbend, + rpn, + nrpn }; static struct { @@ -99,6 +101,13 @@ static int midi_configure_instance(instance* inst, char* option, char* value){ data->write = strdup(value); return 0; } + else if(!strcmp(option, "epn-tx")){ + data->epn_tx_short = 0; + if(!strcmp(value, "short")){ + data->epn_tx_short = 1; + } + return 0; + } LOGPF("Unknown instance option %s", option); return 1; @@ -147,6 +156,14 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ ident.fields.type = pressure; channel += 8; } + else if(!strncmp(channel, "rpn", 3)){ + ident.fields.type = rpn; + channel += 3; + } + else if(!strncmp(channel, "nrpn", 4)){ + ident.fields.type = nrpn; + channel += 4; + } else if(!strncmp(channel, "pitch", 5)){ ident.fields.type = pitchbend; } @@ -167,9 +184,37 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ return NULL; } +static void midi_tx(int port, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){ + snd_seq_event_t ev; + + snd_seq_ev_clear(&ev); + snd_seq_ev_set_source(&ev, port); + snd_seq_ev_set_subs(&ev); + snd_seq_ev_set_direct(&ev); + + switch(type){ + case note: + snd_seq_ev_set_noteon(&ev, channel, control, value); + break; + case cc: + snd_seq_ev_set_controller(&ev, channel, control, value); + break; + case pressure: + snd_seq_ev_set_keypress(&ev, channel, control, value); + break; + case pitchbend: + snd_seq_ev_set_pitchbend(&ev, channel, value); + break; + case aftertouch: + snd_seq_ev_set_chanpress(&ev, channel, value); + break; + } + + snd_seq_event_output(sequencer, &ev); +} + static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t u; - snd_seq_event_t ev; midi_instance_data* data = (midi_instance_data*) inst->impl; midi_channel_ident ident = { .label = 0 @@ -178,30 +223,28 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ for(u = 0; u < num; u++){ ident.label = c[u]->ident; - snd_seq_ev_clear(&ev); - snd_seq_ev_set_source(&ev, data->port); - snd_seq_ev_set_subs(&ev); - snd_seq_ev_set_direct(&ev); - switch(ident.fields.type){ - case note: - snd_seq_ev_set_noteon(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); - break; - case cc: - snd_seq_ev_set_controller(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); - break; - case pressure: - snd_seq_ev_set_keypress(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); + case rpn: + case nrpn: + //transmit parameter number + midi_tx(data->port, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control & 0x3F80) >> 7); + midi_tx(data->port, cc, ident.fields.channel, (ident.fields.type == rpn) ? 100 : 98, ident.fields.control & 0x7F); + //transmit parameter value + midi_tx(data->port, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) & 0x3F80) >> 7); + midi_tx(data->port, cc, ident.fields.channel, 38, ((uint16_t) (v[u].normalised * 16383.0)) & 0x7F); + + if(!data->epn_tx_short){ + //clear active parameter + midi_tx(data->port, cc, ident.fields.channel, 101, 127); + midi_tx(data->port, cc, ident.fields.channel, 100, 127); + } break; case pitchbend: - snd_seq_ev_set_pitchbend(&ev, ident.fields.channel, (v[u].normalised * 16383.0) - 8192); - break; - case aftertouch: - snd_seq_ev_set_chanpress(&ev, ident.fields.channel, v[u].normalised * 127.0); + midi_tx(data->port, ident.fields.type, ident.fields.channel, ident.fields.control, (v[u].normalised * 16383.0) - 8192); break; + default: + midi_tx(data->port, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); } - - snd_seq_event_output(sequencer, &ev); } snd_seq_drain_output(sequencer); @@ -216,6 +259,10 @@ static char* midi_type_name(uint8_t type){ return "note"; case cc: return "cc"; + case rpn: + return "rpn"; + case nrpn: + return "nrpn"; case pressure: return "pressure"; case aftertouch: @@ -248,6 +295,7 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.fields.control = ev->data.note.note; val.normalised = (double) ev->data.note.velocity / 127.0; + //TODO (n)rpn RX switch(ev->type){ case SND_SEQ_EVENT_NOTEON: case SND_SEQ_EVENT_NOTEOFF: diff --git a/backends/midi.h b/backends/midi.h index dcee010..4e2ac09 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -14,14 +14,18 @@ typedef struct /*_midi_instance_data*/ { int port; char* read; char* write; + uint8_t epn_tx_short; + + uint16_t epn_control; + uint16_t epn_value; } midi_instance_data; typedef union { struct { - uint8_t pad[5]; + uint8_t pad[4]; uint8_t type; uint8_t channel; - uint8_t control; + uint16_t control; } fields; uint64_t label; } midi_channel_ident; diff --git a/backends/midi.md b/backends/midi.md index d3d6e33..3ac011e 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -15,6 +15,7 @@ The MIDI backend provides read-write access to the MIDI protocol via virtual por |---------------|-----------------------|-----------------------|-----------------------| | `read` | `20:0` | none | MIDI device to connect for input | | `write` | `DeviceName` | none | MIDI device to connect for output | +| `epn-tx` | `short` | `full` | Configures whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter | MIDI device names may either be `client:port` portnames or prefixes of MIDI device names. Run `aconnect -i` to list input ports and `aconnect -o` to list output ports. @@ -30,6 +31,8 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages +* `rpn` - Registered parameter numbers (14bit extension) +* `nrpn` - Non-registered parameter numbers (14bit extension) A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). @@ -40,15 +43,22 @@ MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (n additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called 'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. +Every channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using +the CC controls 101/100/99/98/38/6. Both control types have 14-bit IDs and 14-bit values. + Example mappings: ``` midi1.ch0.note9 > midi2.channel1.cc4 midi1.channel15.pressure1 > midi1.channel0.note0 midi1.ch1.aftertouch > midi2.ch2.cc0 midi1.ch0.pitch > midi2.ch1.pitch +midi1.ch0.nrpn900 > midi2.ch0.rpn1 ``` #### Known bugs / problems +Extended parameter numbers (`rpn` and `nrpn` control types) can currently only be transmitted, not properly +received as such. Support for this functionality is planned. + To access MIDI data, the user running MIDIMonster needs read & write access to the ALSA sequencer. This can usually be done by adding this user to the `audio` system group. -- cgit v1.2.3 From 26b91b849899976b455bc5d780688de6962569e1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 2 Jan 2021 17:15:57 +0100 Subject: Implement EPN transmission for the winmidi backend --- backends/midi.c | 6 ++-- backends/midi.md | 6 ++-- backends/winmidi.c | 85 +++++++++++++++++++++++++++++++++++++++++------------ backends/winmidi.h | 9 ++++-- backends/winmidi.md | 10 +++++++ 5 files changed, 88 insertions(+), 28 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index 8a8887a..d581a01 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -109,7 +109,7 @@ static int midi_configure_instance(instance* inst, char* option, char* value){ return 0; } - LOGPF("Unknown instance option %s", option); + LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); return 1; } @@ -227,10 +227,10 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ case rpn: case nrpn: //transmit parameter number - midi_tx(data->port, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control & 0x3F80) >> 7); + midi_tx(data->port, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control >> 7) & 0x7F); midi_tx(data->port, cc, ident.fields.channel, (ident.fields.type == rpn) ? 100 : 98, ident.fields.control & 0x7F); //transmit parameter value - midi_tx(data->port, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) & 0x3F80) >> 7); + midi_tx(data->port, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) >> 7) & 0x7F); midi_tx(data->port, cc, ident.fields.channel, 38, ((uint16_t) (v[u].normalised * 16383.0)) & 0x7F); if(!data->epn_tx_short){ diff --git a/backends/midi.md b/backends/midi.md index 3ac011e..4732452 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -31,8 +31,8 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages -* `rpn` - Registered parameter numbers (14bit extension) -* `nrpn` - Non-registered parameter numbers (14bit extension) +* `rpn` - Registered parameter numbers (14-bit extension) +* `nrpn` - Non-registered parameter numbers (14-bit extension) A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). @@ -43,7 +43,7 @@ MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (n additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called 'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. -Every channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using +Every MIDI channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using the CC controls 101/100/99/98/38/6. Both control types have 14-bit IDs and 14-bit values. Example mappings: diff --git a/backends/winmidi.c b/backends/winmidi.c index 030062d..c89a098 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -82,7 +82,7 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value) data->read = strdup(value); return 0; } - if(!strcmp(option, "write")){ + else if(!strcmp(option, "write")){ if(data->write){ LOGPF("Instance %s already connected to an output device", inst->name); return 1; @@ -90,6 +90,13 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value) data->write = strdup(value); return 0; } + else if(!strcmp(option, "epn-tx")){ + data->epn_tx_short = 0; + if(!strcmp(value, "short")){ + data->epn_tx_short = 1; + } + return 0; + } LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); return 1; @@ -148,6 +155,14 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ ident.fields.type = pressure; next_token += 8; } + else if(!strncmp(next_token, "rpn", 3)){ + ident.fields.type = rpn; + next_token += 3; + } + else if(!strncmp(next_token, "nrpn", 4)){ + ident.fields.type = nrpn; + next_token += 4; + } else if(!strncmp(next_token, "pitch", 5)){ ident.fields.type = pitchbend; } @@ -167,11 +182,7 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ return NULL; } -static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ - winmidi_instance_data* data = (winmidi_instance_data*) inst->impl; - winmidi_channel_ident ident = { - .label = 0 - }; +static void winmidi_tx(HMIDIOUT port, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){ union { struct { uint8_t status; @@ -183,6 +194,28 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v } output = { .dword = 0 }; + + output.components.status = type | channel; + output.components.data1 = control; + output.components.data2 = value; + + if(type == pitchbend){ + output.components.data1 = value & 0x7F; + output.components.data2 = (value >> 7) & 0x7F; + } + else if(type == aftertouch){ + output.components.data1 = value; + output.components.data2 = 0; + } + + midiOutShortMsg(port, output.dword); +} + +static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ + winmidi_instance_data* data = (winmidi_instance_data*) inst->impl; + winmidi_channel_ident ident = { + .label = 0 + }; size_t u; if(!data->device_out){ @@ -193,20 +226,29 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v for(u = 0; u < num; u++){ ident.label = c[u]->ident; - //build output message - output.components.status = ident.fields.type | ident.fields.channel; - output.components.data1 = ident.fields.control; - output.components.data2 = v[u].normalised * 127.0; - if(ident.fields.type == pitchbend){ - output.components.data1 = ((int)(v[u].normalised * 16384.0)) & 0x7F; - output.components.data2 = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; - } - else if(ident.fields.type == aftertouch){ - output.components.data1 = v[u].normalised * 127.0; - output.components.data2 = 0; + switch(ident.fields.type){ + case rpn: + case nrpn: + //transmit parameter number + winmidi_tx(data->device_out, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control >> 7) & 0x7F); + winmidi_tx(data->device_out, cc, ident.fields.channel, (ident.fields.type == rpn) ? 100 : 98, ident.fields.control & 0x7F); + + //transmit parameter value + winmidi_tx(data->device_out, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) >> 7) & 0x7F); + winmidi_tx(data->device_out, cc, ident.fields.channel, 38, ((uint16_t) (v[u].normalised * 16383.0)) & 0x7F); + + if(!data->epn_tx_short){ + //clear active parameter + winmidi_tx(data->device_out, cc, ident.fields.channel, 101, 127); + winmidi_tx(data->device_out, cc, ident.fields.channel, 100, 127); + } + break; + case pitchbend: + winmidi_tx(data->device_out, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 16383.0); + break; + default: + winmidi_tx(data->device_out, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); } - - midiOutShortMsg(data->device_out, output.dword); } return 0; @@ -218,6 +260,10 @@ static char* winmidi_type_name(uint8_t typecode){ return "note"; case cc: return "cc"; + case rpn: + return "rpn"; + case nrpn: + return "nrpn"; case pressure: return "pressure"; case aftertouch: @@ -295,6 +341,7 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW //callbacks may run on different threads, so we queue all events and alert the main thread via the feedback socket DBGPF("Input callback on thread %ld", GetCurrentThreadId()); + //TODO handle (n)rpn RX switch(message){ case MIM_MOREDATA: //processing too slow, do not immediately alert the main loop diff --git a/backends/winmidi.h b/backends/winmidi.h index 4c740ea..fbb2c94 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -13,6 +13,7 @@ static int winmidi_shutdown(size_t n, instance** inst); typedef struct /*_winmidi_instance_data*/ { char* read; char* write; + uint8_t epn_tx_short; HMIDIIN device_in; HMIDIOUT device_out; } winmidi_instance_data; @@ -23,15 +24,17 @@ enum /*_winmidi_channel_type*/ { cc = 0xB0, pressure = 0xA0, aftertouch = 0xD0, - pitchbend = 0xE0 + pitchbend = 0xE0, + rpn = 0xF0, + nrpn = 0xF1 }; typedef union { struct { - uint8_t pad[5]; + uint8_t pad[4]; uint8_t type; uint8_t channel; - uint8_t control; + uint16_t control; } fields; uint64_t label; } winmidi_channel_ident; diff --git a/backends/winmidi.md b/backends/winmidi.md index 25a6378..6b0fa98 100644 --- a/backends/winmidi.md +++ b/backends/winmidi.md @@ -19,6 +19,7 @@ some deviations may still be present. |---------------|-----------------------|-----------------------|-----------------------| | `read` | `2` | none | MIDI device to connect for input | | `write` | `DeviceName` | none | MIDI device to connect for output | +| `epn-tx` | `short` | `full` | Configure whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter. | Input/output device names may either be prefixes of MIDI device names or numeric indices corresponding to the listing shown at startup when using the global `list` option. @@ -32,6 +33,8 @@ The `winmidi` backend supports mapping different MIDI events as MIDIMonster chan * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages +* `rpn` - Registered parameter numbers (14-bit extension) +* `nrpn` - Non-registered parameter numbers (14-bit extension) A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). @@ -42,16 +45,23 @@ MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (n additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called 'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. +Every MIDI channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using +the CC controls 101/100/99/98/38/6. Both control types have 14-bit IDs and 14-bit values. + Example mappings: ``` midi1.ch0.note9 > midi2.channel1.cc4 midi1.channel15.pressure1 > midi1.channel0.note0 midi1.ch1.aftertouch > midi2.ch2.cc0 midi1.ch0.pitch > midi2.ch1.pitch +midi2.ch0.nrpn900 > midi1.ch1.rpn1 ``` #### Known bugs / problems +Extended parameter numbers (`rpn` and `nrpn` control types) can currently only be transmitted, not properly +received as such. Support for this functionality is planned. + Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are generated, which amount to the same thing according to the spec). This may be implemented as a configuration option at a later time. -- cgit v1.2.3 From 1e4a11bd9848c40e6cd19632bef1981bb33b3b3d Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 3 Jan 2021 18:12:48 +0100 Subject: Implement EPN transmission for the jack backend --- backends/jack.c | 99 +++++++++++++++++++++++++++++++++++++++++--------------- backends/jack.h | 9 ++++-- backends/jack.md | 7 ++++ 3 files changed, 86 insertions(+), 29 deletions(-) diff --git a/backends/jack.c b/backends/jack.c index c84ed0f..a3caf73 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -18,8 +18,6 @@ #endif #endif -//FIXME pitchbend range is somewhat oob - static struct /*_mmjack_backend_cfg*/ { unsigned verbosity; volatile sig_atomic_t jack_shutdown; @@ -80,18 +78,42 @@ static int mmjack_midiqueue_append(mmjack_port* port, mmjack_channel_ident ident return 0; } +static void mmjack_process_midiout(void* buffer, size_t sample_offset, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){ + jack_midi_data_t* event_data = jack_midi_event_reserve(buffer, sample_offset, (type == midi_aftertouch) ? 2 : 3); + + if(!event_data){ + LOG("Failed to reserve MIDI stream data"); + return; + } + + //build midi event + event_data[0] = channel | type; + event_data[1] = control & 0x7F; + event_data[2] = value & 0x7F; + + if(type == midi_pitchbend){ + event_data[1] = value & 0x7F; + event_data[2] = (value >> 7) & 0x7F; + } + else if(type == midi_aftertouch){ + event_data[1] = value & 0x7F; + event_data[2] = 0; + } +} + static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){ + mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; void* buffer = jack_port_get_buffer(port->port, nframes); jack_nframes_t event_count = jack_midi_get_event_count(buffer); jack_midi_event_t event; - jack_midi_data_t* event_data; mmjack_channel_ident ident; - size_t u; + size_t u, frame; uint16_t value; if(port->input){ if(event_count){ DBGPF("Reading %u MIDI events from port %s", event_count, port->name); + //TODO (n)rpn RX for(u = 0; u < event_count; u++){ ident.label = 0; //read midi data from stream @@ -124,30 +146,33 @@ static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes //clear buffer jack_midi_clear_buffer(buffer); + frame = 0; for(u = 0; u < port->queue_len; u++){ - //build midi event ident.label = port->queue[u].ident.label; - event_data = jack_midi_event_reserve(buffer, u, (ident.fields.sub_type == midi_aftertouch) ? 2 : 3); - if(!event_data){ - LOG("Failed to reserve MIDI stream data"); - return 1; - } - event_data[0] = ident.fields.sub_channel | ident.fields.sub_type; - if(ident.fields.sub_type == midi_pitchbend){ - event_data[1] = port->queue[u].raw & 0x7F; - event_data[2] = (port->queue[u].raw >> 7) & 0x7F; - } - else if(ident.fields.sub_type == midi_aftertouch){ - event_data[1] = port->queue[u].raw & 0x7F; + + if(ident.fields.sub_type == midi_rpn + || ident.fields.sub_type == midi_nrpn){ + //transmit parameter number + mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, (ident.fields.sub_type == midi_rpn) ? 101 : 99, (ident.fields.sub_control >> 7) & 0x7F); + mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, (ident.fields.sub_type == midi_rpn) ? 100 : 98, ident.fields.sub_control & 0x7F); + + //transmit parameter value + mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 6, (port->queue[u].raw >> 7) & 0x7F); + mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 38, port->queue[u].raw & 0x7F); + + if(!data->midi_epn_tx_short){ + //clear active parameter + mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 101, 127); + mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 100, 127); + } } else{ - event_data[1] = ident.fields.sub_control; - event_data[2] = port->queue[u].raw & 0x7F; + mmjack_process_midiout(buffer, frame++, ident.fields.sub_type, ident.fields.sub_channel, ident.fields.sub_control, port->queue[u].raw); } } - if(port->queue_len){ - DBGPF("Wrote %" PRIsize_t " MIDI events to port %s", port->queue_len, port->name); + if(frame){ + DBGPF("Wrote %" PRIsize_t " MIDI events to port %s", frame, port->name); } port->queue_len = 0; } @@ -305,6 +330,13 @@ static int mmjack_configure_instance(instance* inst, char* option, char* value){ data->server_name = strdup(value); return 0; } + else if(!strcmp(option, "epn-tx")){ + data->midi_epn_tx_short = 0; + if(!strcmp(value, "short")){ + data->midi_epn_tx_short = 1; + } + return 0; + } //register new port, first check for unique name for(p = 0; p < data->ports; p++){ @@ -385,6 +417,14 @@ static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){ ident->fields.sub_type = midi_pressure; next_token += 8; } + else if(!strncmp(next_token, "rpn", 3)){ + ident->fields.sub_type = midi_rpn; + next_token += 3; + } + else if(!strncmp(next_token, "nrpn", 4)){ + ident->fields.sub_type = midi_nrpn; + next_token += 4; + } else if(!strncmp(next_token, "pitch", 5)){ ident->fields.sub_type = midi_pitchbend; } @@ -399,7 +439,9 @@ static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){ ident->fields.sub_control = strtoul(next_token, NULL, 10); if(ident->fields.sub_type == midi_none - || ident->fields.sub_control > 127){ + || (ident->fields.sub_type != midi_nrpn + && ident->fields.sub_type != midi_rpn + && ident->fields.sub_control > 127)){ LOGPF("Invalid MIDI spec %s", spec); return 1; } @@ -467,9 +509,12 @@ static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v) break; case port_midi: value = v[u].normalised * 127.0; - if(ident.fields.sub_type == midi_pitchbend){ - value = ((uint16_t)(v[u].normalised * 16384.0)); + if(ident.fields.sub_type == midi_pitchbend + || ident.fields.sub_type == midi_nrpn + || ident.fields.sub_type == midi_rpn){ + value = ((uint16_t)(v[u].normalised * 16383.0)); } + if(mmjack_midiqueue_append(data->port + ident.fields.port, ident, value)){ pthread_mutex_unlock(&data->port[ident.fields.port].lock); return 1; @@ -494,8 +539,10 @@ static void mmjack_handle_midi(instance* inst, size_t index, mmjack_port* port){ port->queue[u].ident.fields.port = index; chan = mm_channel(inst, port->queue[u].ident.label, 0); if(chan){ - if(port->queue[u].ident.fields.sub_type == midi_pitchbend){ - val.normalised = ((double)port->queue[u].raw) / 16384.0; + if(port->queue[u].ident.fields.sub_type == midi_pitchbend + || port->queue[u].ident.fields.sub_type == midi_rpn + || port->queue[u].ident.fields.sub_type == midi_nrpn){ + val.normalised = ((double)port->queue[u].raw) / 16383.0; } else{ val.normalised = ((double)port->queue[u].raw) / 127.0; diff --git a/backends/jack.h b/backends/jack.h index 03ce052..ca62ea5 100644 --- a/backends/jack.h +++ b/backends/jack.h @@ -22,16 +22,17 @@ enum /*mmjack_midi_channel_type*/ { midi_cc = 0xB0, midi_pressure = 0xA0, midi_aftertouch = 0xD0, - midi_pitchbend = 0xE0 + midi_pitchbend = 0xE0, + midi_rpn = 0xF0, + midi_nrpn = 0xF1 }; typedef union { struct { uint32_t port; - uint8_t pad; uint8_t sub_type; uint8_t sub_channel; - uint8_t sub_control; + uint16_t sub_control; } fields; uint64_t label; } mmjack_channel_ident; @@ -70,6 +71,8 @@ typedef struct /*_jack_instance_data*/ { char* client_name; int fd; + uint8_t midi_epn_tx_short; + jack_client_t* client; size_t ports; mmjack_port* port; diff --git a/backends/jack.md b/backends/jack.md index b6ff5a9..3d426f3 100644 --- a/backends/jack.md +++ b/backends/jack.md @@ -16,6 +16,7 @@ transport of control data via either JACK midi ports or control voltage (CV) inp |---------------|-----------------------|-----------------------|-----------------------| | `name` | `Controller` | `MIDIMonster` | Client name for the JACK connection | | `server` | `jackserver` | `default` | JACK server identifier to connect to | +| `epn-tx` | `short` | `full` | Configure whether to clear the active parameter number after transmitting a MIDI `nrpn` or `rpn` parameter. | Channels (corresponding to JACK ports) need to be configured with their type and, if applicable, value limits. To configure a port, specify it in the instance configuration using the following syntax: @@ -65,6 +66,8 @@ The following values are recognized for `type`: * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages +* `rpn` - Registered parameter numbers (14-bit extension) +* `nrpn` - Non-registered parameter numbers (14-bit extension) The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. @@ -72,6 +75,7 @@ Example mappings: ``` jack1.cv_in > jack1.midi_out.ch0.note3 jack1.midi_in.ch0.pitch > jack1.cv_out +jack2.midi_in.ch0.nrpn900 > jack1.midi_out.ch1.rpn1 ``` The MIDI subchannel syntax is intentionally kept compatible to the different MIDI backends also supported @@ -79,6 +83,9 @@ by the MIDIMonster #### Known bugs / problems +Extended parameter numbers (`rpn` and `nrpn` control types) can currently only be transmitted, not properly +received as such. Support for this functionality is planned. + While JACK has rudimentary capabilities for transporting OSC messages, configuring and parsing such channels with this backend would take a great amount of dedicated syntax & code. CV ports can provide fine-grained single control channels as an alternative to MIDI. This feature may be implemented at some point in the future. -- cgit v1.2.3 From 50ce7276315671b72ecbfbcb7bebe6d34654639a Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 4 Jan 2021 01:16:28 +0100 Subject: Implement EPN reception for the midi backend --- backends/midi.c | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++----- backends/midi.h | 10 ++++-- backends/midi.md | 7 ++-- 3 files changed, 110 insertions(+), 12 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index d581a01..7883662 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -273,11 +273,91 @@ static char* midi_type_name(uint8_t type){ return "unknown"; } +static void midi_handle_epn(instance* inst, uint8_t chan, uint16_t control, uint16_t value){ + midi_instance_data* data = (midi_instance_data*) inst->impl; + midi_channel_ident ident = { + .label = 0 + }; + channel* changed = NULL; + channel_value val; + //check for 3-byte update TODO + + //switching between nrpn and rpn clears all valid bits + if(((data->epn_status[chan] & EPN_NRPN) && (control == 101 || control == 100)) + || (!(data->epn_status[chan] & EPN_NRPN) && (control == 99 || control == 98))){ + data->epn_status[chan] &= ~(EPN_NRPN | EPN_PARAMETER_LO | EPN_PARAMETER_HI); + } + + //setting an address always invalidates the value valid bits + if(control >= 98 && control <= 101){ + data->epn_status[chan] &= ~(EPN_VALUE_HI /*| EPN_VALUE_LO*/); + } + + //parameter hi + if(control == 101 || control == 99){ + data->epn_control[chan] &= 0x7F; + data->epn_control[chan] |= value << 7; + data->epn_status[chan] |= EPN_PARAMETER_HI | ((control == 99) ? EPN_NRPN : 0); + if(control == 101 && value == 127){ + data->epn_status[chan] &= ~EPN_PARAMETER_HI; + } + } + + //parameter lo + if(control == 100 || control == 98){ + data->epn_control[chan] &= ~0x7F; + data->epn_control[chan] |= value & 0x7F; + data->epn_status[chan] |= EPN_PARAMETER_LO | ((control == 98) ? EPN_NRPN : 0); + if(control == 100 && value == 127){ + data->epn_status[chan] &= ~EPN_PARAMETER_LO; + } + } + + //value hi, clears low, mark as update candidate + if(control == 6 + //check if parameter is set before accepting value update + && ((data->epn_status[chan] & (EPN_PARAMETER_HI | EPN_PARAMETER_LO)) == (EPN_PARAMETER_HI | EPN_PARAMETER_LO))){ + data->epn_value[chan] = value << 7; + data->epn_status[chan] |= EPN_VALUE_HI; + } + + //FIXME is the update order for the value bits fixed? + //FIXME can there be standalone updates on CC 38? + + //value lo, flush the value + if(control == 38 + && data->epn_status[chan] & EPN_VALUE_HI){ + data->epn_value[chan] &= ~0x7F; + data->epn_value[chan] |= value & 0x7F; + //FIXME not clearing the valid bit would allow for fast low-order updates + data->epn_status[chan] &= ~EPN_VALUE_HI; + + if(midi_config.detect){ + LOGPF("Incoming EPN data on channel %s.ch%d.%s%d", inst->name, chan, data->epn_status[chan] & EPN_NRPN ? "nrpn" : "rpn", data->epn_control[chan]); + } + + //find the updated channel + ident.fields.type = data->epn_status[chan] & EPN_NRPN ? nrpn : rpn; + ident.fields.channel = chan; + ident.fields.control = data->epn_control[chan]; + val.normalised = (double) data->epn_value[chan] / 16383.0; + + //push the new value + changed = mm_channel(inst, ident.label, 0); + if(changed){ + mm_channel_event(changed, val); + } + } +} + static int midi_handle(size_t num, managed_fd* fds){ snd_seq_event_t* ev = NULL; instance* inst = NULL; + midi_instance_data* data = NULL; + channel* changed = NULL; channel_value val; + char* event_type = NULL; midi_channel_ident ident = { .label = 0 @@ -295,7 +375,14 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.fields.control = ev->data.note.note; val.normalised = (double) ev->data.note.velocity / 127.0; - //TODO (n)rpn RX + //scan for the instance before parsing incoming data, instance state is required for the EPN state machine + inst = mm_instance_find(BACKEND_NAME, ev->dest.port); + if(!inst){ + LOG("Delivered event did not match any instance"); + continue; + } + data = (midi_instance_data*) inst->impl; + switch(ev->type){ case SND_SEQ_EVENT_NOTEON: case SND_SEQ_EVENT_NOTEOFF: @@ -323,6 +410,15 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.fields.channel = ev->data.control.channel; ident.fields.control = ev->data.control.param; val.normalised = (double) ev->data.control.value / 127.0; + + //check for EPN CCs and update the state machine + if((ident.fields.control <= 101 && ident.fields.control >= 98) + || ident.fields.control == 6 + || ident.fields.control == 38 + //if the high-order value bits are set, forward any control to the state machine for the short update form + || data->epn_status[ident.fields.channel] & EPN_VALUE_HI){ + midi_handle_epn(inst, ident.fields.channel, ident.fields.control, ev->data.control.value); + } break; default: LOG("Ignored event of unsupported type"); @@ -330,13 +426,6 @@ static int midi_handle(size_t num, managed_fd* fds){ } event_type = midi_type_name(ident.fields.type); - inst = mm_instance_find(BACKEND_NAME, ev->dest.port); - if(!inst){ - //FIXME might want to return failure - LOG("Delivered event did not match any instance"); - continue; - } - changed = mm_channel(inst, ident.label, 0); if(changed){ if(mm_channel_event(changed, val)){ diff --git a/backends/midi.h b/backends/midi.h index 4e2ac09..51b4a30 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -10,14 +10,20 @@ static int midi_handle(size_t num, managed_fd* fds); static int midi_start(size_t n, instance** inst); static int midi_shutdown(size_t n, instance** inst); +#define EPN_NRPN 8 +#define EPN_PARAMETER_HI 4 +#define EPN_PARAMETER_LO 2 +#define EPN_VALUE_HI 1 + typedef struct /*_midi_instance_data*/ { int port; char* read; char* write; uint8_t epn_tx_short; - uint16_t epn_control; - uint16_t epn_value; + uint16_t epn_control[16]; + uint16_t epn_value[16]; + uint8_t epn_status[16]; } midi_instance_data; typedef union { diff --git a/backends/midi.md b/backends/midi.md index 4732452..87d06a1 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -56,8 +56,11 @@ midi1.ch0.nrpn900 > midi2.ch0.rpn1 ``` #### Known bugs / problems -Extended parameter numbers (`rpn` and `nrpn` control types) can currently only be transmitted, not properly -received as such. Support for this functionality is planned. +Extended parameter numbers (EPNs, the `rpn` and `nrpn` control types) will also generate events on the controls (CC 101 through +98, 38 and 6) that are used as the lower layer transport. When using EPNs, mapping those controls is probably not useful. + +EPN control types support only the full 14-bit transfer encoding, not the shorter variant transmitting only the 7 +high-order bits. This may be changed if there is sufficient interest in the functionality. To access MIDI data, the user running MIDIMonster needs read & write access to the ALSA sequencer. This can usually be done by adding this user to the `audio` system group. -- cgit v1.2.3 From 41cb85a842a696e1183e1d55116c99b63099fde3 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 8 Jan 2021 21:38:43 +0100 Subject: Implement EPN reception for the winmidi backend --- backends/midi.c | 2 + backends/midi.h | 2 +- backends/winmidi.c | 123 +++++++++++++++++++++++++++++++++++++++++++--------- backends/winmidi.h | 10 +++++ backends/winmidi.md | 7 ++- 5 files changed, 121 insertions(+), 23 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index 7883662..bddabb5 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -273,6 +273,7 @@ static char* midi_type_name(uint8_t type){ return "unknown"; } +//this state machine is used more-or-less verbatim in the winmidi and jack backends - fixes need to be applied there, too static void midi_handle_epn(instance* inst, uint8_t chan, uint16_t control, uint16_t value){ midi_instance_data* data = (midi_instance_data*) inst->impl; midi_channel_ident ident = { @@ -402,6 +403,7 @@ static int midi_handle(size_t num, managed_fd* fds){ break; case SND_SEQ_EVENT_PITCHBEND: ident.fields.type = pitchbend; + ident.fields.control = 0; ident.fields.channel = ev->data.control.channel; val.normalised = ((double) ev->data.control.value + 8192) / 16383.0; break; diff --git a/backends/midi.h b/backends/midi.h index 51b4a30..e2d6543 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -19,8 +19,8 @@ typedef struct /*_midi_instance_data*/ { int port; char* read; char* write; - uint8_t epn_tx_short; + uint8_t epn_tx_short; uint16_t epn_control[16]; uint16_t epn_value[16]; uint8_t epn_status[16]; diff --git a/backends/winmidi.c b/backends/winmidi.c index c89a098..66456e8 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -321,11 +321,98 @@ static int winmidi_handle(size_t num, managed_fd* fds){ return 0; } -static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DWORD_PTR inst, DWORD param1, DWORD param2){ +static int winmidi_enqueue_input(instance* inst, winmidi_channel_ident ident, channel_value val){ + EnterCriticalSection(&backend_config.push_events); + if(backend_config.events_alloc <= backend_config.events_active){ + backend_config.event = realloc((void*) backend_config.event, (backend_config.events_alloc + 1) * sizeof(winmidi_event)); + if(!backend_config.event){ + LOG("Failed to allocate memory"); + backend_config.events_alloc = 0; + backend_config.events_active = 0; + LeaveCriticalSection(&backend_config.push_events); + return 1; + } + backend_config.events_alloc++; + } + backend_config.event[backend_config.events_active].inst = inst; + backend_config.event[backend_config.events_active].channel.label = ident.label; + backend_config.event[backend_config.events_active].value = val; + backend_config.events_active++; + LeaveCriticalSection(&backend_config.push_events); + return 0; +} + +//this state machine was copied more-or-less verbatim from the alsa midi implementation - fixes there will need to be integrated +static void winmidi_handle_epn(instance* inst, uint8_t chan, uint16_t control, uint16_t value){ + winmidi_instance_data* data = (winmidi_instance_data*) inst->impl; winmidi_channel_ident ident = { .label = 0 }; channel_value val; + + //switching between nrpn and rpn clears all valid bits + if(((data->epn_status[chan] & EPN_NRPN) && (control == 101 || control == 100)) + || (!(data->epn_status[chan] & EPN_NRPN) && (control == 99 || control == 98))){ + data->epn_status[chan] &= ~(EPN_NRPN | EPN_PARAMETER_LO | EPN_PARAMETER_HI); + } + + //setting an address always invalidates the value valid bits + if(control >= 98 && control <= 101){ + data->epn_status[chan] &= ~EPN_VALUE_HI; + } + + //parameter hi + if(control == 101 || control == 99){ + data->epn_control[chan] &= 0x7F; + data->epn_control[chan] |= value << 7; + data->epn_status[chan] |= EPN_PARAMETER_HI | ((control == 99) ? EPN_NRPN : 0); + if(control == 101 && value == 127){ + data->epn_status[chan] &= ~EPN_PARAMETER_HI; + } + } + + //parameter lo + if(control == 100 || control == 98){ + data->epn_control[chan] &= ~0x7F; + data->epn_control[chan] |= value & 0x7F; + data->epn_status[chan] |= EPN_PARAMETER_LO | ((control == 98) ? EPN_NRPN : 0); + if(control == 100 && value == 127){ + data->epn_status[chan] &= ~EPN_PARAMETER_LO; + } + } + + //value hi, clears low, mark as update candidate + if(control == 6 + //check if parameter is set before accepting value update + && ((data->epn_status[chan] & (EPN_PARAMETER_HI | EPN_PARAMETER_LO)) == (EPN_PARAMETER_HI | EPN_PARAMETER_LO))){ + data->epn_value[chan] = value << 7; + data->epn_status[chan] |= EPN_VALUE_HI; + } + + //value lo, flush the value + if(control == 38 + && data->epn_status[chan] & EPN_VALUE_HI){ + data->epn_value[chan] &= ~0x7F; + data->epn_value[chan] |= value & 0x7F; + data->epn_status[chan] &= ~EPN_VALUE_HI; + + //find the updated channel + ident.fields.type = data->epn_status[chan] & EPN_NRPN ? nrpn : rpn; + ident.fields.channel = chan; + ident.fields.control = data->epn_control[chan]; + val.normalised = (double) data->epn_value[chan] / 16383.0; + + winmidi_enqueue_input(inst, ident,val); + } +} + +static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DWORD_PTR inst, DWORD param1, DWORD param2){ + winmidi_channel_ident ident = { + .label = 0 + }; + channel_value val = { + 0 + }; union { struct { uint8_t status; @@ -341,7 +428,6 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW //callbacks may run on different threads, so we queue all events and alert the main thread via the feedback socket DBGPF("Input callback on thread %ld", GetCurrentThreadId()); - //TODO handle (n)rpn RX switch(message){ case MIM_MOREDATA: //processing too slow, do not immediately alert the main loop @@ -352,18 +438,22 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW ident.fields.type = input.components.status & 0xF0; ident.fields.control = input.components.data1; val.normalised = (double) input.components.data2 / 127.0; + val.raw.u64 = input.components.data2; if(ident.fields.type == 0x80){ ident.fields.type = note; val.normalised = 0; + val.raw.u64 = 0; } else if(ident.fields.type == pitchbend){ ident.fields.control = 0; - val.normalised = (double)((input.components.data2 << 7) | input.components.data1) / 16384.0; + val.normalised = (double) ((input.components.data2 << 7) | input.components.data1) / 16383.0; + val.raw.u64 = input.components.data2 << 7 | input.components.data1; } else if(ident.fields.type == aftertouch){ ident.fields.control = 0; val.normalised = (double) input.components.data1 / 127.0; + val.raw.u64 = input.components.data1; } break; case MIM_LONGDATA: @@ -379,26 +469,19 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW return; } + //pass changes in the (n)rpn CCs to the EPN state machine + if(ident.fields.type == cc + && ((ident.fields.control <= 101 && ident.fields.control >= 98) + || ident.fields.control == 6 + || ident.fields.control == 38)){ + winmidi_handle_epn((instance*) inst, ident.fields.channel, ident.fields.control, val.raw.u64); + } + DBGPF("Incoming message type %d channel %d control %d value %f", ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised); - - EnterCriticalSection(&backend_config.push_events); - if(backend_config.events_alloc <= backend_config.events_active){ - backend_config.event = realloc((void*) backend_config.event, (backend_config.events_alloc + 1) * sizeof(winmidi_event)); - if(!backend_config.event){ - LOG("Failed to allocate memory"); - backend_config.events_alloc = 0; - backend_config.events_active = 0; - LeaveCriticalSection(&backend_config.push_events); - return; - } - backend_config.events_alloc++; + if(winmidi_enqueue_input((instance*) inst, ident, val)){ + LOG("Failed to enqueue incoming data"); } - backend_config.event[backend_config.events_active].inst = (instance*) inst; - backend_config.event[backend_config.events_active].channel.label = ident.label; - backend_config.event[backend_config.events_active].value = val; - backend_config.events_active++; - LeaveCriticalSection(&backend_config.push_events); if(message != MIM_MOREDATA){ //alert the main loop diff --git a/backends/winmidi.h b/backends/winmidi.h index fbb2c94..4d3e2dd 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -10,10 +10,20 @@ static int winmidi_handle(size_t num, managed_fd* fds); static int winmidi_start(size_t n, instance** inst); static int winmidi_shutdown(size_t n, instance** inst); +#define EPN_NRPN 8 +#define EPN_PARAMETER_HI 4 +#define EPN_PARAMETER_LO 2 +#define EPN_VALUE_HI 1 + typedef struct /*_winmidi_instance_data*/ { char* read; char* write; + uint8_t epn_tx_short; + uint16_t epn_control[16]; + uint16_t epn_value[16]; + uint8_t epn_status[16]; + HMIDIIN device_in; HMIDIOUT device_out; } winmidi_instance_data; diff --git a/backends/winmidi.md b/backends/winmidi.md index 6b0fa98..be14424 100644 --- a/backends/winmidi.md +++ b/backends/winmidi.md @@ -59,8 +59,11 @@ midi2.ch0.nrpn900 > midi1.ch1.rpn1 #### Known bugs / problems -Extended parameter numbers (`rpn` and `nrpn` control types) can currently only be transmitted, not properly -received as such. Support for this functionality is planned. +Extended parameter numbers (EPNs, the `rpn` and `nrpn` control types) will also generate events on the controls (CC 101 through +98, 38 and 6) that are used as the lower layer transport. When using EPNs, mapping those controls is probably not useful. + +EPN control types support only the full 14-bit transfer encoding, not the shorter variant transmitting only the 7 +high-order bits. This may be changed if there is sufficient interest in the functionality. Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are generated, which amount to the same thing according to the spec). This may be implemented as -- cgit v1.2.3 From 00ba26c238a2e75c5b7d2e32469eae02179efde9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 8 Jan 2021 23:03:11 +0100 Subject: Implement EPN reception for the jack backend --- TODO | 1 + backends/jack.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++- backends/jack.h | 10 ++++++++ backends/jack.md | 7 ++++-- backends/winmidi.c | 2 +- 5 files changed, 88 insertions(+), 4 deletions(-) diff --git a/TODO b/TODO index 5662479..2ab5f10 100644 --- a/TODO +++ b/TODO @@ -5,6 +5,7 @@ make event collectors threadsafe to stop marshalling data... collect & check backend API version move all connection establishment to _start to be able to hot-stop/start all backends event deduplication in core? +move all typenames to _t per-channel filters * invert diff --git a/backends/jack.c b/backends/jack.c index a3caf73..176144f 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -101,6 +101,68 @@ static void mmjack_process_midiout(void* buffer, size_t sample_offset, uint8_t t } } +//this state machine was copied more-or-less verbatim from the alsa midi implementation - fixes there will need to be integrated +static void mmjack_handle_epn(mmjack_port* port, uint8_t chan, uint16_t control, uint16_t value){ + mmjack_channel_ident ident = { + .label = 0 + }; + + //switching between nrpn and rpn clears all valid bits + if(((port->epn_status[chan] & EPN_NRPN) && (control == 101 || control == 100)) + || (!(port->epn_status[chan] & EPN_NRPN) && (control == 99 || control == 98))){ + port->epn_status[chan] &= ~(EPN_NRPN | EPN_PARAMETER_LO | EPN_PARAMETER_HI); + } + + //setting an address always invalidates the value valid bits + if(control >= 98 && control <= 101){ + port->epn_status[chan] &= ~EPN_VALUE_HI; + } + + //parameter hi + if(control == 101 || control == 99){ + port->epn_control[chan] &= 0x7F; + port->epn_control[chan] |= value << 7; + port->epn_status[chan] |= EPN_PARAMETER_HI | ((control == 99) ? EPN_NRPN : 0); + if(control == 101 && value == 127){ + port->epn_status[chan] &= ~EPN_PARAMETER_HI; + } + } + + //parameter lo + if(control == 100 || control == 98){ + port->epn_control[chan] &= ~0x7F; + port->epn_control[chan] |= value & 0x7F; + port->epn_status[chan] |= EPN_PARAMETER_LO | ((control == 98) ? EPN_NRPN : 0); + if(control == 100 && value == 127){ + port->epn_status[chan] &= ~EPN_PARAMETER_LO; + } + } + + //value hi, clears low, mark as update candidate + if(control == 6 + //check if parameter is set before accepting value update + && ((port->epn_status[chan] & (EPN_PARAMETER_HI | EPN_PARAMETER_LO)) == (EPN_PARAMETER_HI | EPN_PARAMETER_LO))){ + port->epn_value[chan] = value << 7; + port->epn_status[chan] |= EPN_VALUE_HI; + } + + //value lo, flush the value + if(control == 38 + && port->epn_status[chan] & EPN_VALUE_HI){ + port->epn_value[chan] &= ~0x7F; + port->epn_value[chan] |= value & 0x7F; + port->epn_status[chan] &= ~EPN_VALUE_HI; + + //find the updated channel + ident.fields.sub_type = port->epn_status[chan] & EPN_NRPN ? midi_nrpn : midi_rpn; + ident.fields.sub_channel = chan; + ident.fields.sub_control = port->epn_control[chan]; + + //ident.fields.port set on output in mmjack_handle_midi + mmjack_midiqueue_append(port, ident, port->epn_value[chan]); + } +} + static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; void* buffer = jack_port_get_buffer(port->port, nframes); @@ -113,7 +175,6 @@ static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes if(port->input){ if(event_count){ DBGPF("Reading %u MIDI events from port %s", event_count, port->name); - //TODO (n)rpn RX for(u = 0; u < event_count; u++){ ident.label = 0; //read midi data from stream @@ -135,6 +196,15 @@ static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes ident.fields.sub_control = 0; value = event.buffer[1]; } + + //forward the EPN CCs to the EPN state machine + if(ident.fields.sub_type == midi_cc + && ((ident.fields.sub_control <= 101 && ident.fields.sub_control >= 98) + || ident.fields.sub_control == 6 + || ident.fields.sub_control == 38)){ + mmjack_handle_epn(port, ident.fields.sub_channel, ident.fields.sub_control, value); + } + //append midi data mmjack_midiqueue_append(port, ident, value); } diff --git a/backends/jack.h b/backends/jack.h index ca62ea5..762282b 100644 --- a/backends/jack.h +++ b/backends/jack.h @@ -16,6 +16,11 @@ static int mmjack_shutdown(size_t n, instance** inst); #define JACK_DEFAULT_SERVER_NAME "default" #define JACK_MIDIQUEUE_CHUNK 10 +#define EPN_NRPN 8 +#define EPN_PARAMETER_HI 4 +#define EPN_PARAMETER_LO 2 +#define EPN_VALUE_HI 1 + enum /*mmjack_midi_channel_type*/ { midi_none = 0, midi_note = 0x90, @@ -59,10 +64,15 @@ typedef struct /*_mmjack_port_data*/ { double min; uint8_t mark; double last; + size_t queue_len; size_t queue_alloc; mmjack_midiqueue* queue; + uint16_t epn_control[16]; + uint16_t epn_value[16]; + uint8_t epn_status[16]; + pthread_mutex_t lock; } mmjack_port; diff --git a/backends/jack.md b/backends/jack.md index 3d426f3..4ff77f6 100644 --- a/backends/jack.md +++ b/backends/jack.md @@ -83,8 +83,11 @@ by the MIDIMonster #### Known bugs / problems -Extended parameter numbers (`rpn` and `nrpn` control types) can currently only be transmitted, not properly -received as such. Support for this functionality is planned. +MIDI extended parameter numbers (EPNs, the `rpn` and `nrpn` control types) will also generate events on the controls (CC 101 through +98, 38 and 6) that are used as the lower layer transport. When using EPNs, mapping those controls is probably not useful. + +EPN control types support only the full 14-bit transfer encoding, not the shorter variant transmitting only the 7 +high-order bits. This may be changed if there is sufficient interest in the functionality. While JACK has rudimentary capabilities for transporting OSC messages, configuring and parsing such channels with this backend would take a great amount of dedicated syntax & code. CV ports can provide fine-grained single diff --git a/backends/winmidi.c b/backends/winmidi.c index 66456e8..d12dc71 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -402,7 +402,7 @@ static void winmidi_handle_epn(instance* inst, uint8_t chan, uint16_t control, u ident.fields.control = data->epn_control[chan]; val.normalised = (double) data->epn_value[chan] / 16383.0; - winmidi_enqueue_input(inst, ident,val); + winmidi_enqueue_input(inst, ident, val); } } -- cgit v1.2.3 From cbef6a61a92453afba5005c287873001354f5090 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 9 Jan 2021 11:36:09 +0100 Subject: Implement keepalive and protocol fallback --- backends/mqtt.c | 56 +++++++++++++++++++++++++++++++++++++------------------- backends/mqtt.h | 5 ++++- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/backends/mqtt.c b/backends/mqtt.c index d42d1f5..e9493a9 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -135,6 +135,7 @@ static size_t mqtt_push_utf8(uint8_t* buffer, size_t buffer_length, char* conten static void mqtt_disconnect(instance* inst){ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl; + data->last_control = 0; //unmanage the fd mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL); @@ -153,13 +154,14 @@ static int mqtt_transmit(instance* inst, uint8_t type, size_t vh_length, uint8_t offset += mqtt_push_varint(vh_length + payload_length, sizeof(fixed_header) - offset, fixed_header + offset); if(mmbackend_send(data->fd, fixed_header, offset) - || mmbackend_send(data->fd, vh, vh_length) - || mmbackend_send(data->fd, payload, payload_length)){ + || (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; } @@ -169,7 +171,7 @@ static int mqtt_configure(char* option, char* value){ } static int mqtt_reconnect(instance* inst){ - uint8_t variable_header[MQTT_BUFFER_LENGTH] = {0x00, 0x04, 'M', 'Q', 'T', 'T', MQTT_VERSION, 0x00 /*flags*/, (MQTT_KEEPALIVE >> 8) & 0xFF, MQTT_KEEPALIVE & 0xFF}; + 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; @@ -198,25 +200,28 @@ static int mqtt_reconnect(instance* inst){ return 1; } - //prepare CONNECT message flags + //prepare CONNECT message header + variable_header[6] = data->mqtt_version; variable_header[7] = 0x02 /*clean start*/ | (data->user ? 0x80 : 0x00) | (data->user ? 0x40 : 0x00); //TODO set session expiry interval option //TODO re-use previos session on reconnect - //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; + 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 @@ -279,6 +284,7 @@ static int mqtt_instance(instance* inst){ } data->fd = -1; + data->mqtt_version = MQTT_VERSION_DEFAULT; inst->impl = data; if(mqtt_generate_instanceid(inst)){ @@ -317,6 +323,10 @@ static int mqtt_maintenance(){ 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); @@ -324,10 +334,17 @@ static int mqtt_maintenance(){ } static int mqtt_handle_message(instance* inst, uint8_t type, uint8_t* variable_header, size_t length){ + mqtt_instance_data* data = (mqtt_instance_data*) inst->impl; + switch(type){ case MSG_CONNACK: 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]); } else{ @@ -335,6 +352,9 @@ static int mqtt_handle_message(instance* inst, uint8_t type, uint8_t* variable_h } } break; + case MSG_PINGRESP: + //ignore ping responses + break; default: LOGPF("Unhandled MQTT message type 0x%02X on %s", type, inst->name); } @@ -379,13 +399,11 @@ static int mqtt_handle_fd(instance* inst){ } } - data->receive_offset += bytes_read; return 0; } static int mqtt_handle(size_t num, managed_fd* fds){ size_t n = 0; - int rv = 0; for(n = 0; n < num; n++){ if(mqtt_handle_fd((instance*) fds[n].impl) >= 2){ diff --git a/backends/mqtt.h b/backends/mqtt.h index df63319..a0f5356 100644 --- a/backends/mqtt.h +++ b/backends/mqtt.h @@ -14,7 +14,7 @@ static int mqtt_shutdown(size_t n, instance** inst); #define MQTT_TLS_PORT "8883" #define MQTT_BUFFER_LENGTH 8192 #define MQTT_KEEPALIVE 10 -#define MQTT_VERSION 0x05 +#define MQTT_VERSION_DEFAULT 0x05 enum { MSG_RESERVED = 0x00, @@ -39,6 +39,7 @@ typedef struct /*_mqtt_instance_data*/ { uint8_t tls; char* host; char* port; + uint8_t mqtt_version; char* user; char* password; @@ -50,6 +51,8 @@ typedef struct /*_mqtt_instance_data*/ { int fd; uint8_t receive_buffer[MQTT_BUFFER_LENGTH]; size_t receive_offset; + + uint64_t last_control; } mqtt_instance_data; //per-channel -- cgit v1.2.3 From 7902842bd10b17d8d5b6bfc586f440299646d48c Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 9 Jan 2021 15:21:24 +0100 Subject: Add source/target as aliases for MIDI read/write config options (#79) --- backends/midi.c | 4 ++-- backends/midi.md | 10 +++++----- backends/winmidi.c | 4 ++-- backends/winmidi.md | 10 +++++----- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index bddabb5..e32c975 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -83,7 +83,7 @@ static int midi_configure_instance(instance* inst, char* option, char* value){ midi_instance_data* data = (midi_instance_data*) inst->impl; //FIXME maybe allow connecting more than one device - if(!strcmp(option, "read")){ + if(!strcmp(option, "read") || !strcmp(option, "source")){ //connect input device if(data->read){ LOGPF("Instance %s was already connected to an input device", inst->name); @@ -92,7 +92,7 @@ static int midi_configure_instance(instance* inst, char* option, char* value){ data->read = strdup(value); return 0; } - else if(!strcmp(option, "write")){ + else if(!strcmp(option, "write") || !strcmp(option, "target")){ //connect output device if(data->write){ LOGPF("Instance %s was already connected to an output device", inst->name); diff --git a/backends/midi.md b/backends/midi.md index 87d06a1..60a4d06 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -11,11 +11,11 @@ The MIDI backend provides read-write access to the MIDI protocol via virtual por #### Instance configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `read` | `20:0` | none | MIDI device to connect for input | -| `write` | `DeviceName` | none | MIDI device to connect for output | -| `epn-tx` | `short` | `full` | Configures whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter | +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------| +| `read` / `source` | `20:0` | none | MIDI device to connect for input | +| `write` / `target` | `DeviceName` | none | MIDI device to connect for output | +| `epn-tx` | `short` | `full` | Configures whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter | MIDI device names may either be `client:port` portnames or prefixes of MIDI device names. Run `aconnect -i` to list input ports and `aconnect -o` to list output ports. diff --git a/backends/winmidi.c b/backends/winmidi.c index d12dc71..ec0fb44 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -74,7 +74,7 @@ static int winmidi_configure(char* option, char* value){ static int winmidi_configure_instance(instance* inst, char* option, char* value){ winmidi_instance_data* data = (winmidi_instance_data*) inst->impl; - if(!strcmp(option, "read")){ + if(!strcmp(option, "read") || !strcmp(option, "source")){ if(data->read){ LOGPF("Instance %s already connected to an input device", inst->name); return 1; @@ -82,7 +82,7 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value) data->read = strdup(value); return 0; } - else if(!strcmp(option, "write")){ + else if(!strcmp(option, "write") || !strcmp(option, "target")){ if(data->write){ LOGPF("Instance %s already connected to an output device", inst->name); return 1; diff --git a/backends/winmidi.md b/backends/winmidi.md index be14424..4de792a 100644 --- a/backends/winmidi.md +++ b/backends/winmidi.md @@ -15,11 +15,11 @@ some deviations may still be present. #### Instance configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `read` | `2` | none | MIDI device to connect for input | -| `write` | `DeviceName` | none | MIDI device to connect for output | -| `epn-tx` | `short` | `full` | Configure whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter. | +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------| +| `read` / `source` | `2` | none | MIDI device to connect for input | +| `write` / `target` | `DeviceName` | none | MIDI device to connect for output | +| `epn-tx` | `short` | `full` | Configure whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter. | Input/output device names may either be prefixes of MIDI device names or numeric indices corresponding to the listing shown at startup when using the global `list` option. -- cgit v1.2.3 From 6334a78d0c475ebfa76a739577a561bded135086 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 10 Jan 2021 09:42:54 +0100 Subject: Subscribe to input channels --- backends/mqtt.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++------- backends/mqtt.h | 17 +++++++++---- 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/backends/mqtt.c b/backends/mqtt.c index e9493a9..d3e516f 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -9,9 +9,6 @@ static uint64_t last_maintenance = 0; -//TODO -// * Periodic connection retries - MM_PLUGIN_API int init(){ backend mqtt = { .name = BACKEND_NAME, @@ -80,7 +77,7 @@ static int mqtt_parse_hostspec(instance* inst, char* hostspec){ static int mqtt_generate_instanceid(instance* inst){ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl; - char clientid[23] = ""; + char clientid[24] = ""; snprintf(clientid, sizeof(clientid), "MIDIMonster-%d-%s", (uint32_t) time(NULL), inst->name); return mmbackend_strdup(&(data->client_id), clientid); @@ -275,6 +272,32 @@ static int mqtt_configure_instance(instance* inst, char* option, char* value){ return 1; } +static int mqtt_push_subscriptions(instance* inst){ + mqtt_instance_data* data = calloc(1, sizeof(mqtt_instance_data)); + 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){ + 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++; + 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)); @@ -294,12 +317,47 @@ static int mqtt_instance(instance* inst){ } static channel* mqtt_channel(instance* inst, char* spec, uint8_t flags){ - //TODO - return NULL; + 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; + 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 = 0; + data->channel[u].flags = flags; + + if(!data->channel[u].topic){ + LOG("Failed to allocate memory"); + return NULL; + } + data->nchannels++; + } + + return mm_channel(inst, u, 1); } static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){ - //TODO + //TODO mqtt v3.1.1 local filtering return 0; } @@ -349,6 +407,7 @@ static int mqtt_handle_message(instance* inst, uint8_t type, uint8_t* variable_h } else{ LOGPF("Connection on %s established", inst->name); + return mqtt_push_subscriptions(inst); } } break; @@ -424,7 +483,7 @@ static int mqtt_handle(size_t num, managed_fd* fds){ } static int mqtt_start(size_t n, instance** inst){ - size_t u = 0; + size_t u = 0, fds = 0; for(u = 0; u < n; u++){ switch(mqtt_reconnect(inst[u])){ @@ -435,9 +494,11 @@ static int mqtt_start(size_t n, instance** inst){ 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(); @@ -453,7 +514,7 @@ static int mqtt_shutdown(size_t n, instance** inst){ mqtt_disconnect(inst[u]); for(p = 0; p < data->nchannels; p++){ - free(data->channel[p]); + free(data->channel[p].topic); } free(data->channel); free(data->host); diff --git a/backends/mqtt.h b/backends/mqtt.h index a0f5356..c9bce81 100644 --- a/backends/mqtt.h +++ b/backends/mqtt.h @@ -16,6 +16,8 @@ static int mqtt_shutdown(size_t n, instance** inst); #define MQTT_KEEPALIVE 10 #define MQTT_VERSION_DEFAULT 0x05 +#define MQTT5_NO_LOCAL 0x04 + enum { MSG_RESERVED = 0x00, MSG_CONNECT = 0x10, @@ -25,7 +27,7 @@ enum { MSG_PUBREC = 0x50, MSG_PUBREL = 0x60, MSG_PUBCOMP = 0x70, - MSG_SUBSCRIBE = 0x80, + MSG_SUBSCRIBE = 0x82, MSG_SUBACK = 0x90, MSG_UNSUBSCRIBE = 0xA0, MSG_UNSUBACK = 0xB0, @@ -35,6 +37,13 @@ enum { MSG_AUTH = 0xF0 }; +//qos, subscribe +typedef struct /*_mqtt_channel*/ { + char* topic; + uint16_t topic_alias; + uint8_t flags; +} mqtt_channel_data; + typedef struct /*_mqtt_instance_data*/ { uint8_t tls; char* host; @@ -46,14 +55,12 @@ typedef struct /*_mqtt_instance_data*/ { char* client_id; size_t nchannels; - char** channel; + 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; } mqtt_instance_data; - -//per-channel -//qos, subscribe -- cgit v1.2.3 From a3a893f6b8b6c10ff281fcdfe0b4a4ddafe89023 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 10 Jan 2021 17:30:12 +0100 Subject: Implement program change control for the midi, winmidi and jack backends (Fixes #79) --- backends/jack.c | 9 ++++++--- backends/jack.h | 7 ++++--- backends/jack.md | 7 ++++++- backends/midi.c | 18 +++++++++++++++++- backends/midi.md | 4 +++- backends/winmidi.c | 12 +++++++++--- backends/winmidi.h | 7 ++++--- backends/winmidi.md | 4 +++- 8 files changed, 52 insertions(+), 16 deletions(-) diff --git a/backends/jack.c b/backends/jack.c index 176144f..fe74a80 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -79,7 +79,7 @@ static int mmjack_midiqueue_append(mmjack_port* port, mmjack_channel_ident ident } static void mmjack_process_midiout(void* buffer, size_t sample_offset, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){ - jack_midi_data_t* event_data = jack_midi_event_reserve(buffer, sample_offset, (type == midi_aftertouch) ? 2 : 3); + jack_midi_data_t* event_data = jack_midi_event_reserve(buffer, sample_offset, (type == midi_aftertouch || type == midi_program) ? 2 : 3); if(!event_data){ LOG("Failed to reserve MIDI stream data"); @@ -95,7 +95,7 @@ static void mmjack_process_midiout(void* buffer, size_t sample_offset, uint8_t t event_data[1] = value & 0x7F; event_data[2] = (value >> 7) & 0x7F; } - else if(type == midi_aftertouch){ + else if(type == midi_aftertouch || type == midi_program){ event_data[1] = value & 0x7F; event_data[2] = 0; } @@ -192,7 +192,7 @@ static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes ident.fields.sub_control = 0; value = event.buffer[1] | (event.buffer[2] << 7); } - else if(ident.fields.sub_type == midi_aftertouch){ + else if(ident.fields.sub_type == midi_aftertouch || ident.fields.sub_type == midi_program){ ident.fields.sub_control = 0; value = event.buffer[1]; } @@ -501,6 +501,9 @@ static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){ else if(!strncmp(next_token, "aftertouch", 10)){ ident->fields.sub_type = midi_aftertouch; } + else if(!strncmp(next_token, "program", 7)){ + ident->fields.sub_type = midi_program; + } else{ LOGPF("Unknown MIDI control type in spec %s", spec); return 1; diff --git a/backends/jack.h b/backends/jack.h index 762282b..42905f1 100644 --- a/backends/jack.h +++ b/backends/jack.h @@ -24,12 +24,13 @@ static int mmjack_shutdown(size_t n, instance** inst); enum /*mmjack_midi_channel_type*/ { midi_none = 0, midi_note = 0x90, - midi_cc = 0xB0, midi_pressure = 0xA0, + midi_cc = 0xB0, + midi_program = 0xC0, midi_aftertouch = 0xD0, midi_pitchbend = 0xE0, - midi_rpn = 0xF0, - midi_nrpn = 0xF1 + midi_rpn = 0xF1, + midi_nrpn = 0xF2 }; typedef union { diff --git a/backends/jack.md b/backends/jack.md index 4ff77f6..c67f060 100644 --- a/backends/jack.md +++ b/backends/jack.md @@ -56,6 +56,9 @@ MIDI ports provide subchannels for the various MIDI controls available. Each MID corresponding pressure controls for each note, 128 control change (CC) controls (numbered likewise), one channel wide "aftertouch" control and one channel-wide pitchbend control. +Every MIDI channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using +the CC controls 101/100/99/98/38/6. Both control types have 14-bit IDs and 14-bit values. + A MIDI port subchannel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). @@ -66,16 +69,18 @@ The following values are recognized for `type`: * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages +* `program` - Channel program change messages * `rpn` - Registered parameter numbers (14-bit extension) * `nrpn` - Non-registered parameter numbers (14-bit extension) -The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. +The `pitch`, `aftertouch` and `program` messages/events are channel-wide, thus they can be specified as `channel.`. Example mappings: ``` jack1.cv_in > jack1.midi_out.ch0.note3 jack1.midi_in.ch0.pitch > jack1.cv_out jack2.midi_in.ch0.nrpn900 > jack1.midi_out.ch1.rpn1 +jack1.midi_in.ch15.note1 > jack1.midi_out.ch4.program ``` The MIDI subchannel syntax is intentionally kept compatible to the different MIDI backends also supported diff --git a/backends/midi.c b/backends/midi.c index e32c975..10c8c4a 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -14,6 +14,7 @@ enum /*_midi_channel_type*/ { pressure, aftertouch, pitchbend, + program, rpn, nrpn }; @@ -167,6 +168,9 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ else if(!strncmp(channel, "pitch", 5)){ ident.fields.type = pitchbend; } + else if(!strncmp(channel, "program", 7)){ + ident.fields.type = program; + } else if(!strncmp(channel, "aftertouch", 10)){ ident.fields.type = aftertouch; } @@ -208,6 +212,9 @@ static void midi_tx(int port, uint8_t type, uint8_t channel, uint8_t control, ui case aftertouch: snd_seq_ev_set_chanpress(&ev, channel, value); break; + case program: + snd_seq_ev_set_pgmchange(&ev, channel, value); + break; } snd_seq_event_output(sequencer, &ev); @@ -269,6 +276,8 @@ static char* midi_type_name(uint8_t type){ return "aftertouch"; case pitchbend: return "pitch"; + case program: + return "program"; } return "unknown"; } @@ -399,6 +408,7 @@ static int midi_handle(size_t num, managed_fd* fds){ case SND_SEQ_EVENT_CHANPRESS: ident.fields.type = aftertouch; ident.fields.channel = ev->data.control.channel; + ident.fields.control = 0; val.normalised = (double) ev->data.control.value / 127.0; break; case SND_SEQ_EVENT_PITCHBEND: @@ -407,6 +417,12 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.fields.channel = ev->data.control.channel; val.normalised = ((double) ev->data.control.value + 8192) / 16383.0; break; + case SND_SEQ_EVENT_PGMCHANGE: + ident.fields.type = program; + ident.fields.control = 0; + ident.fields.channel = ev->data.control.channel; + val.normalised = (double) ev->data.control.value / 127.0; + break; case SND_SEQ_EVENT_CONTROLLER: ident.fields.type = cc; ident.fields.channel = ev->data.control.channel; @@ -437,7 +453,7 @@ static int midi_handle(size_t num, managed_fd* fds){ } if(midi_config.detect && event_type){ - if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){ + if(ident.fields.type == pitchbend || ident.fields.type == aftertouch || ident.fields.type == program){ LOGPF("Incoming data on channel %s.ch%d.%s", inst->name, ident.fields.channel, event_type); } else{ diff --git a/backends/midi.md b/backends/midi.md index 60a4d06..6280205 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -31,13 +31,14 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages +* `program` - Channel program change messages * `rpn` - Registered parameter numbers (14-bit extension) * `nrpn` - Non-registered parameter numbers (14-bit extension) A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). -The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. +The `pitch`, `aftertouch` and `program` messages/events are channel-wide, thus they can be specified as `channel.`. MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called @@ -53,6 +54,7 @@ midi1.channel15.pressure1 > midi1.channel0.note0 midi1.ch1.aftertouch > midi2.ch2.cc0 midi1.ch0.pitch > midi2.ch1.pitch midi1.ch0.nrpn900 > midi2.ch0.rpn1 +midi2.ch15.note1 > midi1.ch2.program ``` #### Known bugs / problems diff --git a/backends/winmidi.c b/backends/winmidi.c index ec0fb44..a1fa686 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -169,6 +169,9 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ else if(!strncmp(next_token, "aftertouch", 10)){ ident.fields.type = aftertouch; } + else if(!strncmp(next_token, "program", 7)){ + ident.fields.type = program; + } else{ LOGPF("Unknown control type in %s", spec); return NULL; @@ -203,7 +206,7 @@ static void winmidi_tx(HMIDIOUT port, uint8_t type, uint8_t channel, uint8_t con output.components.data1 = value & 0x7F; output.components.data2 = (value >> 7) & 0x7F; } - else if(type == aftertouch){ + else if(type == aftertouch || type == program){ output.components.data1 = value; output.components.data2 = 0; } @@ -270,6 +273,8 @@ static char* winmidi_type_name(uint8_t typecode){ return "aftertouch"; case pitchbend: return "pitch"; + case program: + return "program"; } return "unknown"; } @@ -294,7 +299,8 @@ static int winmidi_handle(size_t num, managed_fd* fds){ if(backend_config.detect){ //pretty-print channel-wide events if(backend_config.event[u].channel.fields.type == pitchbend - || backend_config.event[u].channel.fields.type == aftertouch){ + || backend_config.event[u].channel.fields.type == aftertouch + || backend_config.event[u].channel.fields.type == program){ LOGPF("Incoming data on channel %s.ch%d.%s, value %f", backend_config.event[u].inst->name, backend_config.event[u].channel.fields.channel, @@ -450,7 +456,7 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW val.normalised = (double) ((input.components.data2 << 7) | input.components.data1) / 16383.0; val.raw.u64 = input.components.data2 << 7 | input.components.data1; } - else if(ident.fields.type == aftertouch){ + else if(ident.fields.type == aftertouch || ident.fields.type == program){ ident.fields.control = 0; val.normalised = (double) input.components.data1 / 127.0; val.raw.u64 = input.components.data1; diff --git a/backends/winmidi.h b/backends/winmidi.h index 4d3e2dd..40b3554 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -31,12 +31,13 @@ typedef struct /*_winmidi_instance_data*/ { enum /*_winmidi_channel_type*/ { none = 0, note = 0x90, - cc = 0xB0, pressure = 0xA0, + cc = 0xB0, + program = 0xC0, aftertouch = 0xD0, pitchbend = 0xE0, - rpn = 0xF0, - nrpn = 0xF1 + rpn = 0xF1, + nrpn = 0xF2 }; typedef union { diff --git a/backends/winmidi.md b/backends/winmidi.md index 4de792a..9e7d9cc 100644 --- a/backends/winmidi.md +++ b/backends/winmidi.md @@ -33,13 +33,14 @@ The `winmidi` backend supports mapping different MIDI events as MIDIMonster chan * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages +* `program` - Channel program change messages * `rpn` - Registered parameter numbers (14-bit extension) * `nrpn` - Non-registered parameter numbers (14-bit extension) A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). -The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. +The `pitch`, `aftertouch` and `program` messages/events are channel-wide, thus they can be specified as `channel.`. MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called @@ -55,6 +56,7 @@ midi1.channel15.pressure1 > midi1.channel0.note0 midi1.ch1.aftertouch > midi2.ch2.cc0 midi1.ch0.pitch > midi2.ch1.pitch midi2.ch0.nrpn900 > midi1.ch1.rpn1 +midi2.ch15.note1 > midi1.ch2.program ``` #### Known bugs / problems -- cgit v1.2.3 From 8ff86335bc9f5233564a0f791174a9cc49ae2df4 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 10 Jan 2021 19:10:01 +0100 Subject: Implement program change control for the rtpmidi backend (#79) --- backends/rtpmidi.c | 23 +++++++++++++++-------- backends/rtpmidi.h | 3 ++- backends/rtpmidi.md | 4 +++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 7c5aa69..8d5525c 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -427,6 +427,8 @@ static char* rtpmidi_type_name(uint8_t type){ return "aftertouch"; case pitchbend: return "pitch"; + case program: + return "program"; } return "unknown"; } @@ -552,7 +554,7 @@ static int rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t contro memcpy(&dest_addr, &(data->peer[peer].dest), min(sizeof(dest_addr), data->peer[peer].dest_len)); if(control){ - //calculate remote control port from data port + //calculate remote control port from data port ((struct sockaddr_in*) &dest_addr)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &dest_addr)->sin_port) - 1); } @@ -715,6 +717,9 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ else if(!strncmp(next_token, "aftertouch", 10)){ ident.fields.type = aftertouch; } + else if(!strncmp(next_token, "program", 7)){ + ident.fields.type = program; + } else{ LOGPF("Unknown control type in spec %s", spec); return NULL; @@ -761,11 +766,11 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v payload[3] = v[u].normalised * 127.0; if(ident.fields.type == pitchbend){ - payload[2] = ((int)(v[u].normalised * 16384.0)) & 0x7F; - payload[3] = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; + payload[2] = ((int)(v[u].normalised * 16383.0)) & 0x7F; + payload[3] = (((int)(v[u].normalised * 16383.0)) >> 7) & 0x7F; } - //channel-wide aftertouch is only 2 bytes - else if(ident.fields.type == aftertouch){ + //channel-wides aftertouch and program are only 2 bytes + else if(ident.fields.type == aftertouch || ident.fields.type == program){ payload[2] = payload[3]; payload -= 1; offset -= 1; @@ -996,7 +1001,7 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ ident.fields.channel = midi_status & 0x0F; //single byte command - if(ident.fields.type == aftertouch){ + if(ident.fields.type == aftertouch || ident.fields.type == program){ ident.fields.control = 0; val.normalised = (double) frame[offset] / 127.0; offset++; @@ -1010,7 +1015,7 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ if(ident.fields.type == pitchbend){ ident.fields.control = 0; - val.normalised = (double)((frame[offset] << 7) | frame[offset - 1]) / 16384.0; + val.normalised = (double)((frame[offset] << 7) | frame[offset - 1]) / 16383.0; } else{ ident.fields.control = frame[offset - 1]; @@ -1030,7 +1035,9 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised); if(cfg.detect){ - if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){ + if(ident.fields.type == pitchbend + || ident.fields.type == aftertouch + || ident.fields.type == program){ LOGPF("Incoming data on channel %s.ch%d.%s, value %f", inst->name, ident.fields.channel, rtpmidi_type_name(ident.fields.type), val.normalised); diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 7e6eccc..5f1621e 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -35,8 +35,9 @@ static int rtpmidi_shutdown(size_t n, instance** inst); enum /*_rtpmidi_channel_type*/ { none = 0, note = 0x90, - cc = 0xB0, pressure = 0xA0, + cc = 0xB0, + program = 0xC0, aftertouch = 0xD0, pitchbend = 0xE0 }; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 82548bf..9f56f3d 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -63,11 +63,12 @@ The `rtpmidi` backend supports mapping different MIDI events to MIDIMonster chan * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages +* `program` - Channel program change messages A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). -The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. +The `pitch`, `aftertouch` program messages/events are channel-wide, thus they can be specified as `channel.`. MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called @@ -80,6 +81,7 @@ rmidi1.ch0.note9 > rmidi2.channel1.cc4 rmidi1.channel15.pressure1 > rmidi1.channel0.note0 rmidi1.ch1.aftertouch > rmidi2.ch2.cc0 rmidi1.ch0.pitch > rmidi2.ch1.pitch +rmidi2.ch15.note1 > rmidi2.ch2.program ``` #### Known bugs / problems -- cgit v1.2.3 From de1cbe2de1c558c21f1566cfa78b20daef828ed5 Mon Sep 17 00:00:00 2001 From: Paul Hedderly Date: Wed, 13 Jan 2021 08:17:36 +0000 Subject: prh: lua example to turn any value input to 1.0 to make pads boolean --- configs/returnone.lua | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 configs/returnone.lua diff --git a/configs/returnone.lua b/configs/returnone.lua new file mode 100644 index 0000000..cde0b03 --- /dev/null +++ b/configs/returnone.lua @@ -0,0 +1,24 @@ +-- ReturnOne by Paul Hedderly +-- Sometimes you just want an on/off from linear pads +-- For example I want to activate scenes in OBS from a Korg NanoPad2 +-- But I dont want to have to thump the pads to get a 1.0 output +-- +-- You could use this as: +-- [midi nanoP] +-- read = nanoPAD2 +-- write = nanoPAD2 +-- [lua trackpad] +-- script = trackpad.lua +-- default-handler = returnone +-- .. +-- nanoP.ch0.note{36..51} > returnone.one{1..16} -- To feed all the 16 pads to +-- returnone.outone1 > obs./obs/scene/1/preview +-- returnone.outone2 > obs./obs/scene/2/preview +-- etc +-- The output channel will be the same as the channel you feed prepended "out" + + +function returnone(v) -- Use a default function - then you can use any input channel name + if v>0 then output("out"..input_channel(),1) end; + if v==0 then output("out"..input_channel(),0) end; +end -- cgit v1.2.3 From d63b0d35fb22baa15c833dedbd54c971c62fee5b Mon Sep 17 00:00:00 2001 From: Paul Hedderly Date: Wed, 13 Jan 2021 08:18:01 +0000 Subject: prh: lua example to turn a trackpad into a numberpad and trigger swipes and gestures --- configs/trackpad.lua | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 configs/trackpad.lua diff --git a/configs/trackpad.lua b/configs/trackpad.lua new file mode 100644 index 0000000..0aa9de7 --- /dev/null +++ b/configs/trackpad.lua @@ -0,0 +1,59 @@ +-- Trackpad input by Paul Hedderly +-- Expects three sources X, Y and touch +-- On the Korg Nanopad2 these would be nanoP.ch0.cc1, nanoP.ch0.cc2, nanoP.ch0.cc16 +-- so you could map and feed this script with something like: +-- [midi nanoP] +-- read = nanoPAD2 +-- write = nanoPAD2 +-- [lua trackpad] +-- script = trackpad.lua +-- .. +-- nanoP.ch0.cc1 > trackpad.x +-- nanoP.ch0.cc2 > trackpad.y +-- nanoP.ch0.cc16 > trackpad.touch +-- +-- Each touch will generate four outputs +-- - on[1-9] - the first point of touch (might not be very useful!) +-- - off[1-9] - the final point of touch +-- - swipe[1-9][1-9] - the first and last as a *simple* gesture or swipe +-- - gesture[1-9]..[1-9] - every segment you touch in order so you can do complicated gestures +-- +-- Each output of 1 is followed by an output of 0 +-- You would map these as +-- trackpad.on3 > ... +-- trackpad.off9 > .... +-- trackpad.swipe17 > .... -- would catch a line from top left to bottom left but could go anywhere in between +-- trackpad.gesture78965 > .... would catch a backwards capital L starting at the bottom left + +-- -- Reserve state variables +contact=0; +trace=""; +x=0; y=0 +lpos="" + +function x(v) -- NOTE the code assumes that we get an X before the Y - Some devices might differ! + x=math.floor((v+0.09)*2.55) +end + +function y(v) + y=2-math.floor((v+0.09)*2.55) -- 2- so that we have 1 at the top + pos=""..x+1+y*3 -- we need a string to compare + lpos=string.sub(trace,-1) + print("pos"..pos.." lpos"..lpos.." = "..trace) + if pos ~= lpos then trace=trace..pos end +end + +function touch(v) + -- print("TOUCH .."..contact..".... trace"..trace) + if v==1 then contact=1 + elseif v==0 then + first=string.sub(trace,1,1); last=string.sub(trace,-1) + ends=first..last + output("on"..last,1); output ("on"..last,0) + output("off"..last,1); output ("off"..last,0) + output("swipe"..ends,1); output ("swipe"..ends,0) + output("gesture"..trace,1); output ("gesture"..trace,0) + print("TRACKPAD>>>"..trace.." ends.."..ends) + trace="" -- reset tracking + end; +end -- cgit v1.2.3 From 35f4798673194733358cd3db19a4d2baf70887fd Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 15 Jan 2021 21:35:21 +0100 Subject: Implement EPN's for the rtpmidi backend --- backends/midi.c | 3 +- backends/rtpmidi.c | 177 ++++++++++++++++++++++++++++++++++++++++++++++------ backends/rtpmidi.h | 20 ++++-- backends/rtpmidi.md | 13 ++++ backends/winmidi.c | 2 +- 5 files changed, 191 insertions(+), 24 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index 10c8c4a..4bf846a 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -247,6 +247,7 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ } break; case pitchbend: + //TODO check whether this actually works that well midi_tx(data->port, ident.fields.type, ident.fields.channel, ident.fields.control, (v[u].normalised * 16383.0) - 8192); break; default: @@ -282,7 +283,7 @@ static char* midi_type_name(uint8_t type){ return "unknown"; } -//this state machine is used more-or-less verbatim in the winmidi and jack backends - fixes need to be applied there, too +//this state machine is used more-or-less verbatim in the winmidi, rtpmidi and jack backends - fixes need to be applied there, too static void midi_handle_epn(instance* inst, uint8_t chan, uint16_t control, uint16_t value){ midi_instance_data* data = (midi_instance_data*) inst->impl; midi_channel_ident ident = { diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 8d5525c..3a54e26 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -429,6 +429,10 @@ static char* rtpmidi_type_name(uint8_t type){ return "pitch"; case program: return "program"; + case rpn: + return "rpn"; + case nrpn: + return "nrpn"; } return "unknown"; } @@ -579,6 +583,13 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) LOGPF("Unknown instance mode %s for instance %s", value, inst->name); return 1; } + else if(!strcmp(option, "epn-tx")){ + data->epn_tx_short = 0; + if(!strcmp(value, "short")){ + data->epn_tx_short = 1; + } + return 0; + } else if(!strcmp(option, "ssrc")){ data->ssrc = strtoul(value, NULL, 0); if(!data->ssrc){ @@ -707,6 +718,14 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ ident.fields.type = note; next_token += 4; } + else if(!strncmp(next_token, "rpn", 3)){ + ident.fields.type = rpn; + next_token += 3; + } + else if(!strncmp(next_token, "nrpn", 4)){ + ident.fields.type = nrpn; + next_token += 4; + } else if(!strncmp(next_token, "pressure", 8)){ ident.fields.type = pressure; next_token += 8; @@ -733,6 +752,32 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ return NULL; } +static size_t rtpmidi_push_midi(uint8_t* payload, size_t bytes_left, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){ + //FIXME this is a bit simplistic but it works for now + if(bytes_left < 4){ + return 0; + } + + //encode timestamp + payload[0] = 0; + + //encode midi command + payload[1] = type | channel; + payload[2] = control; + payload[3] = value & 0x7F; + + if(type == pitchbend){ + payload[2] = value & 0x7F; + payload[3] = (value >> 7) & 0x7F; + } + //channel-wides aftertouch and program are only 2 bytes + else if(type == aftertouch || type == program){ + payload[2] = payload[3]; + return 3; + } + return 4; +} + static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; @@ -741,6 +786,7 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v size_t offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0; uint8_t* payload = frame + offset; rtpmidi_channel_ident ident; + size_t command_length = 0; rtp_header->vpxcc = RTPMIDI_HEADER_MAGIC; //some receivers seem to have problems reading rfcs and interpreting the marker bit correctly @@ -757,27 +803,37 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v for(u = 0; u < num; u++){ ident.label = c[u]->ident; - //encode timestamp - payload[0] = 0; - - //encode midi command - payload[1] = ident.fields.type | ident.fields.channel; - payload[2] = ident.fields.control; - payload[3] = v[u].normalised * 127.0; - - if(ident.fields.type == pitchbend){ - payload[2] = ((int)(v[u].normalised * 16383.0)) & 0x7F; - payload[3] = (((int)(v[u].normalised * 16383.0)) >> 7) & 0x7F; + switch(ident.fields.type){ + case rpn: + case nrpn: + //transmit parameter number + command_length = rtpmidi_push_midi(payload + offset, sizeof(frame) - offset, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control >> 7) & 0x7F); + command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, (ident.fields.type == rpn) ? 100 : 98, ident.fields.control & 0x7F); + + //transmit parameter value + command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) >> 7) & 0x7F); + command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 38, ((uint16_t) (v[u].normalised * 16383.0)) & 0x7F); + + if(!data->epn_tx_short){ + //clear active parameter + command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 101, 127); + command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 100, 127); + } + break; + case pitchbend: + //TODO check whether this works + command_length = rtpmidi_push_midi(payload + offset, sizeof(frame) - offset, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 16383.0); + break; + default: + command_length = rtpmidi_push_midi(payload + offset, sizeof(frame) - offset, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); } - //channel-wides aftertouch and program are only 2 bytes - else if(ident.fields.type == aftertouch || ident.fields.type == program){ - payload[2] = payload[3]; - payload -= 1; - offset -= 1; + + if(command_length == 0){ + LOGPF("Transmit buffer size exceeded on %s", inst->name); + break; } - payload += 4; - offset += 4; + offset += command_length; } //update command section length @@ -929,6 +985,79 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size return 0; } +//this state machine was copied more-or-less verbatim from the alsa midi implementation - fixes there will need to be integrated +static void rtpmidi_handle_epn(instance* inst, uint8_t chan, uint16_t control, uint16_t value){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + rtpmidi_channel_ident ident = { + .label = 0 + }; + channel* changed = NULL; + channel_value val; + + //switching between nrpn and rpn clears all valid bits + if(((data->epn_status[chan] & EPN_NRPN) && (control == 101 || control == 100)) + || (!(data->epn_status[chan] & EPN_NRPN) && (control == 99 || control == 98))){ + data->epn_status[chan] &= ~(EPN_NRPN | EPN_PARAMETER_LO | EPN_PARAMETER_HI); + } + + //setting an address always invalidates the value valid bits + if(control >= 98 && control <= 101){ + data->epn_status[chan] &= ~EPN_VALUE_HI; + } + + //parameter hi + if(control == 101 || control == 99){ + data->epn_control[chan] &= 0x7F; + data->epn_control[chan] |= value << 7; + data->epn_status[chan] |= EPN_PARAMETER_HI | ((control == 99) ? EPN_NRPN : 0); + if(control == 101 && value == 127){ + data->epn_status[chan] &= ~EPN_PARAMETER_HI; + } + } + + //parameter lo + if(control == 100 || control == 98){ + data->epn_control[chan] &= ~0x7F; + data->epn_control[chan] |= value & 0x7F; + data->epn_status[chan] |= EPN_PARAMETER_LO | ((control == 98) ? EPN_NRPN : 0); + if(control == 100 && value == 127){ + data->epn_status[chan] &= ~EPN_PARAMETER_LO; + } + } + + //value hi, clears low, mark as update candidate + if(control == 6 + //check if parameter is set before accepting value update + && ((data->epn_status[chan] & (EPN_PARAMETER_HI | EPN_PARAMETER_LO)) == (EPN_PARAMETER_HI | EPN_PARAMETER_LO))){ + data->epn_value[chan] = value << 7; + data->epn_status[chan] |= EPN_VALUE_HI; + } + + //value lo, flush the value + if(control == 38 + && data->epn_status[chan] & EPN_VALUE_HI){ + data->epn_value[chan] &= ~0x7F; + data->epn_value[chan] |= value & 0x7F; + data->epn_status[chan] &= ~EPN_VALUE_HI; + + if(cfg.detect){ + LOGPF("Incoming EPN data on channel %s.ch%d.%s%d", inst->name, chan, data->epn_status[chan] & EPN_NRPN ? "nrpn" : "rpn", data->epn_control[chan]); + } + + //find the updated channel + ident.fields.type = data->epn_status[chan] & EPN_NRPN ? nrpn : rpn; + ident.fields.channel = chan; + ident.fields.control = data->epn_control[chan]; + val.normalised = (double) data->epn_value[chan] / 16383.0; + + //push the new value + changed = mm_channel(inst, ident.label, 0); + if(changed){ + mm_channel_event(changed, val); + } + } +} + static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ uint16_t length = 0; size_t offset = 1, decode_time = 0, command_bytes = 0; @@ -1004,6 +1133,7 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ if(ident.fields.type == aftertouch || ident.fields.type == program){ ident.fields.control = 0; val.normalised = (double) frame[offset] / 127.0; + val.raw.u64 = frame[offset]; offset++; } //two-byte command @@ -1016,16 +1146,19 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ if(ident.fields.type == pitchbend){ ident.fields.control = 0; val.normalised = (double)((frame[offset] << 7) | frame[offset - 1]) / 16383.0; + val.raw.u64 = (frame[offset] << 7) | frame[offset - 1]; } else{ ident.fields.control = frame[offset - 1]; val.normalised = (double) frame[offset] / 127.0; + val.raw.u64 = frame[offset]; } //fix-up note off events if(ident.fields.type == 0x80){ ident.fields.type = note; val.normalised = 0; + val.raw.u64 = 0; } offset++; @@ -1034,6 +1167,14 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ DBGPF("Decoded command type %02X channel %d control %d value %f", ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised); + //forward EPN CCs to the EPN state machine + if(ident.fields.type == cc + && ((ident.fields.control <= 101 && ident.fields.control >= 98) + || ident.fields.control == 6 + || ident.fields.control == 38)){ + rtpmidi_handle_epn(inst, ident.fields.channel, ident.fields.control, val.raw.u64); + } + if(cfg.detect){ if(ident.fields.type == pitchbend || ident.fields.type == aftertouch diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 5f1621e..e88530f 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -32,6 +32,11 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define DNS_OPCODE(a) (((a) & 0x78) >> 3) #define DNS_RESPONSE(a) ((a) & 0x80) +#define EPN_NRPN 8 +#define EPN_PARAMETER_HI 4 +#define EPN_PARAMETER_LO 2 +#define EPN_VALUE_HI 1 + enum /*_rtpmidi_channel_type*/ { none = 0, note = 0x90, @@ -39,7 +44,9 @@ enum /*_rtpmidi_channel_type*/ { cc = 0xB0, program = 0xC0, aftertouch = 0xD0, - pitchbend = 0xE0 + pitchbend = 0xE0, + rpn = 0xF1, + nrpn = 0xF2 }; typedef enum /*_rtpmidi_instance_mode*/ { @@ -50,10 +57,10 @@ typedef enum /*_rtpmidi_instance_mode*/ { typedef union { struct { - uint8_t pad[5]; + uint8_t pad[4]; uint8_t type; uint8_t channel; - uint8_t control; + uint16_t control; } fields; uint64_t label; } rtpmidi_channel_ident; @@ -68,7 +75,7 @@ typedef struct /*_rtpmidi_peer*/ { ssize_t invite; //invite-list index for apple-mode learned peers (used to track ipv6/ipv4 overlapping invitations) } rtpmidi_peer; -typedef struct /*_rtmidi_instance_data*/ { +typedef struct /*_rtpmidi_instance_data*/ { rtpmidi_instance_mode mode; int fd; @@ -80,6 +87,11 @@ typedef struct /*_rtmidi_instance_data*/ { uint32_t ssrc; uint16_t sequence; + uint8_t epn_tx_short; + uint16_t epn_control[16]; + uint16_t epn_value[16]; + uint8_t epn_status[16]; + //apple-midi config char* accept; uint64_t last_announce; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 9f56f3d..8014572 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -38,6 +38,7 @@ Common instance configuration parameters | `ssrc` | `0xDEADBEEF` | Randomly generated | 32-bit synchronization source identifier | | `mode` | `direct` | none | Instance session management mode (`direct` or `apple`) | | `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times. Bypasses session discovery (but still performs session negotiation) | +| `epn-tx` | `short` | `full` | Configure whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter. | `direct` mode instance configuration parameters @@ -64,6 +65,8 @@ The `rtpmidi` backend supports mapping different MIDI events to MIDIMonster chan * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages * `program` - Channel program change messages +* `rpn` - Registered parameter numbers (14-bit extension) +* `nrpn` - Non-registered parameter numbers (14-bit extension) A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). @@ -74,6 +77,9 @@ MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (n additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called 'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. +Every MIDI channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using +the CC controls 101/100/99/98/38/6. Both control types have 14-bit IDs and 14-bit values. + Example mappings: ``` @@ -82,6 +88,7 @@ rmidi1.channel15.pressure1 > rmidi1.channel0.note0 rmidi1.ch1.aftertouch > rmidi2.ch2.cc0 rmidi1.ch0.pitch > rmidi2.ch1.pitch rmidi2.ch15.note1 > rmidi2.ch2.program +rmidi2.ch0.nrpn900 > rmidi1.ch1.rpn1 ``` #### Known bugs / problems @@ -93,6 +100,12 @@ The mDNS and DNS-SD implementations in this backend are extremely terse, to the specifications in multiple cases. Due to the complexity involved in supporting these protocols, problems arising from this will be considered a bug only in cases where they hinder normal operation of the backend. +Extended parameter numbers (EPNs, the `rpn` and `nrpn` control types) will also generate events on the controls (CC 101 through +98, 38 and 6) that are used as the lower layer transport. When using EPNs, mapping those controls is probably not useful. + +EPN control types support only the full 14-bit transfer encoding, not the shorter variant transmitting only the 7 +high-order bits. This may be changed if there is sufficient interest in the functionality. + mDNS discovery may announce flawed records when run on a host with multiple active interfaces. While this backend should be reasonably stable, there may be problematic edge cases simply due to the diff --git a/backends/winmidi.c b/backends/winmidi.c index a1fa686..649af2e 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -200,7 +200,7 @@ static void winmidi_tx(HMIDIOUT port, uint8_t type, uint8_t channel, uint8_t con output.components.status = type | channel; output.components.data1 = control; - output.components.data2 = value; + output.components.data2 = value & 0x7F; if(type == pitchbend){ output.components.data1 = value & 0x7F; -- cgit v1.2.3 From 71d86ec46259ce1b5488989ec30152c7cf810a8e Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 15 Jan 2021 23:49:12 +0100 Subject: Fix static analysis failures --- backends/rtpmidi.c | 7 ++++--- backends/visca.h | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 3a54e26..f0987f2 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -783,10 +783,9 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; rtpmidi_header* rtp_header = (rtpmidi_header*) frame; rtpmidi_command_header* command_header = (rtpmidi_command_header*) (frame + sizeof(rtpmidi_header)); - size_t offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0; + size_t command_length = 0, offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0; uint8_t* payload = frame + offset; rtpmidi_channel_ident ident; - size_t command_length = 0; rtp_header->vpxcc = RTPMIDI_HEADER_MAGIC; //some receivers seem to have problems reading rfcs and interpreting the marker bit correctly @@ -845,7 +844,9 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v for(u = 0; u < data->peers; u++){ if(data->peer[u].active && data->peer[u].connected){ - sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len); + if(sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len) <= 0){ + LOGPF("Failed to transmit to peer: %s", mmbackend_socket_strerror(errno)); + } } } diff --git a/backends/visca.h b/backends/visca.h index 47ada19..1004076 100644 --- a/backends/visca.h +++ b/backends/visca.h @@ -88,5 +88,6 @@ static struct { [store] = {"store", 7, {0x80, 0x01, 0x04, 0x3F, 0x01, 0, 0xFF}, 0, 254, 0, ptz_set_memory_store}, [home] = {"home", 5, {0x80, 0x01, 0x06, 0x04, 0xFF}, 0, 0, 0, NULL}, [relmove] = {"move", 9, {0x80, 0x01, 0x06, 0x01, 0, 0, 0, 0, 0xFF}, 0, 1, 0, ptz_set_relmove}, - [stop] = {"stop", 9, {0x80, 0x01, 0x06, 0x01, 0, 0, 0x03, 0x03, 0xFF}, 0, 0, 0, ptz_set_relmove} + [stop] = {"stop", 9, {0x80, 0x01, 0x06, 0x01, 0, 0, 0x03, 0x03, 0xFF}, 0, 0, 0, ptz_set_relmove}, + [sentinel] = {"SENTINEL"} }; -- cgit v1.2.3 From b199f019b47829f3745e8af8e62ed1ac4e65acf9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 16 Jan 2021 11:43:10 +0100 Subject: Exit when losing evdev connection --- backends/evdev.c | 9 ++++++++- backends/evdev.md | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backends/evdev.c b/backends/evdev.c index 4c734f9..3dbf837 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -367,7 +367,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 +385,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..bf192b0 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 (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 | -- cgit v1.2.3 From 91764dfc3ad86994ce27e5c80a92c034e12b849c Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 16 Jan 2021 19:34:21 +0100 Subject: Add notes --- TODO | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO b/TODO index 2ab5f10..9158e24 100644 --- a/TODO +++ b/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 -- cgit v1.2.3 From 983313fe782047a5fe84314fafa2eb0b56e0e3a1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 6 Feb 2021 22:51:54 +0100 Subject: Implement MQTT output --- backends/mqtt.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- backends/mqtt.h | 3 ++- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/backends/mqtt.c b/backends/mqtt.c index d3e516f..00045c0 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -9,6 +9,14 @@ static uint64_t last_maintenance = 0; +/* + * TODO + * * proper RETAIN handling + * * use topic aliases if possible + * * mqtt v3.1.1 local filtering + * * modifiable output mappings + */ + MM_PLUGIN_API int init(){ backend mqtt = { .name = BACKEND_NAME, @@ -267,13 +275,20 @@ static int mqtt_configure_instance(instance* inst, char* option, char* value){ 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; + } LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); return 1; } static int mqtt_push_subscriptions(instance* inst){ - mqtt_instance_data* data = calloc(1, sizeof(mqtt_instance_data)); + 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; @@ -282,6 +297,7 @@ static int mqtt_push_subscriptions(instance* inst){ 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; @@ -330,6 +346,7 @@ static channel* mqtt_channel(instance* inst, char* spec, uint8_t flags){ 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; } } @@ -343,13 +360,16 @@ static channel* mqtt_channel(instance* inst, char* spec, uint8_t flags){ } data->channel[u].topic = strdup(spec); - data->channel[u].topic_alias = 0; + data->channel[u].topic_alias_sent = 0; + data->channel[u].topic_alias_rcvd = 0; data->channel[u].flags = flags; 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++; } @@ -357,7 +377,52 @@ 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){ - //TODO mqtt v3.1.1 local filtering + mqtt_instance_data* data = (mqtt_instance_data*) inst->impl; + uint8_t variable_header[MQTT_BUFFER_LENGTH]; + uint8_t payload[MQTT_BUFFER_LENGTH]; + size_t vh_length = 0, payload_length = 0; + size_t u; + + for(u = 0; u < num; u++){ + vh_length = payload_length = 0; + + if(data->mqtt_version == 0x05 && data->channel[c[u]->ident].topic_alias_sent){ + //push zero-length topic + variable_header[vh_length++] = 0; + variable_header[vh_length++] = 0; + + //push property length + variable_header[vh_length++] = 5; + + //push payload type (0x01) + variable_header[vh_length++] = 0x01; + variable_header[vh_length++] = 1; + + //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; + } + 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 = snprintf((char*) payload, sizeof(payload), "%f", v[u].normalised); + //payload_length = snprintf((char*) (payload + 2), sizeof(payload) - 2, "%f", v[u].normalised); + //payload[0] = (payload_length >> 8) & 0xFF; + //payload[1] = payload_length & 0xFF; + //payload_length += 2; + mqtt_transmit(inst, MSG_PUBLISH, vh_length, variable_header, payload_length, payload); + } + return 0; } @@ -412,7 +477,9 @@ static int mqtt_handle_message(instance* inst, uint8_t type, uint8_t* variable_h } break; case MSG_PINGRESP: - //ignore ping responses + case MSG_SUBACK: + //ignore most responses + //FIXME error check SUBACK break; default: LOGPF("Unhandled MQTT message type 0x%02X on %s", type, inst->name); diff --git a/backends/mqtt.h b/backends/mqtt.h index c9bce81..6483364 100644 --- a/backends/mqtt.h +++ b/backends/mqtt.h @@ -40,7 +40,8 @@ enum { //qos, subscribe typedef struct /*_mqtt_channel*/ { char* topic; - uint16_t topic_alias; + uint16_t topic_alias_sent; + uint16_t topic_alias_rcvd; uint8_t flags; } mqtt_channel_data; -- cgit v1.2.3 From fdd8f8075fbcb22d349135bd87bf95b8ce88c8e1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 13 Feb 2021 16:58:12 +0100 Subject: Implement basic MQTT reception --- backends/mqtt.c | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- backends/mqtt.h | 9 +++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/backends/mqtt.c b/backends/mqtt.c index 00045c0..8bff531 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -8,6 +8,40 @@ #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 @@ -15,6 +49,8 @@ static uint64_t last_maintenance = 0; * * use topic aliases if possible * * mqtt v3.1.1 local filtering * * modifiable output mappings + * * TLS + * * JSON subchannels */ MM_PLUGIN_API int init(){ @@ -91,7 +127,7 @@ static int mqtt_generate_instanceid(instance* inst){ return mmbackend_strdup(&(data->client_id), clientid); } -static int mqtt_pop_varint(uint8_t* buffer, size_t len, uint32_t* result){ +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){ @@ -138,6 +174,21 @@ static size_t mqtt_push_utf8(uint8_t* buffer, size_t buffer_length, char* conten 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; data->last_control = 0; @@ -305,6 +356,11 @@ static int mqtt_push_subscriptions(instance* inst){ 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++; } @@ -324,6 +380,7 @@ static int mqtt_instance(instance* inst){ data->fd = -1; data->mqtt_version = MQTT_VERSION_DEFAULT; + data->packet_identifier = 1; inst->impl = data; if(mqtt_generate_instanceid(inst)){ @@ -456,6 +513,62 @@ static int mqtt_maintenance(){ 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; + channel_value val; + uint8_t qos = (type & 0x06) >> 1, content_utf8 = 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; + + //TODO parse properties + //find topic alias + //find type code + } + + //match via topic alias + if(topic_length == 0){ + //TODO match topic aliases + //TODO build topic alias database + } + //match via topic + else{ + for(u = 0; u < data->nchannels; u++){ + if(!strncmp(data->channel[u].topic, topic, topic_length)){ + break; + } + } + } + + 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); + //FIXME implement json subchannels + //FIXME implement input mappings + changed = mm_channel(inst, u, 0); + if(changed){ + val.normalised = strtod(payload, NULL); + mm_channel_event(changed, val); + } + } + return 0; +} + static int mqtt_handle_message(instance* inst, uint8_t type, uint8_t* variable_header, size_t length){ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl; @@ -482,6 +595,9 @@ static int mqtt_handle_message(instance* inst, uint8_t type, uint8_t* variable_h //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; diff --git a/backends/mqtt.h b/backends/mqtt.h index 6483364..0cb2617 100644 --- a/backends/mqtt.h +++ b/backends/mqtt.h @@ -18,6 +18,15 @@ static int mqtt_shutdown(size_t n, instance** inst); #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, -- cgit v1.2.3 From 670f3ff21f30b8aae765d61a38a7269d0eaab19d Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 14 Feb 2021 00:21:47 +0100 Subject: Implement MQTT5 topic aliases --- backends/mqtt.c | 197 +++++++++++++++++++++++++++++++++++++++++++------------- backends/mqtt.h | 2 + 2 files changed, 155 insertions(+), 44 deletions(-) diff --git a/backends/mqtt.c b/backends/mqtt.c index 8bff531..29f0436 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -46,7 +46,6 @@ static struct { /* * TODO * * proper RETAIN handling - * * use topic aliases if possible * * mqtt v3.1.1 local filtering * * modifiable output mappings * * TLS @@ -138,10 +137,52 @@ static size_t mqtt_pop_varint(uint8_t* buffer, size_t len, uint32_t* result){ offset++; } while(buffer[offset - 1] & 0x80); - *result = value; + 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; @@ -191,8 +232,18 @@ static size_t mqtt_pop_utf8(uint8_t* buffer, size_t buffer_length, char** data){ 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); @@ -241,11 +292,12 @@ static int mqtt_reconnect(instance* inst){ mqtt_disconnect(inst); } - LOGPF("Connecting instance %s to host %s port %s (TLS: %s, Authentication: %s)", + 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->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), @@ -260,9 +312,6 @@ static int mqtt_reconnect(instance* inst){ variable_header[6] = data->mqtt_version; variable_header[7] = 0x02 /*clean start*/ | (data->user ? 0x80 : 0x00) | (data->user ? 0x40 : 0x00); - //TODO set session expiry interval option - //TODO re-use previos session on reconnect - 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.. @@ -381,6 +430,7 @@ static int mqtt_instance(instance* inst){ 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)){ @@ -443,22 +493,40 @@ static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){ for(u = 0; u < num; u++){ vh_length = payload_length = 0; - if(data->mqtt_version == 0x05 && data->channel[c[u]->ident].topic_alias_sent){ - //push zero-length topic - variable_header[vh_length++] = 0; - variable_header[vh_length++] = 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); + } + } //push property length - variable_header[vh_length++] = 5; + 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; - //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; + 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 = snprintf((char*) (payload + 2), sizeof(payload) - 2, "%f", v[u].normalised); + payload[0] = (payload_length >> 8) & 0xFF; + payload[1] = payload_length & 0xFF; + payload_length += 2; } else{ //push topic @@ -471,12 +539,9 @@ static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){ variable_header[vh_length++] = 0x01; variable_header[vh_length++] = 1; } + payload_length = snprintf((char*) payload, sizeof(payload), "%f", v[u].normalised); } - payload_length = snprintf((char*) payload, sizeof(payload), "%f", v[u].normalised); - //payload_length = snprintf((char*) (payload + 2), sizeof(payload) - 2, "%f", v[u].normalised); - //payload[0] = (payload_length >> 8) & 0xFF; - //payload[1] = payload_length & 0xFF; - //payload_length += 2; + mqtt_transmit(inst, MSG_PUBLISH, vh_length, variable_header, payload_length, payload); } @@ -519,6 +584,7 @@ static int mqtt_handle_publish(instance* inst, uint8_t type, uint8_t* variable_h channel* changed = NULL; channel_value val; 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); @@ -529,23 +595,42 @@ static int mqtt_handle_publish(instance* inst, uint8_t type, uint8_t* variable_h payload_offset += mqtt_pop_varint(variable_header + property_offset, length - property_offset, &property_length); payload_offset += property_length; - //TODO parse properties - //find topic alias - //find type code + 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 == 0){ - //TODO match topic aliases - //TODO build topic alias database + 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{ + 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){ @@ -569,26 +654,50 @@ static int mqtt_handle_publish(instance* inst, uint8_t type, uint8_t* variable_h return 0; } -static int mqtt_handle_message(instance* inst, uint8_t type, uint8_t* variable_header, size_t length){ +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; + } - switch(type){ - case MSG_CONNACK: - 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]); - } - else{ - LOGPF("Connection on %s established", inst->name); - return mqtt_push_subscriptions(inst); + //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); } - break; + } + + 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 diff --git a/backends/mqtt.h b/backends/mqtt.h index 0cb2617..d40e83d 100644 --- a/backends/mqtt.h +++ b/backends/mqtt.h @@ -73,4 +73,6 @@ typedef struct /*_mqtt_instance_data*/ { uint64_t last_control; uint16_t packet_identifier; + uint16_t server_max_alias; + uint16_t current_alias; } mqtt_instance_data; -- cgit v1.2.3 From f483c0e50676d6a3b8eb8258daca1115550dd808 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 14 Feb 2021 02:12:22 +0100 Subject: MQTT backend documentation --- backends/mqtt.c | 8 ++++++-- backends/mqtt.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 backends/mqtt.md diff --git a/backends/mqtt.c b/backends/mqtt.c index 29f0436..41fa772 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -580,7 +580,7 @@ static int mqtt_maintenance(){ 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; + char* topic = NULL, *payload = NULL, *conversion_end = NULL; channel* changed = NULL; channel_value val; uint8_t qos = (type & 0x06) >> 1, content_utf8 = 0; @@ -647,7 +647,11 @@ static int mqtt_handle_publish(instance* inst, uint8_t type, uint8_t* variable_h //FIXME implement input mappings changed = mm_channel(inst, u, 0); if(changed){ - val.normalised = strtod(payload, NULL); + val.normalised = clamp(strtod(payload, &conversion_end), 1.0, 0.0); + if(payload == conversion_end){ + LOGPF("Failed to parse incoming data for %s.%s", inst->name, data->channel[u].topic); + return 0; + } mm_channel_event(changed, val); } } diff --git a/backends/mqtt.md b/backends/mqtt.md new file mode 100644 index 0000000..895831c --- /dev/null +++ b/backends/mqtt.md @@ -0,0 +1,53 @@ +### The `mqtt` backend + +This backend provides input from and output to an 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) | +| `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 normalised event value. + +Values above the maximum or below the minimum will be clamped. The MIDIMonster will not output values out of those +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. -- cgit v1.2.3 From fca46bef7dd8448216d44f0777f0b5ef31ac5883 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 27 Feb 2021 16:54:06 +0100 Subject: Implement input/output mapping --- backends/mqtt.c | 296 ++++++++++++++++++++++++++++++++++++++++++++----------- backends/mqtt.h | 11 ++- backends/mqtt.md | 38 ++++++- 3 files changed, 282 insertions(+), 63 deletions(-) diff --git a/backends/mqtt.c b/backends/mqtt.c index 41fa772..36aed03 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -3,6 +3,7 @@ #include #include +#include #include "libmmbackend.h" #include "mqtt.h" @@ -46,8 +47,6 @@ static struct { /* * TODO * * proper RETAIN handling - * * mqtt v3.1.1 local filtering - * * modifiable output mappings * * TLS * * JSON subchannels */ @@ -218,7 +217,7 @@ static size_t mqtt_push_utf8(uint8_t* buffer, size_t buffer_length, char* conten 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; } @@ -349,6 +348,92 @@ static int mqtt_reconnect(instance* inst){ 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; @@ -383,8 +468,8 @@ static int mqtt_configure_instance(instance* inst, char* option, char* value){ return 0; } - LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); - return 1; + //try to register as channel preconfig + return mqtt_configure_channel(inst, option, value); } static int mqtt_push_subscriptions(instance* inst){ @@ -470,12 +555,14 @@ static channel* mqtt_channel(instance* inst, char* spec, uint8_t flags){ 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++; } @@ -483,15 +570,125 @@ static channel* mqtt_channel(instance* inst, char* spec, uint8_t flags){ 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]; - size_t vh_length = 0, payload_length = 0; - size_t u; + 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 = 0; + vh_length = payload_length = alias_assigned = 0; if(data->mqtt_version == 0x05){ if(data->channel[c[u]->ident].topic_alias_sent){ @@ -506,6 +703,8 @@ static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){ 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; } } @@ -523,10 +722,12 @@ static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){ variable_header[vh_length++] = data->channel[c[u]->ident].topic_alias_sent & 0xFF; } - payload_length = snprintf((char*) (payload + 2), sizeof(payload) - 2, "%f", v[u].normalised); - payload[0] = (payload_length >> 8) & 0xFF; - payload[1] = payload_length & 0xFF; - payload_length += 2; + 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 @@ -539,50 +740,27 @@ static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){ variable_header[vh_length++] = 0x01; variable_header[vh_length++] = 1; } - payload_length = snprintf((char*) payload, sizeof(payload), "%f", v[u].normalised); + payload_length = mqtt_serialize(inst, data->channel + c[u]->ident, (char*) payload, sizeof(payload), v[u].normalised); } - mqtt_transmit(inst, MSG_PUBLISH, vh_length, variable_header, payload_length, payload); - } - - return 0; -} - -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; - } + 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(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); + else if(alias_assigned){ + //undo alias assignment + data->channel[c[u]->ident].topic_alias_sent = 0; + data->current_alias--; } } - free(inst); 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, *conversion_end = NULL; + char* topic = NULL, *payload = NULL; channel* changed = NULL; - channel_value val; uint8_t qos = (type & 0x06) >> 1, content_utf8 = 0; uint16_t topic_alias = 0; uint32_t property_length = 0; @@ -643,16 +821,9 @@ static int mqtt_handle_publish(instance* inst, uint8_t type, uint8_t* variable_h 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); - //FIXME implement json subchannels - //FIXME implement input mappings changed = mm_channel(inst, u, 0); if(changed){ - val.normalised = clamp(strtod(payload, &conversion_end), 1.0, 0.0); - if(payload == conversion_end){ - LOGPF("Failed to parse incoming data for %s.%s", inst->name, data->channel[u].topic); - return 0; - } - mm_channel_event(changed, val); + mqtt_deserialize(inst, changed, data->channel + u, payload, payload_length); } } return 0; @@ -679,7 +850,7 @@ static int mqtt_handle_connack(instance* inst, uint8_t type, uint8_t* variable_h 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]; @@ -736,9 +907,8 @@ static int mqtt_handle_fd(instance* inst){ DBGPF("Instance %s, offset %" PRIsize_t ", read %" PRIsize_t " bytes", inst->name, data->receive_offset, bytes_read); data->receive_offset += bytes_read; - //TODO loop this while at least one unhandled message is in the buffer - //check for complete message - if(data->receive_offset >= 2){ + 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]); @@ -752,6 +922,9 @@ static int mqtt_handle_fd(instance* inst){ } data->receive_offset -= message_length + header_length + 1; } + else{ + break; + } } return 0; @@ -802,7 +975,7 @@ static int mqtt_start(size_t n, instance** inst){ } static int mqtt_shutdown(size_t n, instance** inst){ - size_t u, p; + size_t u, p, v; mqtt_instance_data* data = NULL; for(u = 0; u < n; u++){ @@ -810,6 +983,10 @@ static int mqtt_shutdown(size_t n, instance** inst){ 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); @@ -817,6 +994,7 @@ static int mqtt_shutdown(size_t n, instance** inst){ free(data->port); free(data->user); free(data->password); + free(data->client_id); free(inst[u]->impl); inst[u]->impl = NULL; diff --git a/backends/mqtt.h b/backends/mqtt.h index d40e83d..c684f99 100644 --- a/backends/mqtt.h +++ b/backends/mqtt.h @@ -46,12 +46,21 @@ enum { MSG_AUTH = 0xF0 }; -//qos, subscribe +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*/ { diff --git a/backends/mqtt.md b/backends/mqtt.md index 895831c..6623438 100644 --- a/backends/mqtt.md +++ b/backends/mqtt.md @@ -30,10 +30,42 @@ The MQTT protocol places very few restrictions on the exchanged data. Thus, it i 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 normalised event value. +point number between `0.0` and `1.0`. The MIDIMonster will read these and use the value as the normalized event value. -Values above the maximum or below the minimum will be clamped. The MIDIMonster will not output values out of those -bounds. +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 + +``` + = range + = discrete [!] [!] +``` + +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 `` and `` 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 -- cgit v1.2.3 From e2b1110bca5b2b208657a733a91fd713e5c38b71 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 6 Mar 2021 19:39:51 +0100 Subject: Fix prefix-matching for evdev input names (#85) --- backends/evdev.c | 27 ++++++++++++++++----------- backends/evdev.md | 3 +-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/backends/evdev.c b/backends/evdev.c index 3dbf837..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; } diff --git a/backends/evdev.md b/backends/evdev.md index bf192b0..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), can be used instead of the `device` option | +| `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 -- cgit v1.2.3 From 8c30b37cd76ddec2111eca25a41214dd2bcdceb6 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Sat, 24 Apr 2021 20:32:50 +0200 Subject: Installer> Refactoring and Shellcheck error fixes. (#88) * Installer> Refactor install_dependencies. * Installer> Fixing some shellcheck errors. * Installer> Refactor error_handler to output errors. * Installer> Fix all remaining shellcheck errors. * Installer> Fix tabbing. * Installler> FIx tabbing again.. --- .gitignore | 1 + installer.sh | 76 +++++++++++++++++++++++++++++++++++------------------------- 2 files changed, 46 insertions(+), 31 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/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}${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%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}${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}${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}${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 -- cgit v1.2.3 From db76143dfff9aa69318273010fe2922c1e60ea4c Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 16 May 2021 17:09:23 +0200 Subject: Fix VISCA serial output (Fixes #91) --- backends/visca.c | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- backends/visca.h | 1 + backends/visca.md | 2 +- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/backends/visca.c b/backends/visca.c index ba81f8d..611d142 100644 --- a/backends/visca.c +++ b/backends/visca.c @@ -2,7 +2,10 @@ #define DEBUG #include -#include +#include +#include +#include + #include "visca.h" #include "libmmbackend.h" @@ -81,11 +84,41 @@ static int ptz_configure_instance(instance* inst, char* option, char* value){ LOG("Direct device connections are not possible on Windows"); return 1; #else - data->fd = open(value, O_NONBLOCK); + + struct termios2 device_config; + + options = strchr(value, ' '); + if(options){ + //terminate port name + *options = 0; + options++; + } + + data->fd = open(value, O_RDWR | O_NONBLOCK); if(data->fd < 0){ LOGPF("Failed to connect instance %s to device %s", inst->name, value); return 1; } + data->direct_device = 1; + + //configure baudrate + if(options){ + //get current port config + if(ioctl(data->fd, TCGETS2, &device_config)){ + LOGPF("Failed to get port configuration data for %s: %s", value, strerror(errno)); + return 0; + } + + device_config.c_cflag &= ~CBAUD; + device_config.c_cflag |= BOTHER; + device_config.c_ispeed = strtoul(options, NULL, 10); + device_config.c_ospeed = strtoul(options, NULL, 10); + + //set updated config + if(ioctl(data->fd, TCSETS2, &device_config)){ + LOGPF("Failed to set port configuration data for %s: %s", value, strerror(errno)); + } + } return 0; #endif } @@ -315,6 +348,21 @@ static size_t ptz_set_memory_store(instance* inst, channel* c, channel_value* v, return ptz_channels[store].bytes; } +static int ptz_write_serial(int fd, uint8_t* data, size_t bytes){ + ssize_t total = 0, sent; + + while(total < bytes){ + sent = write(fd, data + total, bytes - total); + if(sent < 0){ + LOGPF("Failed to write to serial port: %s", strerror(errno)); + return 1; + } + total += sent; + } + + return 0; +} + static int ptz_set(instance* inst, size_t num, channel** c, channel_value* v){ ptz_instance_data* data = (ptz_instance_data*) inst->impl; size_t n = 0, bytes = 0; @@ -336,7 +384,10 @@ static int ptz_set(instance* inst, size_t num, channel** c, channel_value* v){ bytes = ptz_channels[command].set(inst, c[n], v + n, tx); } - if(bytes && mmbackend_send(data->fd, tx, bytes)){ + if(data->direct_device && bytes && ptz_write_serial(data->fd, tx, bytes)){ + LOGPF("Failed to write %s command on instance %s", ptz_channels[command].name, inst->name); + } + else if(!data->direct_device && bytes && mmbackend_send(data->fd, tx, bytes)){ LOGPF("Failed to push %s command on instance %s", ptz_channels[command].name, inst->name); } } diff --git a/backends/visca.h b/backends/visca.h index 1004076..1b8c0e5 100644 --- a/backends/visca.h +++ b/backends/visca.h @@ -30,6 +30,7 @@ typedef struct /*_ptz_instance_data*/ { uint8_t tiltspeed; uint8_t relative_movement; double deadzone; + uint8_t direct_device; } ptz_instance_data; enum /*ptz_channels*/ { diff --git a/backends/visca.md b/backends/visca.md index cf5906d..101aa20 100644 --- a/backends/visca.md +++ b/backends/visca.md @@ -18,7 +18,7 @@ The `visca` backend does not take any global configuration. |---------------|-----------------------|-----------------------|---------------------------------------------------------------| | `id` | `5` | `1` | VISCA Camera address (normally 1 for network communication | | `connect` | `10.10.10.1 5678` | none | Camera network address and port. Default connection is TCP, when optionally suffixed with the `udp` keyword, connection will be UDP | -| `device` | `/dev/ttyUSB0` | none | (Linux only) Device node for a serial port adapter connecting to the camera | +| `device` | `/dev/ttyUSB0 115200` | none | (Linux only) Device node for a serial port adapter connecting to the camera, optionally followed by the baudrate | | `deadzone` | `0.1` | `0.1` | Amount of event value variation to be ignored for relative movement commands | #### Channel specification -- cgit v1.2.3 From c70ab97fe76003019ebf93d5c37a01366769138f Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 21 Jun 2021 21:06:31 +0200 Subject: Publish MQTT backend --- README.md | 2 ++ backends/mqtt.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 26a8c90..2379691 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,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 +161,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/backends/mqtt.c b/backends/mqtt.c index 36aed03..f2a7c83 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "mqtt" -#define DEBUG +//#define DEBUG #include #include -- cgit v1.2.3 From 921ce069a4770fbadad7bb4e806361e857469409 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 21 Jun 2021 22:03:17 +0200 Subject: Repository cleanup --- Makefile | 4 +- README.md | 1 - backend.c | 361 ---------------------------- backend.h | 17 -- backends/mqtt.md | 4 +- backends/osc.c | 3 +- backends/osc.md | 2 +- config.c | 708 ------------------------------------------------------- config.h | 54 ----- core/backend.c | 361 ++++++++++++++++++++++++++++ core/backend.h | 17 ++ core/config.c | 708 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ core/config.h | 54 +++++ core/plugin.c | 187 +++++++++++++++ core/plugin.h | 3 + plugin.c | 187 --------------- plugin.h | 3 - 17 files changed, 1337 insertions(+), 1337 deletions(-) delete mode 100644 backend.c delete mode 100644 backend.h delete mode 100644 config.c delete mode 100644 config.h create mode 100644 core/backend.c create mode 100644 core/backend.h create mode 100644 core/config.c create mode 100644 core/config.h create mode 100644 core/plugin.c create mode 100644 core/plugin.h delete mode 100644 plugin.c delete mode 100644 plugin.h diff --git a/Makefile b/Makefile index bda7bb1..9b95f1b 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 @@ -11,7 +11,7 @@ GITVERSION = $(shell git describe) CFLAGS ?= -g -Wall -Wpedantic #CFLAGS += -DDEBUG # Hide all non-API symbols for export -CFLAGS += -fvisibility=hidden +CFLAGS += -fvisibility=hidden -I./ midimonster: LDLIBS = -ldl # Replace version string with current git-describe if possible diff --git a/README.md b/README.md index 2379691..f958b25 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # The MIDIMonster MIDIMonster Logo -[![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) diff --git a/backend.c b/backend.c deleted file mode 100644 index 16e095c..0000000 --- a/backend.c +++ /dev/null @@ -1,361 +0,0 @@ -#include -#ifndef _WIN32 -#define MM_API __attribute__((visibility ("default"))) -#else -#define MM_API __attribute__((dllexport)) -#endif -#define BACKEND_NAME "core/be" -#include "midimonster.h" -#include "backend.h" - -static struct { - size_t n; - backend* backends; - instance*** instances; -} registry = { - .n = 0 -}; - -//the global channel store was converted from a naive list to a hashmap of lists for performance reasons -static struct { - //channelstore hash is set up for 256 buckets - size_t n[256]; - channel** entry[256]; -} channels = { - .n = { - 0 - } -}; - -static size_t channelstore_hash(instance* inst, uint64_t ident){ - uint64_t repr = ((uint64_t) inst) ^ ident; - return (repr ^ (repr >> 8) ^ (repr >> 16) ^ (repr >> 24) ^ (repr >> 32)) & 0xFF; -} - -int backends_handle(size_t nfds, managed_fd* fds){ - size_t u, p, n; - int rv = 0; - managed_fd xchg; - - for(u = 0; u < registry.n && !rv; u++){ - n = 0; - - for(p = 0; p < nfds; p++){ - if(fds[p].backend == registry.backends + u){ - xchg = fds[n]; - fds[n] = fds[p]; - fds[p] = xchg; - n++; - } - } - - //handle if there is data ready or the backend has active instances for polling - if(n || registry.instances[u]){ - DBGPF("Notifying backend %s of %" PRIsize_t " waiting FDs\n", registry.backends[u].name, n); - rv |= registry.backends[u].process(n, fds); - if(rv){ - fprintf(stderr, "Backend %s failed to handle input\n", registry.backends[u].name); - } - } - } - return rv; -} - -int backends_notify(size_t nev, channel** c, channel_value* v){ - size_t u, p, n; - int rv = 0; - channel_value xval; - channel* xchnl = NULL; - - for(u = 0; u < nev && !rv; u = n){ - //sort for this instance - n = u + 1; - for(p = u + 1; p < nev; p++){ - if(c[p]->instance == c[u]->instance){ - xval = v[p]; - xchnl = c[p]; - - v[p] = v[n]; - c[p] = c[n]; - - v[n] = xval; - c[n] = xchnl; - n++; - } - } - - //TODO eliminate duplicates - DBGPF("Calling handler for instance %s with %" PRIsize_t " events\n", c[u]->instance->name, n - u); - rv |= c[u]->instance->backend->handle(c[u]->instance, n - u, c + u, v + u); - } - - return 0; -} - -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); - 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); - 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); - return channels.entry[bucket][u]; - } - } - - if(!create){ - DBGPF("Requested unknown channel %" PRIu64 " (bucket %" PRIsize_t ") on instance %s\n", ident, bucket, inst->name); - return NULL; - } - - DBGPF("Creating previously unknown channel %" PRIu64 " on instance %s, bucket %" PRIsize_t "\n", ident, inst->name, bucket); - channels.entry[bucket] = realloc(channels.entry[bucket], (channels.n[bucket] + 1) * sizeof(channel*)); - if(!channels.entry[bucket]){ - fprintf(stderr, "Failed to allocate memory\n"); - channels.n[bucket] = 0; - return NULL; - } - - channels.entry[bucket][channels.n[bucket]] = calloc(1, sizeof(channel)); - if(!channels.entry[bucket][channels.n[bucket]]){ - fprintf(stderr, "Failed to allocate memory\n"); - return NULL; - } - - channels.entry[bucket][channels.n[bucket]]->instance = inst; - channels.entry[bucket][channels.n[bucket]]->ident = ident; - return channels.entry[bucket][(channels.n[bucket]++)]; -} - -instance* mm_instance(backend* b){ - size_t u = 0, n = 0; - - for(u = 0; u < registry.n; u++){ - if(registry.backends + u == b){ - //count existing instances - for(n = 0; registry.instances[u] && registry.instances[u][n]; n++){ - } - - //extend - registry.instances[u] = realloc(registry.instances[u], (n + 2) * sizeof(instance*)); - if(!registry.instances[u]){ - fprintf(stderr, "Failed to allocate memory\n"); - return NULL; - } - //sentinel - registry.instances[u][n + 1] = NULL; - registry.instances[u][n] = calloc(1, sizeof(instance)); - if(!registry.instances[u][n]){ - fprintf(stderr, "Failed to allocate memory\n"); - } - registry.instances[u][n]->backend = b; - return registry.instances[u][n]; - } - } - - //this should never happen - return NULL; -} - -MM_API instance* mm_instance_find(char* name, uint64_t ident){ - size_t b = 0; - instance** iter = NULL; - for(b = 0; b < registry.n; b++){ - if(!strcmp(registry.backends[b].name, name)){ - for(iter = registry.instances[b]; iter && *iter; iter++){ - if((*iter)->ident == ident){ - return *iter; - } - } - } - } - - return NULL; -} - -MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ - size_t b = 0, i = 0; - if(!ninst || !inst){ - return 1; - } - - for(b = 0; b < registry.n; b++){ - if(!strcmp(registry.backends[b].name, name)){ - //count instances - for(i = 0; registry.instances[b] && registry.instances[b][i]; i++){ - } - - *ninst = i; - if(!i){ - *inst = NULL; - return 0; - } - - *inst = calloc(i, sizeof(instance*)); - if(!*inst){ - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } - - memcpy(*inst, registry.instances[b], i * sizeof(instance*)); - return 0; - } - } - return 1; -} - -backend* backend_match(char* name){ - size_t u; - for(u = 0; u < registry.n; u++){ - if(!strcmp(registry.backends[u].name, name)){ - return registry.backends + u; - } - } - return NULL; -} - -instance* instance_match(char* name){ - size_t u; - instance** iter = NULL; - for(u = 0; u < registry.n; u++){ - for(iter = registry.instances[u]; iter && *iter; iter++){ - if(!strcmp(name, (*iter)->name)){ - return *iter; - } - } - } - return NULL; -} - -struct timeval backend_timeout(){ - size_t u; - uint32_t res, secs = 1, msecs = 0; - - for(u = 0; u < registry.n; u++){ - //only call interval if backend has instances - if(registry.instances[u] && registry.backends[u].interval){ - res = registry.backends[u].interval(); - if(res && (res / 1000) < secs){ - DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); - secs = res / 1000; - msecs = res % 1000; - } - else if(res && res / 1000 == secs && (res % 1000) < msecs){ - DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); - msecs = res % 1000; - } - } - } - - struct timeval tv = { - secs, - msecs * 1000 - }; - return tv; -} - -MM_API int mm_backend_register(backend b){ - if(!backend_match(b.name)){ - registry.backends = realloc(registry.backends, (registry.n + 1) * sizeof(backend)); - registry.instances = realloc(registry.instances, (registry.n + 1) * sizeof(instance**)); - if(!registry.backends || !registry.instances){ - fprintf(stderr, "Failed to allocate memory\n"); - registry.n = 0; - return 1; - } - registry.backends[registry.n] = b; - registry.instances[registry.n] = NULL; - registry.n++; - - fprintf(stderr, "Registered backend %s\n", b.name); - return 0; - } - return 1; -} - -int backends_start(){ - int rv = 0, current; - instance** inst = NULL; - size_t n, u; - - for(u = 0; u < registry.n; u++){ - //skip backends without instances - if(!registry.instances[u]){ - continue; - } - - //fetch list of instances - if(mm_backend_instances(registry.backends[u].name, &n, &inst)){ - fprintf(stderr, "Failed to fetch instance list for initialization of backend %s\n", registry.backends[u].name); - return 1; - } - - //start the backend - current = registry.backends[u].start(n, inst); - if(current){ - fprintf(stderr, "Failed to start backend %s\n", registry.backends[u].name); - } - - //clean up - free(inst); - inst = NULL; - rv |= current; - } - return rv; -} - -static void channels_free(){ - size_t u, p; - for(u = 0; u < sizeof(channels.n) / sizeof(channels.n[0]); u++){ - DBGPF("Cleaning up channel registry bucket %" PRIsize_t " with %" PRIsize_t " channels", u, channels.n[u]); - for(p = 0; p < channels.n[u]; p++){ - DBGPF("Destroying channel %" PRIu64 " on instance %s\n", channels.entry[u][p]->ident, channels.entry[u][p]->instance->name); - //call the channel_free function if the backend supports it - if(channels.entry[u][p]->impl && channels.entry[u][p]->instance->backend->channel_free){ - channels.entry[u][p]->instance->backend->channel_free(channels.entry[u][p]); - } - free(channels.entry[u][p]); - } - free(channels.entry[u]); - channels.entry[u] = NULL; - channels.n[u] = 0; - } -} - -int backends_stop(){ - size_t u, n; - instance** inst = NULL; - - //channels before instances to support proper shutdown procedures - channels_free(); - - //shut down the registry - for(u = 0; u < registry.n; u++){ - //fetch list of instances - if(mm_backend_instances(registry.backends[u].name, &n, &inst)){ - fprintf(stderr, "Failed to fetch instance list for shutdown of backend %s\n", registry.backends[u].name); - inst = NULL; - n = 0; - } - - registry.backends[u].shutdown(n, inst); - free(inst); - inst = NULL; - - //free instances - for(inst = registry.instances[u]; inst && *inst; inst++){ - free((*inst)->name); - (*inst)->name = NULL; - (*inst)->backend = NULL; - free(*inst); - } - free(registry.instances[u]); - registry.instances[u] = NULL; - } - - free(registry.backends); - free(registry.instances); - registry.n = 0; - return 0; -} diff --git a/backend.h b/backend.h deleted file mode 100644 index 6a69508..0000000 --- a/backend.h +++ /dev/null @@ -1,17 +0,0 @@ -#include - -/* Internal API */ -int backends_handle(size_t nfds, managed_fd* fds); -int backends_notify(size_t nev, channel** c, channel_value* v); -backend* backend_match(char* name); -instance* instance_match(char* name); -struct timeval backend_timeout(); -int backends_start(); -int backends_stop(); -instance* mm_instance(backend* b); - -/* Backend API */ -MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create); -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/backends/mqtt.md b/backends/mqtt.md index 6623438..85784ef 100644 --- a/backends/mqtt.md +++ b/backends/mqtt.md @@ -1,6 +1,6 @@ ### The `mqtt` backend -This backend provides input from and output to an message queueing telemetry transport (MQTT) +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. @@ -18,7 +18,7 @@ This backend does not take any global configuration. | `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) | +| `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]`. 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 #include @@ -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/config.c b/config.c deleted file mode 100644 index c1c3124..0000000 --- a/config.c +++ /dev/null @@ -1,708 +0,0 @@ -#include -#include -#include -#include -#ifndef _WIN32 -#include -#endif - -#define BACKEND_NAME "core/cfg" -#include "midimonster.h" -#include "config.h" -#include "backend.h" - -static enum { - none, - backend_cfg, - instance_cfg, - map -} parser_state = none; - -typedef enum { - map_ltr, - map_rtl, - map_bidir -} map_type; - -static backend* current_backend = NULL; -static instance* current_instance = NULL; -static size_t noverrides = 0; -static config_override* overrides = NULL; - -#ifdef _WIN32 -#define GETLINE_BUFFER 4096 - -static ssize_t getline(char** line, size_t* alloc, FILE* stream){ - size_t bytes_read = 0; - char c; - //sanity checks - if(!line || !alloc || !stream){ - return -1; - } - - //allocate buffer if none provided - if(!*line || !*alloc){ - *alloc = GETLINE_BUFFER; - *line = calloc(GETLINE_BUFFER, sizeof(char)); - if(!*line){ - fprintf(stderr, "Failed to allocate memory\n"); - return -1; - } - } - - if(feof(stream)){ - return -1; - } - - for(c = fgetc(stream); 1; c = fgetc(stream)){ - //end of buffer, resize - if(bytes_read == (*alloc) - 1){ - *alloc += GETLINE_BUFFER; - *line = realloc(*line, (*alloc) * sizeof(char)); - if(!*line){ - fprintf(stderr, "Failed to allocate memory\n"); - return -1; - } - } - - //store character - (*line)[bytes_read] = c; - - //end of line - if(feof(stream) || c == '\n'){ - //terminate string - (*line)[bytes_read + 1] = 0; - return bytes_read; - } - - //input broken - if(ferror(stream)){ - return -1; - } - - bytes_read++; - } -} -#endif - -static char* config_trim_line(char* in){ - ssize_t n; - //trim front - for(; *in && !isgraph(*in); in++){ - } - - //trim back - for(n = strlen(in); n >= 0 && !isgraph(in[n]); n--){ - in[n] = 0; - } - - return in; -} - -static int config_glob_parse_range(channel_glob* glob, char* spec, size_t length){ - //FIXME might want to allow negative delimiters at some point - char* parse_offset = NULL; - glob->type = glob_range; - - //first interval member - glob->limits.u64[0] = strtoul(spec, &parse_offset, 10); - if(!parse_offset || parse_offset - spec >= length || strncmp(parse_offset, "..", 2)){ - return 1; - } - - parse_offset += 2; - //second interval member - glob->limits.u64[1] = strtoul(parse_offset, &parse_offset, 10); - if(!parse_offset || parse_offset - spec != length || *parse_offset != '}'){ - return 1; - } - - //calculate number of channels within interval - if(glob->limits.u64[0] < glob->limits.u64[1]){ - glob->values = glob->limits.u64[1] - glob->limits.u64[0] + 1; - } - else if(glob->limits.u64[0] > glob->limits.u64[1]){ - glob->values = glob->limits.u64[0] - glob->limits.u64[1] + 1; - } - else{ - glob->values = 1; - } - - return 0; -} - -static int config_glob_parse_list(channel_glob* glob, char* spec, size_t length){ - size_t u = 0; - glob->type = glob_list; - glob->values = 1; - - //count number of values in list - for(u = 0; u < length; u++){ - if(spec[u] == ','){ - glob->values++; - } - } - return 0; -} - -static int config_glob_parse(channel_glob* glob, char* spec, size_t length){ - size_t u = 0; - - //detect glob type - for(u = 0; u < length; u++){ - if(length - u > 2 && !strncmp(spec + u, "..", 2)){ - DBGPF("Detected glob %.*s as range type", (int) length, spec); - return config_glob_parse_range(glob, spec, length); - } - else if(spec[u] == ','){ - DBGPF("Detected glob %.*s as list type", (int) length, spec); - return config_glob_parse_list(glob, spec, length); - } - } - - LOGPF("Failed to detect glob type for spec %.*s", (int) length, spec); - return 1; -} - -static int config_glob_scan(instance* inst, channel_spec* spec){ - char* glob_start = spec->spec, *glob_end = NULL; - size_t u; - - //assume a spec is one channel as default - spec->channels = 1; - - //scan and mark globs - for(glob_start = strchr(glob_start, '{'); glob_start; glob_start = strchr(glob_start, '{')){ - glob_end = strchr(glob_start, '}'); - if(!glob_end){ - fprintf(stderr, "Failed to parse channel spec, unterminated glob: %s\n", spec->spec); - return 1; - } - - spec->glob = realloc(spec->glob, (spec->globs + 1) * sizeof(channel_glob)); - if(!spec->glob){ - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } - - spec->glob[spec->globs].offset[0] = glob_start - spec->spec; - spec->glob[spec->globs].offset[1] = glob_end - spec->spec; - spec->globs++; - - //skip this opening brace - glob_start++; - } - - //try to parse globs internally - spec->internal = 1; - for(u = 0; u < spec->globs; u++){ - if(config_glob_parse(spec->glob + u, - spec->spec + spec->glob[u].offset[0] + 1, - spec->glob[u].offset[1] - spec->glob[u].offset[0] - 1)){ - spec->internal = 0; - break; - } - } - if(!spec->internal){ - //TODO try to parse globs externally - fprintf(stderr, "Failed to parse glob %" PRIsize_t " in %s internally\n", u + 1, spec->spec); - return 1; - } - - //calculate channel total - for(u = 0; u < spec->globs; u++){ - spec->channels *= spec->glob[u].values; - } - return 0; -} - -static ssize_t config_glob_resolve_range(char* spec, size_t length, channel_glob* glob, uint64_t n){ - uint64_t current_value = glob->limits.u64[0] + (n % glob->values); - //if counting down - if(glob->limits.u64[0] > glob->limits.u64[1]){ - current_value = glob->limits.u64[0] - (n % glob->values); - } - - //write out value - return snprintf(spec, length, "%" PRIu64, current_value); -} - -static ssize_t config_glob_resolve_list(char* spec, size_t length, channel_glob* glob, uint64_t n){ - uint64_t current_replacement = 0; - size_t replacement_length = 0; - char* source = spec + 1; - n %= glob->values; - - //find start of replacement value - DBGPF("Searching instance %" PRIu64 " of spec %.*s", n, (int) length, spec); - for(current_replacement = 0; current_replacement < n; current_replacement++){ - for(; source[0] != ','; source++){ - } - source++; - } - - //calculate replacement length - for(; source[replacement_length] != ',' && source[replacement_length] != '}'; replacement_length++){ - } - - //write out new value - memmove(spec, source, replacement_length); - return replacement_length; -} - -static channel* config_glob_resolve(instance* inst, channel_spec* spec, uint64_t n, uint8_t map_direction){ - size_t glob = 0, glob_length; - ssize_t bytes = 0; - channel* result = NULL; - char* resolved_spec = strdup(spec->spec); - - if(!resolved_spec){ - LOG("Failed to allocate memory"); - return NULL; - } - - //TODO if not internal, try to resolve externally - //iterate and resolve globs - for(glob = spec->globs; glob > 0; glob--){ - glob_length = spec->glob[glob - 1].offset[1] - spec->glob[glob - 1].offset[0]; - - switch(spec->glob[glob - 1].type){ - case glob_range: - bytes = config_glob_resolve_range(resolved_spec + spec->glob[glob - 1].offset[0], - glob_length, - spec->glob + (glob - 1), - n); - break; - case glob_list: - bytes = config_glob_resolve_list(resolved_spec + spec->glob[glob - 1].offset[0], - glob_length, - spec->glob + (glob - 1), - n); - break; - } - - n /= spec->glob[glob - 1].values; - - //move trailing data - if(bytes > 0 && bytes < glob_length){ - memmove(resolved_spec + spec->glob[glob - 1].offset[0] + bytes, - resolved_spec + spec->glob[glob - 1].offset[1] + 1, - strlen(spec->spec) - spec->glob[glob - 1].offset[1]); - } - else{ - LOGPF("Failure parsing glob spec %s", resolved_spec); - goto bail; - } - } - - DBGPF("Resolved spec %s to %s", spec->spec, resolved_spec); - result = inst->backend->channel(inst, resolved_spec, map_direction); - if(spec->globs && !result){ - LOGPF("Failed to match multichannel evaluation %s to a channel", resolved_spec); - } - -bail: - free(resolved_spec); - return result; -} - -static int config_map(char* to_raw, char* from_raw){ - //create a copy because the original pointer may be used multiple times - char* to = strdup(to_raw), *from = strdup(from_raw); - channel_spec spec_to = { - .spec = to - }, spec_from = { - .spec = from - }; - instance* instance_to = NULL, *instance_from = NULL; - channel* channel_from = NULL, *channel_to = NULL; - uint64_t n = 0; - int rv = 1; - - if(!from || !to){ - free(from); - free(to); - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } - - //separate channel spec from instance - for(; *(spec_to.spec) && *(spec_to.spec) != '.'; spec_to.spec++){ - } - - for(; *(spec_from.spec) && *(spec_from.spec) != '.'; spec_from.spec++){ - } - - if(!spec_from.spec[0] || !spec_to.spec[0]){ - fprintf(stderr, "Mapping does not contain a proper instance specification\n"); - goto done; - } - - //terminate - spec_from.spec[0] = spec_to.spec[0] = 0; - spec_from.spec++; - spec_to.spec++; - - //find matching instances - instance_to = instance_match(to); - instance_from = instance_match(from); - - if(!instance_to || !instance_from){ - fprintf(stderr, "No such instance %s\n", instance_from ? to : from); - goto done; - } - - //scan for globs - if(config_glob_scan(instance_to, &spec_to) - || config_glob_scan(instance_from, &spec_from)){ - goto done; - } - - if((spec_to.channels != spec_from.channels && spec_from.channels != 1 && spec_to.channels != 1) - || spec_to.channels == 0 - || spec_from.channels == 0){ - fprintf(stderr, "Multi-channel specification size mismatch: %s.%s (%" PRIsize_t " channels) - %s.%s (%" PRIsize_t " channels)\n", - instance_from->name, - spec_from.spec, - spec_from.channels, - instance_to->name, - spec_to.spec, - spec_to.channels); - goto done; - } - - //iterate, resolve globs and map - rv = 0; - for(n = 0; !rv && n < max(spec_from.channels, spec_to.channels); n++){ - channel_from = config_glob_resolve(instance_from, &spec_from, min(n, spec_from.channels), mmchannel_input); - channel_to = config_glob_resolve(instance_to, &spec_to, min(n, spec_to.channels), mmchannel_output); - - if(!channel_from || !channel_to){ - rv = 1; - goto done; - } - rv |= mm_map_channel(channel_from, channel_to); - } - -done: - free(spec_from.glob); - free(spec_to.glob); - free(from); - free(to); - return rv; -} - -static int config_line(char* line){ - map_type mapping_type = map_rtl; - char* separator = NULL; - size_t u; - - line = config_trim_line(line); - if(*line == ';' || strlen(line) == 0){ - //skip comments - return 0; - } - if(*line == '[' && line[strlen(line) - 1] == ']'){ - if(!strncmp(line, "[backend ", 9)){ - //backend configuration - parser_state = backend_cfg; - line[strlen(line) - 1] = 0; - current_backend = backend_match(line + 9); - - if(!current_backend){ - fprintf(stderr, "Cannot configure unknown backend %s\n", line + 9); - return 1; - } - - //apply overrides - for(u = 0; u < noverrides; u++){ - if(!overrides[u].handled && overrides[u].type == override_backend - && !strcmp(overrides[u].target, current_backend->name)){ - if(current_backend->conf(overrides[u].option, overrides[u].value)){ - fprintf(stderr, "Configuration override for %s failed for backend %s\n", - overrides[u].option, current_backend->name); - return 1; - } - overrides[u].handled = 1; - } - } - } - else if(!strncmp(line, "[include ", 9)){ - line[strlen(line) - 1] = 0; - return config_read(line + 9); - } - else if(!strcmp(line, "[map]")){ - //mapping configuration - parser_state = map; - } - else{ - //backend instance configuration - parser_state = instance_cfg; - - //trim braces - line[strlen(line) - 1] = 0; - line++; - - //find separating space and terminate - for(separator = line; *separator && *separator != ' '; separator++){ - } - if(!*separator){ - fprintf(stderr, "No instance name specified for backend %s\n", line); - return 1; - } - *separator = 0; - separator++; - - current_backend = backend_match(line); - if(!current_backend){ - fprintf(stderr, "No such backend %s\n", line); - return 1; - } - - if(instance_match(separator)){ - fprintf(stderr, "Duplicate instance name %s\n", separator); - return 1; - } - - //validate instance name - if(strchr(separator, ' ') || strchr(separator, '.')){ - fprintf(stderr, "Invalid instance name %s\n", separator); - return 1; - } - - current_instance = mm_instance(current_backend); - if(!current_instance){ - return 1; - } - - if(current_backend->create(current_instance)){ - fprintf(stderr, "Failed to create %s instance %s\n", line, separator); - return 1; - } - - current_instance->name = strdup(separator); - current_instance->backend = current_backend; - fprintf(stderr, "Created %s instance %s\n", line, separator); - - //apply overrides - for(u = 0; u < noverrides; u++){ - if(!overrides[u].handled && overrides[u].type == override_instance - && !strcmp(overrides[u].target, current_instance->name)){ - if(current_backend->conf_instance(current_instance, overrides[u].option, overrides[u].value)){ - fprintf(stderr, "Configuration override for %s failed for instance %s\n", - overrides[u].option, current_instance->name); - return 1; - } - overrides[u].handled = 1; - } - } - } - } - else if(parser_state == map){ - mapping_type = map_rtl; - //find separator - for(separator = line; *separator && *separator != '<' && *separator != '>'; separator++){ - } - - switch(*separator){ - case '>': - mapping_type = map_ltr; - //fall through - case '<': //default - *separator = 0; - separator++; - break; - case 0: - default: - fprintf(stderr, "Not a channel mapping: %s\n", line); - return 1; - } - - if((mapping_type == map_ltr && *separator == '<') - || (mapping_type == map_rtl && *separator == '>')){ - mapping_type = map_bidir; - separator++; - } - - line = config_trim_line(line); - separator = config_trim_line(separator); - - if(mapping_type == map_ltr || mapping_type == map_bidir){ - if(config_map(separator, line)){ - fprintf(stderr, "Failed to map channel %s to %s\n", line, separator); - return 1; - } - } - if(mapping_type == map_rtl || mapping_type == map_bidir){ - if(config_map(line, separator)){ - fprintf(stderr, "Failed to map channel %s to %s\n", separator, line); - return 1; - } - } - } - else{ - //pass to parser - //find separator - separator = strchr(line, '='); - if(!separator){ - fprintf(stderr, "Not an assignment (currently expecting %s configuration): %s\n", line, (parser_state == backend_cfg) ? "backend" : "instance"); - return 1; - } - - *separator = 0; - separator++; - line = config_trim_line(line); - separator = config_trim_line(separator); - - if(parser_state == backend_cfg && current_backend->conf(line, separator)){ - fprintf(stderr, "Failed to configure backend %s\n", current_backend->name); - return 1; - } - else if(parser_state == instance_cfg && current_backend->conf_instance(current_instance, line, separator)){ - fprintf(stderr, "Failed to configure instance %s\n", current_instance->name); - return 1; - } - } - - return 0; -} - -int config_read(char* cfg_filepath){ - int rv = 1; - size_t line_alloc = 0; - ssize_t status; - FILE* source = NULL; - char* line_raw = NULL; - - //create heap copy of file name because original might be in readonly memory - char* source_dir = strdup(cfg_filepath), *source_file = NULL, original_dir[PATH_MAX * 2] = ""; - #ifdef _WIN32 - char path_separator = '\\'; - #else - char path_separator = '/'; - #endif - - if(!source_dir){ - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } - - //change working directory to the one containing the configuration file so relative paths work as expected - source_file = strrchr(source_dir, path_separator); - if(source_file){ - *source_file = 0; - source_file++; - - if(!getcwd(original_dir, sizeof(original_dir))){ - fprintf(stderr, "Failed to read current working directory: %s\n", strerror(errno)); - goto bail; - } - - if(chdir(source_dir)){ - fprintf(stderr, "Failed to change to configuration file directory %s: %s\n", source_dir, strerror(errno)); - goto bail; - } - } - else{ - source_file = source_dir; - } - - fprintf(stderr, "Reading configuration file %s\n", cfg_filepath); - source = fopen(source_file, "r"); - - if(!source){ - fprintf(stderr, "Failed to open %s for reading\n", cfg_filepath); - goto bail; - } - - for(status = getline(&line_raw, &line_alloc, source); status >= 0; status = getline(&line_raw, &line_alloc, source)){ - if(config_line(line_raw)){ - goto bail; - } - } - - //TODO check whether all overrides have been applied - - rv = 0; -bail: - //change back to previous directory to allow recursive configuration file parsing - if(source_file && source_dir != source_file){ - chdir(original_dir); - } - - free(source_dir); - if(source){ - fclose(source); - } - free(line_raw); - return rv; -} - -int config_add_override(override_type type, char* data_raw){ - int rv = 1; - //heap a copy because the original data is probably not writable - char* data = strdup(data_raw); - - if(!data){ - fprintf(stderr, "Failed to allocate memory\n"); - goto bail; - } - - char* option = strchr(data, '.'); - char* value = strchr(data, '='); - - if(!option || !value){ - fprintf(stderr, "Override %s is not a valid assignment\n", data_raw); - goto bail; - } - - //terminate strings - *option = 0; - option++; - - *value = 0; - value++; - - config_override new = { - .type = type, - .handled = 0, - .target = strdup(config_trim_line(data)), - .option = strdup(config_trim_line(option)), - .value = strdup(config_trim_line(value)) - }; - - if(!new.target || !new.option || !new.value){ - fprintf(stderr, "Failed to allocate memory\n"); - goto bail; - } - - overrides = realloc(overrides, (noverrides + 1) * sizeof(config_override)); - if(!overrides){ - noverrides = 0; - fprintf(stderr, "Failed to allocate memory\n"); - goto bail; - } - overrides[noverrides] = new; - noverrides++; - - rv = 0; -bail: - free(data); - return rv; -} - -void config_free(){ - size_t u; - - for(u = 0; u < noverrides; u++){ - free(overrides[u].target); - free(overrides[u].option); - free(overrides[u].value); - } - - noverrides = 0; - free(overrides); - overrides = NULL; - - parser_state = none; -} diff --git a/config.h b/config.h deleted file mode 100644 index b96a866..0000000 --- a/config.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Channel glob type - */ -enum /*_mm_channel_glob_type */ { - glob_range, - glob_list -}; - -/* - * Channel specification glob - */ -typedef struct /*_mm_channel_glob*/ { - size_t offset[2]; - union { - void* impl; - uint64_t u64[2]; - } limits; - uint8_t type; - uint64_t values; -} channel_glob; - -/* - * (Multi-)Channel specification - */ -typedef struct /*_mm_channel_spec*/ { - char* spec; - uint8_t internal; - size_t channels; - size_t globs; - channel_glob* glob; -} channel_spec; - -/* - * Command-line override types - */ -typedef enum { - override_backend, - override_instance -} override_type; - -/* - * Command-line override data - */ -typedef struct /*_mm_config_override*/ { - override_type type; - uint8_t handled; - char* target; - char* option; - char* value; -} config_override; - -int config_read(char* file); -int config_add_override(override_type type, char* data); -void config_free(); diff --git a/core/backend.c b/core/backend.c new file mode 100644 index 0000000..16e095c --- /dev/null +++ b/core/backend.c @@ -0,0 +1,361 @@ +#include +#ifndef _WIN32 +#define MM_API __attribute__((visibility ("default"))) +#else +#define MM_API __attribute__((dllexport)) +#endif +#define BACKEND_NAME "core/be" +#include "midimonster.h" +#include "backend.h" + +static struct { + size_t n; + backend* backends; + instance*** instances; +} registry = { + .n = 0 +}; + +//the global channel store was converted from a naive list to a hashmap of lists for performance reasons +static struct { + //channelstore hash is set up for 256 buckets + size_t n[256]; + channel** entry[256]; +} channels = { + .n = { + 0 + } +}; + +static size_t channelstore_hash(instance* inst, uint64_t ident){ + uint64_t repr = ((uint64_t) inst) ^ ident; + return (repr ^ (repr >> 8) ^ (repr >> 16) ^ (repr >> 24) ^ (repr >> 32)) & 0xFF; +} + +int backends_handle(size_t nfds, managed_fd* fds){ + size_t u, p, n; + int rv = 0; + managed_fd xchg; + + for(u = 0; u < registry.n && !rv; u++){ + n = 0; + + for(p = 0; p < nfds; p++){ + if(fds[p].backend == registry.backends + u){ + xchg = fds[n]; + fds[n] = fds[p]; + fds[p] = xchg; + n++; + } + } + + //handle if there is data ready or the backend has active instances for polling + if(n || registry.instances[u]){ + DBGPF("Notifying backend %s of %" PRIsize_t " waiting FDs\n", registry.backends[u].name, n); + rv |= registry.backends[u].process(n, fds); + if(rv){ + fprintf(stderr, "Backend %s failed to handle input\n", registry.backends[u].name); + } + } + } + return rv; +} + +int backends_notify(size_t nev, channel** c, channel_value* v){ + size_t u, p, n; + int rv = 0; + channel_value xval; + channel* xchnl = NULL; + + for(u = 0; u < nev && !rv; u = n){ + //sort for this instance + n = u + 1; + for(p = u + 1; p < nev; p++){ + if(c[p]->instance == c[u]->instance){ + xval = v[p]; + xchnl = c[p]; + + v[p] = v[n]; + c[p] = c[n]; + + v[n] = xval; + c[n] = xchnl; + n++; + } + } + + //TODO eliminate duplicates + DBGPF("Calling handler for instance %s with %" PRIsize_t " events\n", c[u]->instance->name, n - u); + rv |= c[u]->instance->backend->handle(c[u]->instance, n - u, c + u, v + u); + } + + return 0; +} + +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); + 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); + 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); + return channels.entry[bucket][u]; + } + } + + if(!create){ + DBGPF("Requested unknown channel %" PRIu64 " (bucket %" PRIsize_t ") on instance %s\n", ident, bucket, inst->name); + return NULL; + } + + DBGPF("Creating previously unknown channel %" PRIu64 " on instance %s, bucket %" PRIsize_t "\n", ident, inst->name, bucket); + channels.entry[bucket] = realloc(channels.entry[bucket], (channels.n[bucket] + 1) * sizeof(channel*)); + if(!channels.entry[bucket]){ + fprintf(stderr, "Failed to allocate memory\n"); + channels.n[bucket] = 0; + return NULL; + } + + channels.entry[bucket][channels.n[bucket]] = calloc(1, sizeof(channel)); + if(!channels.entry[bucket][channels.n[bucket]]){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + channels.entry[bucket][channels.n[bucket]]->instance = inst; + channels.entry[bucket][channels.n[bucket]]->ident = ident; + return channels.entry[bucket][(channels.n[bucket]++)]; +} + +instance* mm_instance(backend* b){ + size_t u = 0, n = 0; + + for(u = 0; u < registry.n; u++){ + if(registry.backends + u == b){ + //count existing instances + for(n = 0; registry.instances[u] && registry.instances[u][n]; n++){ + } + + //extend + registry.instances[u] = realloc(registry.instances[u], (n + 2) * sizeof(instance*)); + if(!registry.instances[u]){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + //sentinel + registry.instances[u][n + 1] = NULL; + registry.instances[u][n] = calloc(1, sizeof(instance)); + if(!registry.instances[u][n]){ + fprintf(stderr, "Failed to allocate memory\n"); + } + registry.instances[u][n]->backend = b; + return registry.instances[u][n]; + } + } + + //this should never happen + return NULL; +} + +MM_API instance* mm_instance_find(char* name, uint64_t ident){ + size_t b = 0; + instance** iter = NULL; + for(b = 0; b < registry.n; b++){ + if(!strcmp(registry.backends[b].name, name)){ + for(iter = registry.instances[b]; iter && *iter; iter++){ + if((*iter)->ident == ident){ + return *iter; + } + } + } + } + + return NULL; +} + +MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ + size_t b = 0, i = 0; + if(!ninst || !inst){ + return 1; + } + + for(b = 0; b < registry.n; b++){ + if(!strcmp(registry.backends[b].name, name)){ + //count instances + for(i = 0; registry.instances[b] && registry.instances[b][i]; i++){ + } + + *ninst = i; + if(!i){ + *inst = NULL; + return 0; + } + + *inst = calloc(i, sizeof(instance*)); + if(!*inst){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + memcpy(*inst, registry.instances[b], i * sizeof(instance*)); + return 0; + } + } + return 1; +} + +backend* backend_match(char* name){ + size_t u; + for(u = 0; u < registry.n; u++){ + if(!strcmp(registry.backends[u].name, name)){ + return registry.backends + u; + } + } + return NULL; +} + +instance* instance_match(char* name){ + size_t u; + instance** iter = NULL; + for(u = 0; u < registry.n; u++){ + for(iter = registry.instances[u]; iter && *iter; iter++){ + if(!strcmp(name, (*iter)->name)){ + return *iter; + } + } + } + return NULL; +} + +struct timeval backend_timeout(){ + size_t u; + uint32_t res, secs = 1, msecs = 0; + + for(u = 0; u < registry.n; u++){ + //only call interval if backend has instances + if(registry.instances[u] && registry.backends[u].interval){ + res = registry.backends[u].interval(); + if(res && (res / 1000) < secs){ + DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); + secs = res / 1000; + msecs = res % 1000; + } + else if(res && res / 1000 == secs && (res % 1000) < msecs){ + DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); + msecs = res % 1000; + } + } + } + + struct timeval tv = { + secs, + msecs * 1000 + }; + return tv; +} + +MM_API int mm_backend_register(backend b){ + if(!backend_match(b.name)){ + registry.backends = realloc(registry.backends, (registry.n + 1) * sizeof(backend)); + registry.instances = realloc(registry.instances, (registry.n + 1) * sizeof(instance**)); + if(!registry.backends || !registry.instances){ + fprintf(stderr, "Failed to allocate memory\n"); + registry.n = 0; + return 1; + } + registry.backends[registry.n] = b; + registry.instances[registry.n] = NULL; + registry.n++; + + fprintf(stderr, "Registered backend %s\n", b.name); + return 0; + } + return 1; +} + +int backends_start(){ + int rv = 0, current; + instance** inst = NULL; + size_t n, u; + + for(u = 0; u < registry.n; u++){ + //skip backends without instances + if(!registry.instances[u]){ + continue; + } + + //fetch list of instances + if(mm_backend_instances(registry.backends[u].name, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list for initialization of backend %s\n", registry.backends[u].name); + return 1; + } + + //start the backend + current = registry.backends[u].start(n, inst); + if(current){ + fprintf(stderr, "Failed to start backend %s\n", registry.backends[u].name); + } + + //clean up + free(inst); + inst = NULL; + rv |= current; + } + return rv; +} + +static void channels_free(){ + size_t u, p; + for(u = 0; u < sizeof(channels.n) / sizeof(channels.n[0]); u++){ + DBGPF("Cleaning up channel registry bucket %" PRIsize_t " with %" PRIsize_t " channels", u, channels.n[u]); + for(p = 0; p < channels.n[u]; p++){ + DBGPF("Destroying channel %" PRIu64 " on instance %s\n", channels.entry[u][p]->ident, channels.entry[u][p]->instance->name); + //call the channel_free function if the backend supports it + if(channels.entry[u][p]->impl && channels.entry[u][p]->instance->backend->channel_free){ + channels.entry[u][p]->instance->backend->channel_free(channels.entry[u][p]); + } + free(channels.entry[u][p]); + } + free(channels.entry[u]); + channels.entry[u] = NULL; + channels.n[u] = 0; + } +} + +int backends_stop(){ + size_t u, n; + instance** inst = NULL; + + //channels before instances to support proper shutdown procedures + channels_free(); + + //shut down the registry + for(u = 0; u < registry.n; u++){ + //fetch list of instances + if(mm_backend_instances(registry.backends[u].name, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list for shutdown of backend %s\n", registry.backends[u].name); + inst = NULL; + n = 0; + } + + registry.backends[u].shutdown(n, inst); + free(inst); + inst = NULL; + + //free instances + for(inst = registry.instances[u]; inst && *inst; inst++){ + free((*inst)->name); + (*inst)->name = NULL; + (*inst)->backend = NULL; + free(*inst); + } + free(registry.instances[u]); + registry.instances[u] = NULL; + } + + free(registry.backends); + free(registry.instances); + registry.n = 0; + return 0; +} diff --git a/core/backend.h b/core/backend.h new file mode 100644 index 0000000..6a69508 --- /dev/null +++ b/core/backend.h @@ -0,0 +1,17 @@ +#include + +/* Internal API */ +int backends_handle(size_t nfds, managed_fd* fds); +int backends_notify(size_t nev, channel** c, channel_value* v); +backend* backend_match(char* name); +instance* instance_match(char* name); +struct timeval backend_timeout(); +int backends_start(); +int backends_stop(); +instance* mm_instance(backend* b); + +/* Backend API */ +MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create); +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/core/config.c b/core/config.c new file mode 100644 index 0000000..c1c3124 --- /dev/null +++ b/core/config.c @@ -0,0 +1,708 @@ +#include +#include +#include +#include +#ifndef _WIN32 +#include +#endif + +#define BACKEND_NAME "core/cfg" +#include "midimonster.h" +#include "config.h" +#include "backend.h" + +static enum { + none, + backend_cfg, + instance_cfg, + map +} parser_state = none; + +typedef enum { + map_ltr, + map_rtl, + map_bidir +} map_type; + +static backend* current_backend = NULL; +static instance* current_instance = NULL; +static size_t noverrides = 0; +static config_override* overrides = NULL; + +#ifdef _WIN32 +#define GETLINE_BUFFER 4096 + +static ssize_t getline(char** line, size_t* alloc, FILE* stream){ + size_t bytes_read = 0; + char c; + //sanity checks + if(!line || !alloc || !stream){ + return -1; + } + + //allocate buffer if none provided + if(!*line || !*alloc){ + *alloc = GETLINE_BUFFER; + *line = calloc(GETLINE_BUFFER, sizeof(char)); + if(!*line){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + } + + if(feof(stream)){ + return -1; + } + + for(c = fgetc(stream); 1; c = fgetc(stream)){ + //end of buffer, resize + if(bytes_read == (*alloc) - 1){ + *alloc += GETLINE_BUFFER; + *line = realloc(*line, (*alloc) * sizeof(char)); + if(!*line){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + } + + //store character + (*line)[bytes_read] = c; + + //end of line + if(feof(stream) || c == '\n'){ + //terminate string + (*line)[bytes_read + 1] = 0; + return bytes_read; + } + + //input broken + if(ferror(stream)){ + return -1; + } + + bytes_read++; + } +} +#endif + +static char* config_trim_line(char* in){ + ssize_t n; + //trim front + for(; *in && !isgraph(*in); in++){ + } + + //trim back + for(n = strlen(in); n >= 0 && !isgraph(in[n]); n--){ + in[n] = 0; + } + + return in; +} + +static int config_glob_parse_range(channel_glob* glob, char* spec, size_t length){ + //FIXME might want to allow negative delimiters at some point + char* parse_offset = NULL; + glob->type = glob_range; + + //first interval member + glob->limits.u64[0] = strtoul(spec, &parse_offset, 10); + if(!parse_offset || parse_offset - spec >= length || strncmp(parse_offset, "..", 2)){ + return 1; + } + + parse_offset += 2; + //second interval member + glob->limits.u64[1] = strtoul(parse_offset, &parse_offset, 10); + if(!parse_offset || parse_offset - spec != length || *parse_offset != '}'){ + return 1; + } + + //calculate number of channels within interval + if(glob->limits.u64[0] < glob->limits.u64[1]){ + glob->values = glob->limits.u64[1] - glob->limits.u64[0] + 1; + } + else if(glob->limits.u64[0] > glob->limits.u64[1]){ + glob->values = glob->limits.u64[0] - glob->limits.u64[1] + 1; + } + else{ + glob->values = 1; + } + + return 0; +} + +static int config_glob_parse_list(channel_glob* glob, char* spec, size_t length){ + size_t u = 0; + glob->type = glob_list; + glob->values = 1; + + //count number of values in list + for(u = 0; u < length; u++){ + if(spec[u] == ','){ + glob->values++; + } + } + return 0; +} + +static int config_glob_parse(channel_glob* glob, char* spec, size_t length){ + size_t u = 0; + + //detect glob type + for(u = 0; u < length; u++){ + if(length - u > 2 && !strncmp(spec + u, "..", 2)){ + DBGPF("Detected glob %.*s as range type", (int) length, spec); + return config_glob_parse_range(glob, spec, length); + } + else if(spec[u] == ','){ + DBGPF("Detected glob %.*s as list type", (int) length, spec); + return config_glob_parse_list(glob, spec, length); + } + } + + LOGPF("Failed to detect glob type for spec %.*s", (int) length, spec); + return 1; +} + +static int config_glob_scan(instance* inst, channel_spec* spec){ + char* glob_start = spec->spec, *glob_end = NULL; + size_t u; + + //assume a spec is one channel as default + spec->channels = 1; + + //scan and mark globs + for(glob_start = strchr(glob_start, '{'); glob_start; glob_start = strchr(glob_start, '{')){ + glob_end = strchr(glob_start, '}'); + if(!glob_end){ + fprintf(stderr, "Failed to parse channel spec, unterminated glob: %s\n", spec->spec); + return 1; + } + + spec->glob = realloc(spec->glob, (spec->globs + 1) * sizeof(channel_glob)); + if(!spec->glob){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + spec->glob[spec->globs].offset[0] = glob_start - spec->spec; + spec->glob[spec->globs].offset[1] = glob_end - spec->spec; + spec->globs++; + + //skip this opening brace + glob_start++; + } + + //try to parse globs internally + spec->internal = 1; + for(u = 0; u < spec->globs; u++){ + if(config_glob_parse(spec->glob + u, + spec->spec + spec->glob[u].offset[0] + 1, + spec->glob[u].offset[1] - spec->glob[u].offset[0] - 1)){ + spec->internal = 0; + break; + } + } + if(!spec->internal){ + //TODO try to parse globs externally + fprintf(stderr, "Failed to parse glob %" PRIsize_t " in %s internally\n", u + 1, spec->spec); + return 1; + } + + //calculate channel total + for(u = 0; u < spec->globs; u++){ + spec->channels *= spec->glob[u].values; + } + return 0; +} + +static ssize_t config_glob_resolve_range(char* spec, size_t length, channel_glob* glob, uint64_t n){ + uint64_t current_value = glob->limits.u64[0] + (n % glob->values); + //if counting down + if(glob->limits.u64[0] > glob->limits.u64[1]){ + current_value = glob->limits.u64[0] - (n % glob->values); + } + + //write out value + return snprintf(spec, length, "%" PRIu64, current_value); +} + +static ssize_t config_glob_resolve_list(char* spec, size_t length, channel_glob* glob, uint64_t n){ + uint64_t current_replacement = 0; + size_t replacement_length = 0; + char* source = spec + 1; + n %= glob->values; + + //find start of replacement value + DBGPF("Searching instance %" PRIu64 " of spec %.*s", n, (int) length, spec); + for(current_replacement = 0; current_replacement < n; current_replacement++){ + for(; source[0] != ','; source++){ + } + source++; + } + + //calculate replacement length + for(; source[replacement_length] != ',' && source[replacement_length] != '}'; replacement_length++){ + } + + //write out new value + memmove(spec, source, replacement_length); + return replacement_length; +} + +static channel* config_glob_resolve(instance* inst, channel_spec* spec, uint64_t n, uint8_t map_direction){ + size_t glob = 0, glob_length; + ssize_t bytes = 0; + channel* result = NULL; + char* resolved_spec = strdup(spec->spec); + + if(!resolved_spec){ + LOG("Failed to allocate memory"); + return NULL; + } + + //TODO if not internal, try to resolve externally + //iterate and resolve globs + for(glob = spec->globs; glob > 0; glob--){ + glob_length = spec->glob[glob - 1].offset[1] - spec->glob[glob - 1].offset[0]; + + switch(spec->glob[glob - 1].type){ + case glob_range: + bytes = config_glob_resolve_range(resolved_spec + spec->glob[glob - 1].offset[0], + glob_length, + spec->glob + (glob - 1), + n); + break; + case glob_list: + bytes = config_glob_resolve_list(resolved_spec + spec->glob[glob - 1].offset[0], + glob_length, + spec->glob + (glob - 1), + n); + break; + } + + n /= spec->glob[glob - 1].values; + + //move trailing data + if(bytes > 0 && bytes < glob_length){ + memmove(resolved_spec + spec->glob[glob - 1].offset[0] + bytes, + resolved_spec + spec->glob[glob - 1].offset[1] + 1, + strlen(spec->spec) - spec->glob[glob - 1].offset[1]); + } + else{ + LOGPF("Failure parsing glob spec %s", resolved_spec); + goto bail; + } + } + + DBGPF("Resolved spec %s to %s", spec->spec, resolved_spec); + result = inst->backend->channel(inst, resolved_spec, map_direction); + if(spec->globs && !result){ + LOGPF("Failed to match multichannel evaluation %s to a channel", resolved_spec); + } + +bail: + free(resolved_spec); + return result; +} + +static int config_map(char* to_raw, char* from_raw){ + //create a copy because the original pointer may be used multiple times + char* to = strdup(to_raw), *from = strdup(from_raw); + channel_spec spec_to = { + .spec = to + }, spec_from = { + .spec = from + }; + instance* instance_to = NULL, *instance_from = NULL; + channel* channel_from = NULL, *channel_to = NULL; + uint64_t n = 0; + int rv = 1; + + if(!from || !to){ + free(from); + free(to); + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + //separate channel spec from instance + for(; *(spec_to.spec) && *(spec_to.spec) != '.'; spec_to.spec++){ + } + + for(; *(spec_from.spec) && *(spec_from.spec) != '.'; spec_from.spec++){ + } + + if(!spec_from.spec[0] || !spec_to.spec[0]){ + fprintf(stderr, "Mapping does not contain a proper instance specification\n"); + goto done; + } + + //terminate + spec_from.spec[0] = spec_to.spec[0] = 0; + spec_from.spec++; + spec_to.spec++; + + //find matching instances + instance_to = instance_match(to); + instance_from = instance_match(from); + + if(!instance_to || !instance_from){ + fprintf(stderr, "No such instance %s\n", instance_from ? to : from); + goto done; + } + + //scan for globs + if(config_glob_scan(instance_to, &spec_to) + || config_glob_scan(instance_from, &spec_from)){ + goto done; + } + + if((spec_to.channels != spec_from.channels && spec_from.channels != 1 && spec_to.channels != 1) + || spec_to.channels == 0 + || spec_from.channels == 0){ + fprintf(stderr, "Multi-channel specification size mismatch: %s.%s (%" PRIsize_t " channels) - %s.%s (%" PRIsize_t " channels)\n", + instance_from->name, + spec_from.spec, + spec_from.channels, + instance_to->name, + spec_to.spec, + spec_to.channels); + goto done; + } + + //iterate, resolve globs and map + rv = 0; + for(n = 0; !rv && n < max(spec_from.channels, spec_to.channels); n++){ + channel_from = config_glob_resolve(instance_from, &spec_from, min(n, spec_from.channels), mmchannel_input); + channel_to = config_glob_resolve(instance_to, &spec_to, min(n, spec_to.channels), mmchannel_output); + + if(!channel_from || !channel_to){ + rv = 1; + goto done; + } + rv |= mm_map_channel(channel_from, channel_to); + } + +done: + free(spec_from.glob); + free(spec_to.glob); + free(from); + free(to); + return rv; +} + +static int config_line(char* line){ + map_type mapping_type = map_rtl; + char* separator = NULL; + size_t u; + + line = config_trim_line(line); + if(*line == ';' || strlen(line) == 0){ + //skip comments + return 0; + } + if(*line == '[' && line[strlen(line) - 1] == ']'){ + if(!strncmp(line, "[backend ", 9)){ + //backend configuration + parser_state = backend_cfg; + line[strlen(line) - 1] = 0; + current_backend = backend_match(line + 9); + + if(!current_backend){ + fprintf(stderr, "Cannot configure unknown backend %s\n", line + 9); + return 1; + } + + //apply overrides + for(u = 0; u < noverrides; u++){ + if(!overrides[u].handled && overrides[u].type == override_backend + && !strcmp(overrides[u].target, current_backend->name)){ + if(current_backend->conf(overrides[u].option, overrides[u].value)){ + fprintf(stderr, "Configuration override for %s failed for backend %s\n", + overrides[u].option, current_backend->name); + return 1; + } + overrides[u].handled = 1; + } + } + } + else if(!strncmp(line, "[include ", 9)){ + line[strlen(line) - 1] = 0; + return config_read(line + 9); + } + else if(!strcmp(line, "[map]")){ + //mapping configuration + parser_state = map; + } + else{ + //backend instance configuration + parser_state = instance_cfg; + + //trim braces + line[strlen(line) - 1] = 0; + line++; + + //find separating space and terminate + for(separator = line; *separator && *separator != ' '; separator++){ + } + if(!*separator){ + fprintf(stderr, "No instance name specified for backend %s\n", line); + return 1; + } + *separator = 0; + separator++; + + current_backend = backend_match(line); + if(!current_backend){ + fprintf(stderr, "No such backend %s\n", line); + return 1; + } + + if(instance_match(separator)){ + fprintf(stderr, "Duplicate instance name %s\n", separator); + return 1; + } + + //validate instance name + if(strchr(separator, ' ') || strchr(separator, '.')){ + fprintf(stderr, "Invalid instance name %s\n", separator); + return 1; + } + + current_instance = mm_instance(current_backend); + if(!current_instance){ + return 1; + } + + if(current_backend->create(current_instance)){ + fprintf(stderr, "Failed to create %s instance %s\n", line, separator); + return 1; + } + + current_instance->name = strdup(separator); + current_instance->backend = current_backend; + fprintf(stderr, "Created %s instance %s\n", line, separator); + + //apply overrides + for(u = 0; u < noverrides; u++){ + if(!overrides[u].handled && overrides[u].type == override_instance + && !strcmp(overrides[u].target, current_instance->name)){ + if(current_backend->conf_instance(current_instance, overrides[u].option, overrides[u].value)){ + fprintf(stderr, "Configuration override for %s failed for instance %s\n", + overrides[u].option, current_instance->name); + return 1; + } + overrides[u].handled = 1; + } + } + } + } + else if(parser_state == map){ + mapping_type = map_rtl; + //find separator + for(separator = line; *separator && *separator != '<' && *separator != '>'; separator++){ + } + + switch(*separator){ + case '>': + mapping_type = map_ltr; + //fall through + case '<': //default + *separator = 0; + separator++; + break; + case 0: + default: + fprintf(stderr, "Not a channel mapping: %s\n", line); + return 1; + } + + if((mapping_type == map_ltr && *separator == '<') + || (mapping_type == map_rtl && *separator == '>')){ + mapping_type = map_bidir; + separator++; + } + + line = config_trim_line(line); + separator = config_trim_line(separator); + + if(mapping_type == map_ltr || mapping_type == map_bidir){ + if(config_map(separator, line)){ + fprintf(stderr, "Failed to map channel %s to %s\n", line, separator); + return 1; + } + } + if(mapping_type == map_rtl || mapping_type == map_bidir){ + if(config_map(line, separator)){ + fprintf(stderr, "Failed to map channel %s to %s\n", separator, line); + return 1; + } + } + } + else{ + //pass to parser + //find separator + separator = strchr(line, '='); + if(!separator){ + fprintf(stderr, "Not an assignment (currently expecting %s configuration): %s\n", line, (parser_state == backend_cfg) ? "backend" : "instance"); + return 1; + } + + *separator = 0; + separator++; + line = config_trim_line(line); + separator = config_trim_line(separator); + + if(parser_state == backend_cfg && current_backend->conf(line, separator)){ + fprintf(stderr, "Failed to configure backend %s\n", current_backend->name); + return 1; + } + else if(parser_state == instance_cfg && current_backend->conf_instance(current_instance, line, separator)){ + fprintf(stderr, "Failed to configure instance %s\n", current_instance->name); + return 1; + } + } + + return 0; +} + +int config_read(char* cfg_filepath){ + int rv = 1; + size_t line_alloc = 0; + ssize_t status; + FILE* source = NULL; + char* line_raw = NULL; + + //create heap copy of file name because original might be in readonly memory + char* source_dir = strdup(cfg_filepath), *source_file = NULL, original_dir[PATH_MAX * 2] = ""; + #ifdef _WIN32 + char path_separator = '\\'; + #else + char path_separator = '/'; + #endif + + if(!source_dir){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + //change working directory to the one containing the configuration file so relative paths work as expected + source_file = strrchr(source_dir, path_separator); + if(source_file){ + *source_file = 0; + source_file++; + + if(!getcwd(original_dir, sizeof(original_dir))){ + fprintf(stderr, "Failed to read current working directory: %s\n", strerror(errno)); + goto bail; + } + + if(chdir(source_dir)){ + fprintf(stderr, "Failed to change to configuration file directory %s: %s\n", source_dir, strerror(errno)); + goto bail; + } + } + else{ + source_file = source_dir; + } + + fprintf(stderr, "Reading configuration file %s\n", cfg_filepath); + source = fopen(source_file, "r"); + + if(!source){ + fprintf(stderr, "Failed to open %s for reading\n", cfg_filepath); + goto bail; + } + + for(status = getline(&line_raw, &line_alloc, source); status >= 0; status = getline(&line_raw, &line_alloc, source)){ + if(config_line(line_raw)){ + goto bail; + } + } + + //TODO check whether all overrides have been applied + + rv = 0; +bail: + //change back to previous directory to allow recursive configuration file parsing + if(source_file && source_dir != source_file){ + chdir(original_dir); + } + + free(source_dir); + if(source){ + fclose(source); + } + free(line_raw); + return rv; +} + +int config_add_override(override_type type, char* data_raw){ + int rv = 1; + //heap a copy because the original data is probably not writable + char* data = strdup(data_raw); + + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + goto bail; + } + + char* option = strchr(data, '.'); + char* value = strchr(data, '='); + + if(!option || !value){ + fprintf(stderr, "Override %s is not a valid assignment\n", data_raw); + goto bail; + } + + //terminate strings + *option = 0; + option++; + + *value = 0; + value++; + + config_override new = { + .type = type, + .handled = 0, + .target = strdup(config_trim_line(data)), + .option = strdup(config_trim_line(option)), + .value = strdup(config_trim_line(value)) + }; + + if(!new.target || !new.option || !new.value){ + fprintf(stderr, "Failed to allocate memory\n"); + goto bail; + } + + overrides = realloc(overrides, (noverrides + 1) * sizeof(config_override)); + if(!overrides){ + noverrides = 0; + fprintf(stderr, "Failed to allocate memory\n"); + goto bail; + } + overrides[noverrides] = new; + noverrides++; + + rv = 0; +bail: + free(data); + return rv; +} + +void config_free(){ + size_t u; + + for(u = 0; u < noverrides; u++){ + free(overrides[u].target); + free(overrides[u].option); + free(overrides[u].value); + } + + noverrides = 0; + free(overrides); + overrides = NULL; + + parser_state = none; +} diff --git a/core/config.h b/core/config.h new file mode 100644 index 0000000..b96a866 --- /dev/null +++ b/core/config.h @@ -0,0 +1,54 @@ +/* + * Channel glob type + */ +enum /*_mm_channel_glob_type */ { + glob_range, + glob_list +}; + +/* + * Channel specification glob + */ +typedef struct /*_mm_channel_glob*/ { + size_t offset[2]; + union { + void* impl; + uint64_t u64[2]; + } limits; + uint8_t type; + uint64_t values; +} channel_glob; + +/* + * (Multi-)Channel specification + */ +typedef struct /*_mm_channel_spec*/ { + char* spec; + uint8_t internal; + size_t channels; + size_t globs; + channel_glob* glob; +} channel_spec; + +/* + * Command-line override types + */ +typedef enum { + override_backend, + override_instance +} override_type; + +/* + * Command-line override data + */ +typedef struct /*_mm_config_override*/ { + override_type type; + uint8_t handled; + char* target; + char* option; + char* value; +} config_override; + +int config_read(char* file); +int config_add_override(override_type type, char* data); +void config_free(); diff --git a/core/plugin.c b/core/plugin.c new file mode 100644 index 0000000..e7d8eba --- /dev/null +++ b/core/plugin.c @@ -0,0 +1,187 @@ +#include +#include +#include +#include +#include +#include +#include +#include "portability.h" +#ifdef _WIN32 +#define dlclose FreeLibrary +#define dlsym GetProcAddress +#define dlerror() "Failed" +#define dlopen(lib,ig) LoadLibrary(lib) +#else +#include +#endif + +#include "plugin.h" + +static size_t plugins = 0; +static void** plugin_handle = NULL; + +static int plugin_attach(char* path, char* file){ + plugin_init init = NULL; + void* handle = NULL; + char* lib = NULL; + #ifdef _WIN32 + char* path_separator = "\\"; + #else + char* path_separator = "/"; + #endif + + if(!path || !file || !strlen(path)){ + fprintf(stderr, "Invalid plugin loader path\n"); + return 1; + } + + lib = calloc(strlen(path) + strlen(file) + 2, sizeof(char)); + if(!lib){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + snprintf(lib, strlen(path) + strlen(file) + 2, "%s%s%s", + path, + (path[strlen(path) - 1] == path_separator[0]) ? "" : path_separator, + file); + + handle = dlopen(lib, RTLD_NOW); + if(!handle){ + #ifdef _WIN32 + char* error = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + fprintf(stderr, "Failed to load plugin %s, check that all supporting libraries are present: %s\n", lib, error); + LocalFree(error); + #else + fprintf(stderr, "Failed to load plugin %s: %s\n", lib, dlerror()); + #endif + free(lib); + return 0; + } + + init = (plugin_init) dlsym(handle, "init"); + if(init){ + if(init()){ + fprintf(stderr, "Plugin %s failed to initialize\n", lib); + dlclose(handle); + free(lib); + return 1; + } + } + else{ + dlclose(handle); + free(lib); + return 0; + } + free(lib); + + plugin_handle = realloc(plugin_handle, (plugins + 1) * sizeof(void*)); + if(!plugin_handle){ + fprintf(stderr, "Failed to allocate memory\n"); + dlclose(handle); + return 1; + } + + plugin_handle[plugins] = handle; + plugins++; + + return 0; +} + +int plugins_load(char* path){ + int rv = -1; + +#ifdef _WIN32 + char* search_expression = calloc(strlen(path) + strlen("*.dll") + 1, sizeof(char)); + if(!search_expression){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + snprintf(search_expression, strlen(path) + strlen("*.dll"), "%s*.dll", path); + + WIN32_FIND_DATA result; + HANDLE hSearch = FindFirstFile(search_expression, &result); + + if(hSearch == INVALID_HANDLE_VALUE){ + LPVOID lpMsgBuf = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL); + fprintf(stderr, "Failed to search for backend plugin files in %s: %s\n", path, lpMsgBuf); + LocalFree(lpMsgBuf); + return -1; + } + + do { + if(plugin_attach(path, result.cFileName)){ + goto load_done; + } + } while(FindNextFile(hSearch, &result)); + + rv = 0; +load_done: + free(search_expression); + FindClose(hSearch); + return rv; +#else + struct dirent* entry; + struct stat file_stat; + DIR* directory = opendir(path); + if(!directory){ + fprintf(stderr, "Failed to open plugin search path %s: %s\n", path, strerror(errno)); + return 1; + } + + for(entry = readdir(directory); entry; entry = readdir(directory)){ + if(strlen(entry->d_name) < 4 || strncmp(".so", entry->d_name + (strlen(entry->d_name) - 3), 3)){ + continue; + } + + if(fstatat(dirfd(directory), entry->d_name, &file_stat, 0) < 0){ + fprintf(stderr, "Failed to stat %s: %s\n", entry->d_name, strerror(errno)); + continue; + } + + if(!S_ISREG(file_stat.st_mode)){ + continue; + } + + if(plugin_attach(path, entry->d_name)){ + goto load_done; + } + } + rv = 0; + +load_done: + if(closedir(directory) < 0){ + fprintf(stderr, "Failed to close plugin directory %s: %s\n", path, strerror(errno)); + return -1; + } + return rv; +#endif +} + +int plugins_close(){ + size_t u; + + for(u = 0; u < plugins; u++){ +#ifdef _WIN32 + char* error = NULL; + //FreeLibrary returns the inverse of dlclose + if(!FreeLibrary(plugin_handle[u])){ + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + fprintf(stderr, "Failed to unload plugin: %s\n", error); + LocalFree(error); + } +#else + if(dlclose(plugin_handle[u])){ + fprintf(stderr, "Failed to unload plugin: %s\n", dlerror()); + } +#endif + } + + free(plugin_handle); + plugins = 0; + return 0; +} diff --git a/core/plugin.h b/core/plugin.h new file mode 100644 index 0000000..64c557f --- /dev/null +++ b/core/plugin.h @@ -0,0 +1,3 @@ +typedef int (*plugin_init)(); +int plugins_load(char* dir); +int plugins_close(); diff --git a/plugin.c b/plugin.c deleted file mode 100644 index e7d8eba..0000000 --- a/plugin.c +++ /dev/null @@ -1,187 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include "portability.h" -#ifdef _WIN32 -#define dlclose FreeLibrary -#define dlsym GetProcAddress -#define dlerror() "Failed" -#define dlopen(lib,ig) LoadLibrary(lib) -#else -#include -#endif - -#include "plugin.h" - -static size_t plugins = 0; -static void** plugin_handle = NULL; - -static int plugin_attach(char* path, char* file){ - plugin_init init = NULL; - void* handle = NULL; - char* lib = NULL; - #ifdef _WIN32 - char* path_separator = "\\"; - #else - char* path_separator = "/"; - #endif - - if(!path || !file || !strlen(path)){ - fprintf(stderr, "Invalid plugin loader path\n"); - return 1; - } - - lib = calloc(strlen(path) + strlen(file) + 2, sizeof(char)); - if(!lib){ - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } - snprintf(lib, strlen(path) + strlen(file) + 2, "%s%s%s", - path, - (path[strlen(path) - 1] == path_separator[0]) ? "" : path_separator, - file); - - handle = dlopen(lib, RTLD_NOW); - if(!handle){ - #ifdef _WIN32 - char* error = NULL; - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); - fprintf(stderr, "Failed to load plugin %s, check that all supporting libraries are present: %s\n", lib, error); - LocalFree(error); - #else - fprintf(stderr, "Failed to load plugin %s: %s\n", lib, dlerror()); - #endif - free(lib); - return 0; - } - - init = (plugin_init) dlsym(handle, "init"); - if(init){ - if(init()){ - fprintf(stderr, "Plugin %s failed to initialize\n", lib); - dlclose(handle); - free(lib); - return 1; - } - } - else{ - dlclose(handle); - free(lib); - return 0; - } - free(lib); - - plugin_handle = realloc(plugin_handle, (plugins + 1) * sizeof(void*)); - if(!plugin_handle){ - fprintf(stderr, "Failed to allocate memory\n"); - dlclose(handle); - return 1; - } - - plugin_handle[plugins] = handle; - plugins++; - - return 0; -} - -int plugins_load(char* path){ - int rv = -1; - -#ifdef _WIN32 - char* search_expression = calloc(strlen(path) + strlen("*.dll") + 1, sizeof(char)); - if(!search_expression){ - fprintf(stderr, "Failed to allocate memory\n"); - return -1; - } - snprintf(search_expression, strlen(path) + strlen("*.dll"), "%s*.dll", path); - - WIN32_FIND_DATA result; - HANDLE hSearch = FindFirstFile(search_expression, &result); - - if(hSearch == INVALID_HANDLE_VALUE){ - LPVOID lpMsgBuf = NULL; - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL); - fprintf(stderr, "Failed to search for backend plugin files in %s: %s\n", path, lpMsgBuf); - LocalFree(lpMsgBuf); - return -1; - } - - do { - if(plugin_attach(path, result.cFileName)){ - goto load_done; - } - } while(FindNextFile(hSearch, &result)); - - rv = 0; -load_done: - free(search_expression); - FindClose(hSearch); - return rv; -#else - struct dirent* entry; - struct stat file_stat; - DIR* directory = opendir(path); - if(!directory){ - fprintf(stderr, "Failed to open plugin search path %s: %s\n", path, strerror(errno)); - return 1; - } - - for(entry = readdir(directory); entry; entry = readdir(directory)){ - if(strlen(entry->d_name) < 4 || strncmp(".so", entry->d_name + (strlen(entry->d_name) - 3), 3)){ - continue; - } - - if(fstatat(dirfd(directory), entry->d_name, &file_stat, 0) < 0){ - fprintf(stderr, "Failed to stat %s: %s\n", entry->d_name, strerror(errno)); - continue; - } - - if(!S_ISREG(file_stat.st_mode)){ - continue; - } - - if(plugin_attach(path, entry->d_name)){ - goto load_done; - } - } - rv = 0; - -load_done: - if(closedir(directory) < 0){ - fprintf(stderr, "Failed to close plugin directory %s: %s\n", path, strerror(errno)); - return -1; - } - return rv; -#endif -} - -int plugins_close(){ - size_t u; - - for(u = 0; u < plugins; u++){ -#ifdef _WIN32 - char* error = NULL; - //FreeLibrary returns the inverse of dlclose - if(!FreeLibrary(plugin_handle[u])){ - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); - fprintf(stderr, "Failed to unload plugin: %s\n", error); - LocalFree(error); - } -#else - if(dlclose(plugin_handle[u])){ - fprintf(stderr, "Failed to unload plugin: %s\n", dlerror()); - } -#endif - } - - free(plugin_handle); - plugins = 0; - return 0; -} diff --git a/plugin.h b/plugin.h deleted file mode 100644 index 64c557f..0000000 --- a/plugin.h +++ /dev/null @@ -1,3 +0,0 @@ -typedef int (*plugin_init)(); -int plugins_load(char* dir); -int plugins_close(); -- cgit v1.2.3 From a743153aafffb38a6d8765d1f1c634dde51d5558 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 21 Jun 2021 22:15:15 +0200 Subject: Repository cleanup, part 2 --- .ci.sh | 134 ----------------------------------------- .travis.yml | 157 ------------------------------------------------- MIDIMonster.svg | 1 - Makefile | 18 +++--- TODO | 15 ----- assets/MIDIMonster.svg | 1 + assets/TODO | 15 +++++ assets/ci-config.yml | 157 +++++++++++++++++++++++++++++++++++++++++++++++++ assets/ci.sh | 134 +++++++++++++++++++++++++++++++++++++++++ assets/midimonster.1 | 42 +++++++++++++ assets/midimonster.ico | Bin 0 -> 321510 bytes assets/midimonster.rc | 22 +++++++ midimonster.1 | 42 ------------- midimonster.c | 6 +- midimonster.ico | Bin 321510 -> 0 bytes midimonster.rc | 22 ------- 16 files changed, 385 insertions(+), 381 deletions(-) delete mode 100755 .ci.sh delete mode 100644 .travis.yml delete mode 100644 MIDIMonster.svg delete mode 100644 TODO create mode 100644 assets/MIDIMonster.svg create mode 100644 assets/TODO create mode 100644 assets/ci-config.yml create mode 100755 assets/ci.sh create mode 100644 assets/midimonster.1 create mode 100644 assets/midimonster.ico create mode 100644 assets/midimonster.rc delete mode 100644 midimonster.1 delete mode 100644 midimonster.ico delete mode 100644 midimonster.rc diff --git a/.ci.sh b/.ci.sh deleted file mode 100755 index 4a646a9..0000000 --- a/.ci.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/bin/bash - -# Check for Travis and use the provided fold method if detected -if declare -f travis_fold > /dev/null; then - ci_fold(){ - travis_fold "$1" "$2" - } -else - ci_fold(){ - printf -- "-- %s stage %s --\n" "$1" "$2" - } -fi - -if [ -z "$OS" ]; then - OS="linux" -fi - -if [ "$TASK" = "spellcheck" ]; then - result=0 - # Create list of files to be spellchecked - spellcheck_files=$(find . -type f | grep -v ".git/") - - # Run spellintian to find spelling errors - sl_results=$(xargs spellintian 2>&1 <<< "$spellcheck_files") - - sl_errors=$(wc -l <<< "$sl_results") - sl_errors_dups=$((grep "\(duplicate word\)" | wc -l) <<< "$sl_results") - sl_errors_nodups=$((grep -v "\(duplicate word\)" | wc -l) <<< "$sl_results") - - if [ "$sl_errors" -ne 0 ]; then - printf "Spellintian found %s errors (%s spelling, %s duplicate words):\n\n" "$sl_errors" "$sl_errors_nodups" "$sl_errors_dups" - printf "%s\n\n" "$sl_results" - result=1 - else - printf "Spellintian reports no errors\n" - fi - - # Run codespell to find some more - cs_results=$(xargs codespell --quiet 2 <<< "$spellcheck_files" 2>&1) - cs_errors=$(wc -l <<< "$cs_results") - if [ "$cs_errors" -ne 0 ]; then - printf "Codespell found %s errors:\n\n" "$cs_errors" - printf "%s\n\n" "$cs_results" - result=1 - else - printf "Codespell reports no errors\n" - fi - exit "$result" -elif [ "$TASK" = "codesmell" ]; then - result=0 - - if [ -z "$(which lizard)" ]; then - printf "Installing lizard...\n" - pip3 install lizard - fi - - # Run shellcheck for all shell scripts - printf "Running shellcheck...\n" - shell_files="$(find . -type f -iname \*.sh)" - xargs shellcheck -Cnever -s bash <<< "$shell_files" - if [ "$?" -ne "0" ]; then - result=1 - fi - - # Run cloc for some stats - printf "Code statistics:\n\n" - cloc ./ - - # Run lizard for the project - printf "Running lizard for code complexity analysis\n" - lizard ./ - if [ "$?" -ne "0" ]; then - result=1 - fi - - exit "$result" -elif [ "$TASK" = "sanitize" ]; then - # Run sanitized compile - ci_fold start "make_sanitize" - if ! make sanitize; then - printf "Failed to build\n" - exit 1 - fi - ci_fold end "make_sanitize" -elif [ "$TASK" = "windows" ]; then - ci_fold start "make_windows" - if ! make windows; then - printf "Failed to build\n" - exit 1 - fi - make -C backends lua.dll - ci_fold end "make_windows" - if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then - ci_fold start "deploy_windows" - mkdir ./deployment - mkdir ./deployment/backends - mkdir ./deployment/docs - # Strip the Windows binaries as they become huge quickly - strip midimonster.exe backends/*.dll - cp ./midimonster.exe ./deployment/ - cp ./backends/*.dll ./deployment/backends/ - cp ./backends/*.dll.disabled ./deployment/backends/ - cp ./monster.cfg ./deployment/monster.cfg - cp ./backends/*.md ./deployment/docs/ - cp -r ./configs ./deployment/ - cd ./deployment - zip -r "./midimonster-$(git describe)-windows.zip" "./" - find . ! -iname '*.zip' -delete - ci_fold end "deploy_windows" - fi -else - # Otherwise compile as normal - ci_fold start "make" - if ! make full; then - printf "Failed to build\n" - exit 1 - fi - ci_fold end "make" - if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then - ci_fold start "deploy_unix" - mkdir ./deployment - mkdir ./deployment/backends - mkdir ./deployment/docs - cp ./midimonster ./deployment/ - cp ./backends/*.so ./deployment/backends/ - cp ./monster.cfg ./deployment/monster.cfg - cp ./backends/*.md ./deployment/docs/ - cp -r ./configs ./deployment/ - cd ./deployment - tar czf "midimonster-$(git describe)-$OS.tgz" "./" - find . ! -iname '*.tgz' -delete - ci_fold end "deploy_unix" - fi -fi diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9fbe236..0000000 --- a/.travis.yml +++ /dev/null @@ -1,157 +0,0 @@ -language: c -group: edge -os: linux -dist: bionic - -before_script: - - export -f travis_fold - - export OS="$TRAVIS_OS_NAME" - -script: - - "bash .ci.sh" - -addons: - apt: - packages: &core_build - # This is all the bits we need to enable all options - - libasound2-dev - - libevdev-dev - - libola-dev - - libjack-jackd2-dev - - liblua5.3-dev - - python3-dev - - libssl-dev - - lintian - packages: &core_build_gpp_latest - - *core_build - - gcc-8 - - g++-8 - packages: &core_build_clang_latest - - *core_build - - clang-6.0 - packages: &core_build_windows - - *core_build - - mingw-w64 - packages: &linters - - python3 - - python3-pip - - lintian - - codespell - - shellcheck - - cloc - -jobs: - fast_finish: true - include: - - os: linux - dist: bionic - compiler: clang - env: TASK='compile' - addons: - apt: - packages: - - *core_build_clang_latest - - os: linux - dist: bionic - compiler: gcc - env: TASK='compile' - addons: - apt: - packages: - - *core_build_gpp_latest - - os: linux - dist: bionic - compiler: mingw32-gcc - env: - - TASK='windows' - - CC='x86_64-w64-mingw32-gcc' - addons: - apt: - packages: - - *core_build_windows - - os: linux - dist: bionic - compiler: clang - env: TASK='sanitize' - addons: - apt: - packages: - - *core_build_clang_latest - - os: osx - osx_image: xcode10.2 - compiler: clang - env: - - TASK='compile' - - os: osx - osx_image: xcode10.2 - compiler: clang - env: - - TASK='sanitize' - - os: linux - dist: bionic - env: TASK='codesmell' - addons: - apt: - packages: - - *linters - - os: linux - dist: bionic - env: TASK='spellcheck' - addons: - apt: - packages: - - *linters - allow_failures: - - os: linux - dist: bionic - env: TASK='codesmell' - - os: linux - dist: bionic - env: TASK='spellcheck' - -env: - global: - # No colours in terminal (to reduce log file size) - - TERM=dumb - # Parallel make build - - MAKEFLAGS="-j 4" - -cache: - apt: true - -before_install: -# Travis clones with --branch, which omits tags. Since we use them for the version string at build time, fetch them - - git pull --tags - - printf "This is %s on %s\n" "$(git describe)" "$TRAVIS_OS_NAME" - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi -# 'brew install' sometimes returns non-zero for some arcane reason. Executing 'true' resets the exit code and allows Travis to continue building... -# Travis seems to have Python 2.7 installed by default, which for some reason prevents pkg-config from reading python3.pc - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ola lua openssl jack python3; brew link --overwrite python; true; fi -# OpenSSL is not a proper install due to some Apple bull, so provide additional locations via the environment... -# Additionally, newer versions of this "recipe" seem to use the name 'openssl@1.1' instead of plain 'openssl' and there seems to be -# no way to programmatically get the link and include paths. Genius! Hardcoding the new version for the time being... - - export CFLAGS="$CFLAGS -I/usr/local/opt/openssl@1.1/include" - - export LDFLAGS="$LDFLAGS -L/usr/local/opt/openssl@1.1/lib" -#Use the latest clang if we're compiling with clang - - if [ "$TRAVIS_OS_NAME" == "linux" -a "$CC" = "clang" ]; then export CC="clang-6.0"; export CXX="clang-6.0"; fi -# Download libraries to link with for Windows - - if [ "$TASK" == "windows" ]; then wget "https://downloads.sourceforge.net/project/luabinaries/5.3.5/Windows%20Libraries/Dynamic/lua-5.3.5_Win64_dllw6_lib.zip" -O lua53.zip; unzip lua53.zip lua53.dll; fi - -notifications: - irc: - channels: - - "irc.hackint.org#midimonster" - on_success: change # default: always - on_failure: always # default: always - nick: mm_ci - use_notice: true - -deploy: - provider: releases - file_glob: true - token: $GITHUB_TOKEN - file: ./deployment/* - skip_cleanup: true - draft: true - on: - tags: true diff --git a/MIDIMonster.svg b/MIDIMonster.svg deleted file mode 100644 index 7e411dc..0000000 --- a/MIDIMonster.svg +++ /dev/null @@ -1 +0,0 @@ -Asset 3 \ No newline at end of file diff --git a/Makefile b/Makefile index 9b95f1b..50fc73e 100644 --- a/Makefile +++ b/Makefile @@ -11,14 +11,18 @@ GITVERSION = $(shell git describe) CFLAGS ?= -g -Wall -Wpedantic #CFLAGS += -DDEBUG # Hide all non-API symbols for export -CFLAGS += -fvisibility=hidden -I./ +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/TODO b/TODO deleted file mode 100644 index 9158e24..0000000 --- a/TODO +++ /dev/null @@ -1,15 +0,0 @@ -keepalive channels per backend? -Note source in channel value struct -udp backends may ignore MTU -make event collectors threadsafe to stop marshalling data... -collect & check backend API version -move all connection establishment to _start to be able to hot-stop/start all backends -event deduplication in core? -move all typenames to _t - -per-channel filters - * invert - * edge detection - -channel discovery / enumeration -note exit condition/reconnection details for backends diff --git a/assets/MIDIMonster.svg b/assets/MIDIMonster.svg new file mode 100644 index 0000000..7e411dc --- /dev/null +++ b/assets/MIDIMonster.svg @@ -0,0 +1 @@ +Asset 3 \ No newline at end of file diff --git a/assets/TODO b/assets/TODO new file mode 100644 index 0000000..9158e24 --- /dev/null +++ b/assets/TODO @@ -0,0 +1,15 @@ +keepalive channels per backend? +Note source in channel value struct +udp backends may ignore MTU +make event collectors threadsafe to stop marshalling data... +collect & check backend API version +move all connection establishment to _start to be able to hot-stop/start all backends +event deduplication in core? +move all typenames to _t + +per-channel filters + * invert + * edge detection + +channel discovery / enumeration +note exit condition/reconnection details for backends diff --git a/assets/ci-config.yml b/assets/ci-config.yml new file mode 100644 index 0000000..9fbe236 --- /dev/null +++ b/assets/ci-config.yml @@ -0,0 +1,157 @@ +language: c +group: edge +os: linux +dist: bionic + +before_script: + - export -f travis_fold + - export OS="$TRAVIS_OS_NAME" + +script: + - "bash .ci.sh" + +addons: + apt: + packages: &core_build + # This is all the bits we need to enable all options + - libasound2-dev + - libevdev-dev + - libola-dev + - libjack-jackd2-dev + - liblua5.3-dev + - python3-dev + - libssl-dev + - lintian + packages: &core_build_gpp_latest + - *core_build + - gcc-8 + - g++-8 + packages: &core_build_clang_latest + - *core_build + - clang-6.0 + packages: &core_build_windows + - *core_build + - mingw-w64 + packages: &linters + - python3 + - python3-pip + - lintian + - codespell + - shellcheck + - cloc + +jobs: + fast_finish: true + include: + - os: linux + dist: bionic + compiler: clang + env: TASK='compile' + addons: + apt: + packages: + - *core_build_clang_latest + - os: linux + dist: bionic + compiler: gcc + env: TASK='compile' + addons: + apt: + packages: + - *core_build_gpp_latest + - os: linux + dist: bionic + compiler: mingw32-gcc + env: + - TASK='windows' + - CC='x86_64-w64-mingw32-gcc' + addons: + apt: + packages: + - *core_build_windows + - os: linux + dist: bionic + compiler: clang + env: TASK='sanitize' + addons: + apt: + packages: + - *core_build_clang_latest + - os: osx + osx_image: xcode10.2 + compiler: clang + env: + - TASK='compile' + - os: osx + osx_image: xcode10.2 + compiler: clang + env: + - TASK='sanitize' + - os: linux + dist: bionic + env: TASK='codesmell' + addons: + apt: + packages: + - *linters + - os: linux + dist: bionic + env: TASK='spellcheck' + addons: + apt: + packages: + - *linters + allow_failures: + - os: linux + dist: bionic + env: TASK='codesmell' + - os: linux + dist: bionic + env: TASK='spellcheck' + +env: + global: + # No colours in terminal (to reduce log file size) + - TERM=dumb + # Parallel make build + - MAKEFLAGS="-j 4" + +cache: + apt: true + +before_install: +# Travis clones with --branch, which omits tags. Since we use them for the version string at build time, fetch them + - git pull --tags + - printf "This is %s on %s\n" "$(git describe)" "$TRAVIS_OS_NAME" + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi +# 'brew install' sometimes returns non-zero for some arcane reason. Executing 'true' resets the exit code and allows Travis to continue building... +# Travis seems to have Python 2.7 installed by default, which for some reason prevents pkg-config from reading python3.pc + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ola lua openssl jack python3; brew link --overwrite python; true; fi +# OpenSSL is not a proper install due to some Apple bull, so provide additional locations via the environment... +# Additionally, newer versions of this "recipe" seem to use the name 'openssl@1.1' instead of plain 'openssl' and there seems to be +# no way to programmatically get the link and include paths. Genius! Hardcoding the new version for the time being... + - export CFLAGS="$CFLAGS -I/usr/local/opt/openssl@1.1/include" + - export LDFLAGS="$LDFLAGS -L/usr/local/opt/openssl@1.1/lib" +#Use the latest clang if we're compiling with clang + - if [ "$TRAVIS_OS_NAME" == "linux" -a "$CC" = "clang" ]; then export CC="clang-6.0"; export CXX="clang-6.0"; fi +# Download libraries to link with for Windows + - if [ "$TASK" == "windows" ]; then wget "https://downloads.sourceforge.net/project/luabinaries/5.3.5/Windows%20Libraries/Dynamic/lua-5.3.5_Win64_dllw6_lib.zip" -O lua53.zip; unzip lua53.zip lua53.dll; fi + +notifications: + irc: + channels: + - "irc.hackint.org#midimonster" + on_success: change # default: always + on_failure: always # default: always + nick: mm_ci + use_notice: true + +deploy: + provider: releases + file_glob: true + token: $GITHUB_TOKEN + file: ./deployment/* + skip_cleanup: true + draft: true + on: + tags: true diff --git a/assets/ci.sh b/assets/ci.sh new file mode 100755 index 0000000..4a646a9 --- /dev/null +++ b/assets/ci.sh @@ -0,0 +1,134 @@ +#!/bin/bash + +# Check for Travis and use the provided fold method if detected +if declare -f travis_fold > /dev/null; then + ci_fold(){ + travis_fold "$1" "$2" + } +else + ci_fold(){ + printf -- "-- %s stage %s --\n" "$1" "$2" + } +fi + +if [ -z "$OS" ]; then + OS="linux" +fi + +if [ "$TASK" = "spellcheck" ]; then + result=0 + # Create list of files to be spellchecked + spellcheck_files=$(find . -type f | grep -v ".git/") + + # Run spellintian to find spelling errors + sl_results=$(xargs spellintian 2>&1 <<< "$spellcheck_files") + + sl_errors=$(wc -l <<< "$sl_results") + sl_errors_dups=$((grep "\(duplicate word\)" | wc -l) <<< "$sl_results") + sl_errors_nodups=$((grep -v "\(duplicate word\)" | wc -l) <<< "$sl_results") + + if [ "$sl_errors" -ne 0 ]; then + printf "Spellintian found %s errors (%s spelling, %s duplicate words):\n\n" "$sl_errors" "$sl_errors_nodups" "$sl_errors_dups" + printf "%s\n\n" "$sl_results" + result=1 + else + printf "Spellintian reports no errors\n" + fi + + # Run codespell to find some more + cs_results=$(xargs codespell --quiet 2 <<< "$spellcheck_files" 2>&1) + cs_errors=$(wc -l <<< "$cs_results") + if [ "$cs_errors" -ne 0 ]; then + printf "Codespell found %s errors:\n\n" "$cs_errors" + printf "%s\n\n" "$cs_results" + result=1 + else + printf "Codespell reports no errors\n" + fi + exit "$result" +elif [ "$TASK" = "codesmell" ]; then + result=0 + + if [ -z "$(which lizard)" ]; then + printf "Installing lizard...\n" + pip3 install lizard + fi + + # Run shellcheck for all shell scripts + printf "Running shellcheck...\n" + shell_files="$(find . -type f -iname \*.sh)" + xargs shellcheck -Cnever -s bash <<< "$shell_files" + if [ "$?" -ne "0" ]; then + result=1 + fi + + # Run cloc for some stats + printf "Code statistics:\n\n" + cloc ./ + + # Run lizard for the project + printf "Running lizard for code complexity analysis\n" + lizard ./ + if [ "$?" -ne "0" ]; then + result=1 + fi + + exit "$result" +elif [ "$TASK" = "sanitize" ]; then + # Run sanitized compile + ci_fold start "make_sanitize" + if ! make sanitize; then + printf "Failed to build\n" + exit 1 + fi + ci_fold end "make_sanitize" +elif [ "$TASK" = "windows" ]; then + ci_fold start "make_windows" + if ! make windows; then + printf "Failed to build\n" + exit 1 + fi + make -C backends lua.dll + ci_fold end "make_windows" + if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then + ci_fold start "deploy_windows" + mkdir ./deployment + mkdir ./deployment/backends + mkdir ./deployment/docs + # Strip the Windows binaries as they become huge quickly + strip midimonster.exe backends/*.dll + cp ./midimonster.exe ./deployment/ + cp ./backends/*.dll ./deployment/backends/ + cp ./backends/*.dll.disabled ./deployment/backends/ + cp ./monster.cfg ./deployment/monster.cfg + cp ./backends/*.md ./deployment/docs/ + cp -r ./configs ./deployment/ + cd ./deployment + zip -r "./midimonster-$(git describe)-windows.zip" "./" + find . ! -iname '*.zip' -delete + ci_fold end "deploy_windows" + fi +else + # Otherwise compile as normal + ci_fold start "make" + if ! make full; then + printf "Failed to build\n" + exit 1 + fi + ci_fold end "make" + if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then + ci_fold start "deploy_unix" + mkdir ./deployment + mkdir ./deployment/backends + mkdir ./deployment/docs + cp ./midimonster ./deployment/ + cp ./backends/*.so ./deployment/backends/ + cp ./monster.cfg ./deployment/monster.cfg + cp ./backends/*.md ./deployment/docs/ + cp -r ./configs ./deployment/ + cd ./deployment + tar czf "midimonster-$(git describe)-$OS.tgz" "./" + find . ! -iname '*.tgz' -delete + ci_fold end "deploy_unix" + fi +fi diff --git a/assets/midimonster.1 b/assets/midimonster.1 new file mode 100644 index 0000000..44c414e --- /dev/null +++ b/assets/midimonster.1 @@ -0,0 +1,42 @@ +.TH MIDIMONSTER 1 "December 2019" +.SH NAME +midimonster \- Multi-protocol translation tool +.SH SYNOPSIS +.B midimonster +.I config-file +.RB [ "-i" +.IR instance.option=value ] +.RB [ "-b" +.IR backend.option=value ] + +.B midimonster -v +.SH DESCRIPTION +.B MIDIMonster +allows the user to translate any channel on one supported protocol into channel(s) +on any other (or the same) supported protocol. +.SH OPTIONS +.TP +.I config-file +The configuration file to read. If not specified, a default configuration file is read. + +.TP +.BI "-i " instance.option=value +Supply an additional instance configuration option +.IR option " for " instance "." +Command-line overrides are applied when the instance is first mentioned in the configuration file. + +.TP +.BI "-b " backend.option=value +Supply an additional backend configuration option +.IR option " to " backend "." +Command-line overrides are applied when the backend is first mentioned in the configuration file. + +.B -v +Display version information +.SH "SEE ALSO" +Online documentation and repository at https://github.com/cbdevnet/midimonster + +For more and in-depth information see the homepage at https://midimonster.net/ +as well as the knowledge base at https://kb.midimonster.net/ +.SH AUTHOR +Fabian "cbdev" Stumpf diff --git a/assets/midimonster.ico b/assets/midimonster.ico new file mode 100644 index 0000000..9391160 Binary files /dev/null and b/assets/midimonster.ico differ diff --git a/assets/midimonster.rc b/assets/midimonster.rc new file mode 100644 index 0000000..45a88aa --- /dev/null +++ b/assets/midimonster.rc @@ -0,0 +1,22 @@ +#include "midimonster.h" + +0 ICON "midimonster.ico" +1 VERSIONINFO +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", "control8r" + VALUE "FileDescription", "MIDIMonster" + VALUE "InternalName", "MIDIMonster Core (Windows Build)" + VALUE "FileVersion", MIDIMONSTER_VERSION + VALUE "OriginalFilename", "midimonster.exe" + VALUE "ProductName", "MIDIMonster" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/midimonster.1 b/midimonster.1 deleted file mode 100644 index 44c414e..0000000 --- a/midimonster.1 +++ /dev/null @@ -1,42 +0,0 @@ -.TH MIDIMONSTER 1 "December 2019" -.SH NAME -midimonster \- Multi-protocol translation tool -.SH SYNOPSIS -.B midimonster -.I config-file -.RB [ "-i" -.IR instance.option=value ] -.RB [ "-b" -.IR backend.option=value ] - -.B midimonster -v -.SH DESCRIPTION -.B MIDIMonster -allows the user to translate any channel on one supported protocol into channel(s) -on any other (or the same) supported protocol. -.SH OPTIONS -.TP -.I config-file -The configuration file to read. If not specified, a default configuration file is read. - -.TP -.BI "-i " instance.option=value -Supply an additional instance configuration option -.IR option " for " instance "." -Command-line overrides are applied when the instance is first mentioned in the configuration file. - -.TP -.BI "-b " backend.option=value -Supply an additional backend configuration option -.IR option " to " backend "." -Command-line overrides are applied when the backend is first mentioned in the configuration file. - -.B -v -Display version information -.SH "SEE ALSO" -Online documentation and repository at https://github.com/cbdevnet/midimonster - -For more and in-depth information see the homepage at https://midimonster.net/ -as well as the knowledge base at https://kb.midimonster.net/ -.SH AUTHOR -Fabian "cbdev" Stumpf diff --git a/midimonster.c b/midimonster.c index 3849953..51fe7ad 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*/ { diff --git a/midimonster.ico b/midimonster.ico deleted file mode 100644 index 9391160..0000000 Binary files a/midimonster.ico and /dev/null differ diff --git a/midimonster.rc b/midimonster.rc deleted file mode 100644 index 45a88aa..0000000 --- a/midimonster.rc +++ /dev/null @@ -1,22 +0,0 @@ -#include "midimonster.h" - -0 ICON "midimonster.ico" -1 VERSIONINFO -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904E4" - BEGIN - VALUE "CompanyName", "control8r" - VALUE "FileDescription", "MIDIMonster" - VALUE "InternalName", "MIDIMonster Core (Windows Build)" - VALUE "FileVersion", MIDIMONSTER_VERSION - VALUE "OriginalFilename", "midimonster.exe" - VALUE "ProductName", "MIDIMonster" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x0409, 1252 - END -END -- cgit v1.2.3 From 68818d94980bfc22d6414ed29d2d9cbb6be24cb1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 21 Jun 2021 22:18:05 +0200 Subject: Fix missing logo image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f958b25..696a46f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # The MIDIMonster -MIDIMonster Logo +MIDIMonster Logo [![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) -- cgit v1.2.3 From ca055614db57386f0e43d65bb10e501bdde699ef Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 22 Jun 2021 03:29:08 +0200 Subject: Keep channel registry map in sync when updating identifiers (#76) --- backends/maweb.c | 2 +- core/backend.c | 47 +++++++++++++++++++++++++++++++++++++++++++++-- core/backend.h | 1 + midimonster.c | 4 ++-- midimonster.h | 25 ++++++++++++++++++++----- 5 files changed, 69 insertions(+), 10 deletions(-) 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/core/backend.c b/core/backend.c index 16e095c..8a8588f 100644 --- a/core/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/core/backend.h b/core/backend.h index 6a69508..46c6c3a 100644 --- a/core/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/midimonster.c b/midimonster.c index 51fe7ad..5817ac7 100644 --- a/midimonster.c +++ b/midimonster.c @@ -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 @@ -244,6 +250,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 -- cgit v1.2.3 From 653c41a208ac004a412e4529f494b8b07baa2172 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 23 Jun 2021 01:27:35 +0200 Subject: Fix sanitize build circular include --- DEVELOPMENT.md | 12 ++++++++++++ Makefile | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) 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 + diff --git a/Makefile b/Makefile index 50fc73e..de9b24e 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ CFLAGS += -fvisibility=hidden # Subdirectory objects need the include path RCCFLAGS += -I./ -CFLAGS += -I./ +core/%: CFLAGS += -I./ midimonster: LDLIBS = -ldl # Replace version string with current git-describe if possible -- cgit v1.2.3 From a89dcd7942958e0ebe5881dde4820ceb77d29e9b Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 23 Jun 2021 22:27:15 +0200 Subject: Fix build with recent environments, fix Coverity CID 371602 and 355842 --- backends/Makefile | 12 +++++++----- backends/mqtt.c | 7 ++++--- backends/rtpmidi.c | 11 ++++++----- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/backends/Makefile b/backends/Makefile index aa9c988..4697a5a 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -60,7 +60,7 @@ openpixelcontrol.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) openpixelcontrol.dll: LDLIBS += -lws2_32 maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB) -maweb.so: LDLIBS = -lssl +maweb.so: LDLIBS = $(shell pkg-config --libs openssl || echo "-DBUILD_ERROR=\"Missing pkg-config data for openssl\"") maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) maweb.dll: LDLIBS += -lws2_32 maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL @@ -88,18 +88,20 @@ lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 || lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || pkg-config --cflags lua || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") lua.dll: LDLIBS += -L../ -llua53 -python.so: CFLAGS += $(shell pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") -python.so: CFLAGS += $(shell pkg-config --libs python3 || pkg-config --libs python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +# Python seems to ship their own little python3-config tool instead of properly maintaining their pkg-config files. +# This one also spams a good deal of unwanted flags into CFLAGS, so we use only --includes and --libs +python.so: CFLAGS += $(shell python3-config --includes || pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +python.so: LDLIBS += $(shell python3-config --libs || pkg-config --libs python3 || pkg-config --libs python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") # Generic rules on how to build .SO/.DLL's from C and CPP sources %.so :: %.c %.h $(BACKEND_LIB) - $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) + $(CC) $(CFLAGS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) $(LDLIBS) %.dll :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) $(LDLIBS) %.so :: %.cpp %.h - $(CXX) $(CPPFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) + $(CXX) $(CPPFLAGS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) $(LDLIBS) # This is the actual first named target, and thus the default all: $(BACKEND_LIB) $(BACKENDS) diff --git a/backends/mqtt.c b/backends/mqtt.c index f2a7c83..47042f1 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -775,14 +775,15 @@ static int mqtt_handle_publish(instance* inst, uint8_t type, uint8_t* variable_h property_offset += mqtt_pop_varint(variable_header + property_offset, length - property_offset, NULL); //parse properties - while(property_offset < payload_offset){ + while(property_offset < payload_offset && property_offset < length){ 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){ + if(variable_header[property_offset] == 0x01 && property_offset < length - 1){ content_utf8 = variable_header[property_offset + 1]; } //read topic alias - else if(variable_header[property_offset] == 0x23){ + else if(variable_header[property_offset] == 0x23 && property_offset < length - 2){ topic_alias = (variable_header[property_offset + 1] << 8) | variable_header[property_offset + 2]; } diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index f0987f2..922075e 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1329,13 +1329,14 @@ static int rtpmidi_mdns_detach(instance* inst){ } offset += bytes; - //TODO length-checks here - frame[offset++] = strlen(inst->name); - memcpy(frame + offset, inst->name, strlen(inst->name)); - offset += strlen(inst->name); + //calculate maximum permitted instance name length + bytes = min(min(strlen(inst->name), sizeof(frame) - offset - 3), 255); + frame[offset++] = bytes; + memcpy(frame + offset, inst->name, bytes); + offset += bytes; frame[offset++] = 0xC0; frame[offset++] = sizeof(dns_header); - rr->data = htobe16(1 + strlen(inst->name) + 2); + rr->data = htobe16(1 + bytes + 2); free(name.name); return rtpmidi_mdns_broadcast(frame, offset); -- cgit v1.2.3 From f646d10ae1c7f9df1cc91243d7166d74791b487b Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 23 Jun 2021 22:41:23 +0200 Subject: Prefer pkg-config over python3-config for linker info --- backends/Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backends/Makefile b/backends/Makefile index 4697a5a..5054236 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -89,9 +89,10 @@ lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5 lua.dll: LDLIBS += -L../ -llua53 # Python seems to ship their own little python3-config tool instead of properly maintaining their pkg-config files. -# This one also spams a good deal of unwanted flags into CFLAGS, so we use only --includes and --libs +# This one also spams a good deal of unwanted flags into CFLAGS, so we use only --includes. On the other hand, the --libs +# info from this one seems to include the actual interpreter library only on some systems, which makes it worse than useless. python.so: CFLAGS += $(shell python3-config --includes || pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") -python.so: LDLIBS += $(shell python3-config --libs || pkg-config --libs python3 || pkg-config --libs python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +python.so: LDLIBS += $(shell pkg-config --libs python3-embed || python3-config --libs || pkg-config --libs python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") # Generic rules on how to build .SO/.DLL's from C and CPP sources %.so :: %.c %.h $(BACKEND_LIB) -- cgit v1.2.3 From 933ca8e600e9f481080ee42a4f77a376c7038b14 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 26 Jun 2021 18:36:12 +0200 Subject: Update debug messages --- core/backend.c | 10 +++++----- midimonster.c | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/backend.c b/core/backend.c index 8a8588f..83121bd 100644 --- a/core/backend.c +++ b/core/backend.c @@ -51,7 +51,7 @@ int backends_handle(size_t nfds, managed_fd* fds){ //handle if there is data ready or the backend has active instances for polling if(n || registry.instances[u]){ - DBGPF("Notifying backend %s of %" PRIsize_t " waiting FDs\n", registry.backends[u].name, n); + DBGPF("Notifying backend %s of %" PRIsize_t " waiting FDs", registry.backends[u].name, n); rv |= registry.backends[u].process(n, fds); if(rv){ fprintf(stderr, "Backend %s failed to handle input\n", registry.backends[u].name); @@ -85,7 +85,7 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ } //TODO eliminate duplicates - DBGPF("Calling handler for instance %s with %" PRIsize_t " events\n", c[u]->instance->name, n - u); + DBGPF("Calling handler for instance %s with %" PRIsize_t " events", c[u]->instance->name, n - u); rv |= c[u]->instance->backend->handle(c[u]->instance, n - u, c + u, v + u); } @@ -105,11 +105,11 @@ MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create){ } if(!create){ - DBGPF("Requested unknown channel %" PRIu64 " (bucket %" PRIsize_t ") on instance %s\n", ident, bucket, inst->name); + DBGPF("Requested unknown channel %" PRIu64 " (bucket %" PRIsize_t ") on instance %s", ident, bucket, inst->name); return NULL; } - DBGPF("Creating previously unknown channel %" PRIu64 " on instance %s, bucket %" PRIsize_t "\n", ident, inst->name, bucket); + DBGPF("Creating previously unknown channel %" PRIu64 " on instance %s, bucket %" PRIsize_t, ident, inst->name, bucket); channels.entry[bucket] = realloc(channels.entry[bucket], (channels.n[bucket] + 1) * sizeof(channel*)); if(!channels.entry[bucket]){ fprintf(stderr, "Failed to allocate memory\n"); @@ -353,7 +353,7 @@ static void channels_free(){ for(u = 0; u < sizeof(channels.n) / sizeof(channels.n[0]); u++){ DBGPF("Cleaning up channel registry bucket %" PRIsize_t " with %" PRIsize_t " channels", u, channels.n[u]); for(p = 0; p < channels.n[u]; p++){ - DBGPF("Destroying channel %" PRIu64 " on instance %s\n", channels.entry[u][p]->ident, channels.entry[u][p]->instance->name); + DBGPF("Destroying channel %" PRIu64 " on instance %s", channels.entry[u][p]->ident, channels.entry[u][p]->instance->name); //call the channel_free function if the backend supports it if(channels.entry[u][p]->impl && channels.entry[u][p]->instance->backend->channel_free){ channels.entry[u][p]->instance->backend->channel_free(channels.entry[u][p]); diff --git a/midimonster.c b/midimonster.c index 5817ac7..b73eeff 100644 --- a/midimonster.c +++ b/midimonster.c @@ -263,7 +263,7 @@ static fd_set fds_collect(int* max_fd){ *max_fd = -1; } - DBGPF("Building selector set from %lu FDs registered to core\n", fds); + DBGPF("Building selector set from %" PRIsize_t " FDs registered to core", fds); FD_ZERO(&rv_fds); for(u = 0; u < fds; u++){ if(fd[u].fd >= 0){ @@ -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", nfds); + DBGPF("%" PRIsize_t " 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", routing.events->n); + DBGPF("Swapping event collectors, %" PRIsize_t " 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; -- cgit v1.2.3 From e28bfea439d09bd0e9fabc865c83ce73e6b16b37 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 26 Jun 2021 19:15:53 +0200 Subject: Fix VISCA windows build includes --- backends/visca.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backends/visca.c b/backends/visca.c index 611d142..a43d74f 100644 --- a/backends/visca.c +++ b/backends/visca.c @@ -3,8 +3,11 @@ #include #include -#include -#include + +#ifndef _WIN32 + #include + #include +#endif #include "visca.h" #include "libmmbackend.h" -- cgit v1.2.3 From 011be343cd1427a1be68f9a0da38401b89de0fec Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 27 Jun 2021 16:20:52 +0200 Subject: Fix python backend calls --- backends/Makefile | 2 +- backends/python.c | 13 +++++++++++-- backends/python.md | 4 ++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/backends/Makefile b/backends/Makefile index 5054236..6c46007 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -92,7 +92,7 @@ lua.dll: LDLIBS += -L../ -llua53 # This one also spams a good deal of unwanted flags into CFLAGS, so we use only --includes. On the other hand, the --libs # info from this one seems to include the actual interpreter library only on some systems, which makes it worse than useless. python.so: CFLAGS += $(shell python3-config --includes || pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") -python.so: LDLIBS += $(shell pkg-config --libs python3-embed || python3-config --libs || pkg-config --libs python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +python.so: LDLIBS += $(shell pkg-config --libs python3-embed || python3-config --libs || pkg-config --libs python3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") # Generic rules on how to build .SO/.DLL's from C and CPP sources %.so :: %.c %.h $(BACKEND_LIB) diff --git a/backends/python.c b/backends/python.c index b9b6518..c1a21be 100644 --- a/backends/python.c +++ b/backends/python.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "python" +//#define DEBUG #define PY_SSIZE_T_CLEAN #include @@ -34,6 +35,8 @@ MM_PLUGIN_API int init(){ LOG("Failed to register backend"); return 1; } + + //Py_UnbufferedStdioFlag = 1; return 0; } @@ -159,7 +162,7 @@ static PyObject* mmpy_output(PyObject* self, PyObject* args){ else{ mm_channel_event(mm_channel(inst, u, 0), val); } - return 0; + break; } } @@ -401,6 +404,7 @@ static int mmpy_exec(PyObject* module) { PyObject* capsule = PyDict_GetItemString(PyThreadState_GetDict(), MMPY_INSTANCE_KEY); if(capsule && inst){ *inst = PyCapsule_GetPointer(capsule, NULL); + DBGPF("Initializing extension module on instance %s", (*inst)->name); return 0; } @@ -415,6 +419,7 @@ static int python_configure_instance(instance* inst, char* option, char* value){ //load python script if(!strcmp(option, "module")){ //swap to interpreter + //PyThreadState_Swap(data->interpreter); PyEval_RestoreThread(data->interpreter); //import the module module = PyImport_ImportModule(value); @@ -491,8 +496,10 @@ static int python_instance(instance* inst){ Py_SetProgramName(program_name); //initialize python Py_InitializeEx(0); - //create, acquire and release the GIL + #if PY_MINOR_VERSION < 7 + //in python 3.6 and earlier, this was required to set up the GIL PyEval_InitThreads(); + #endif python_main = PyEval_SaveThread(); } @@ -717,6 +724,8 @@ static int python_start(size_t n, instance** inst){ //release interpreter PyEval_ReleaseThread(data->interpreter); } + + last_timestamp = mm_timestamp(); return 0; } diff --git a/backends/python.md b/backends/python.md index 2114a08..1c0c96f 100644 --- a/backends/python.md +++ b/backends/python.md @@ -79,8 +79,8 @@ The `python` backend does not take any global configuration. | Option | Example value | Default value | Description | |-----------------------|-----------------------|-----------------------|-----------------------------------------------| -| `module` | `my_handlers.py` | none | (Path to) Python module source file, relative to configuration file location | -| `default-handler` | `mu_handlers.default` | none | Function to be called as handler for all top-level channels (not belonging to a module) | +| `module` | `my_handlers` | none | Name of the python module to load (normally the name of a`.py` file without the extension) | +| `default-handler` | `my_handlers.default` | none | Function to be called as handler for all top-level channels (not belonging to a module) | A single instance may have multiple `module` options specified. This will make all handlers available within their module namespaces (see the section on channel specification). -- cgit v1.2.3 From a92c6fb9f20ae26335b2ea3bd4539dd6e4aec256 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 27 Jun 2021 16:45:35 +0200 Subject: Add a python/lua example configuration --- configs/pyexample.py | 8 ++++++++ configs/scripting-example.cfg | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 configs/pyexample.py create mode 100644 configs/scripting-example.cfg diff --git a/configs/pyexample.py b/configs/pyexample.py new file mode 100644 index 0000000..7213005 --- /dev/null +++ b/configs/pyexample.py @@ -0,0 +1,8 @@ +# Import the MIDIMonster Python API +import midimonster + +def channel1(value): + # Print current input value + print("Python channel 1 is at %s" % (value,)) + # Send inverse on py1.out1 + midimonster.output("out1", 1.0 - value) diff --git a/configs/scripting-example.cfg b/configs/scripting-example.cfg new file mode 100644 index 0000000..fb9d6ca --- /dev/null +++ b/configs/scripting-example.cfg @@ -0,0 +1,22 @@ +; Turn on debugging to see what is coming in +[backend osc] +detect = on + +[python py1] +; This will load the pyexample.py script into this instance +module = pyexample + +[lua lua1] +; This will load the print.lua script into this instance +script = print.lua +; This will send all mapped channels to the `printchannel` function in there +default-handler = printchannel + +[osc in] +; Listen on port 8000 and send answers on port 9000 +bind = 0.0.0.0 8000 +dest = learn@9000 + +[map] +in./1/fader1 > py1.pyexample.channel1 +py1.out1 > lua1.lua-input -- cgit v1.2.3 From 9be900acd86e03c73266c552db133562005f5607 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 28 Jun 2021 21:46:55 +0200 Subject: Enhance visca relative movement --- backends/visca.c | 42 ++++++++++++++++++++++++++---------------- backends/visca.h | 8 ++++---- backends/visca.md | 6 +++++- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/backends/visca.c b/backends/visca.c index a43d74f..a36b139 100644 --- a/backends/visca.c +++ b/backends/visca.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "visca" -#define DEBUG +//#define DEBUG #include #include @@ -17,6 +17,8 @@ * Command output rate limiting / deduplication * Inquiry * Reconnect on connection close + * Speed updates should send motor outputs + * */ MM_PLUGIN_API int init(){ @@ -144,8 +146,8 @@ static int ptz_instance(instance* inst){ data->fd = -1; data->cam_address = 1; //start with maximum speeds - data->panspeed = ptz_channels[panspeed].max; - data->tiltspeed = ptz_channels[tiltspeed].max; + data->max_pan = ptz_channels[panspeed].max; + data->max_tilt = ptz_channels[tiltspeed].max; //start with a bit of slack/deadzone in relative movement axes data->deadzone = 0.1; @@ -218,8 +220,9 @@ static size_t ptz_set_pantilt(instance* inst, channel* c, channel_value* v, uint data->y = ((ptz_channels[tilt].max - ptz_channels[tilt].min) * v->normalised) + ptz_channels[tilt].min - ptz_channels[tilt].offset; } - msg[4] = data->panspeed; - msg[5] = data->tiltspeed; + //absolute movements happen with maximum speed + msg[4] = data->max_pan; + msg[5] = data->max_tilt; //either i'm doing this wrong or visca is just weird. msg[6] = ((data->x & 0xF000) >> 12); @@ -238,10 +241,10 @@ static size_t ptz_set_pantilt(instance* inst, channel* c, channel_value* v, uint static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint8_t* msg){ ptz_instance_data* data = (ptz_instance_data*) inst->impl; if(c->ident == panspeed){ - data->panspeed = ((ptz_channels[panspeed].max - ptz_channels[panspeed].min) * v->normalised) + ptz_channels[panspeed].min - ptz_channels[panspeed].offset; + data->max_pan = ((ptz_channels[panspeed].max - ptz_channels[panspeed].min) * v->normalised) + ptz_channels[panspeed].min - ptz_channels[panspeed].offset; } else{ - data->tiltspeed = ((ptz_channels[tiltspeed].max - ptz_channels[tiltspeed].min) * v->normalised) + ptz_channels[tiltspeed].min - ptz_channels[tiltspeed].offset; + data->max_tilt = ((ptz_channels[tiltspeed].max - ptz_channels[tiltspeed].min) * v->normalised) + ptz_channels[tiltspeed].min - ptz_channels[tiltspeed].offset; } return 0; @@ -250,7 +253,7 @@ static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint static size_t ptz_set_relmove(instance* inst, channel* c, channel_value* v, uint8_t* msg){ ptz_instance_data* data = (ptz_instance_data*) inst->impl; - uint8_t direction = c->ident >> 8; + uint8_t direction = c->ident >> 8, movement = data->relative_movement; double speed_factor = v->normalised; if(direction == rel_x @@ -267,24 +270,31 @@ static size_t ptz_set_relmove(instance* inst, channel* c, channel_value* v, uint //clear modified axis if(direction & rel_x){ - data->relative_movement &= ~rel_x; + movement &= ~rel_x; + data->factor_tilt = speed_factor; } else{ - data->relative_movement &= ~rel_y; + movement &= ~rel_y; + data->factor_pan = speed_factor; } if(speed_factor){ - data->relative_movement |= direction; + movement |= direction; } + //only transmit if something actually changed + if(!movement && !data->relative_movement){ + return 0; + } + data->relative_movement = movement; + //set stored axis speed - //TODO find a way to do relative axis speed via speed_factor, without overwriting a set absolute speed - msg[4] = data->panspeed; - msg[5] = data->tiltspeed; + msg[4] = data->max_pan * data->factor_pan; + msg[5] = data->max_tilt * data->factor_tilt; //update motor control from movement data - msg[6] |= (data->relative_movement & (rel_left | rel_right)) >> 2; - msg[7] |= data->relative_movement & (rel_up | rel_down); + msg[6] |= (movement & (rel_left | rel_right)) >> 2; + msg[7] |= movement & (rel_up | rel_down); //stop motors if unset msg[6] = msg[6] ? msg[6] : 3; diff --git a/backends/visca.h b/backends/visca.h index 1b8c0e5..37f21b1 100644 --- a/backends/visca.h +++ b/backends/visca.h @@ -17,8 +17,8 @@ enum /*_ptz_relmove_channel */ { rel_down = 2, rel_left = 4, rel_right = 8, - rel_x = 3, - rel_y = 12 + rel_x = rel_up | rel_down, + rel_y = rel_left | rel_right }; typedef struct /*_ptz_instance_data*/ { @@ -26,8 +26,8 @@ typedef struct /*_ptz_instance_data*/ { uint8_t cam_address; uint16_t x; uint16_t y; - uint8_t panspeed; - uint8_t tiltspeed; + uint8_t max_pan, max_tilt; + double factor_pan, factor_tilt; uint8_t relative_movement; double deadzone; uint8_t direct_device; diff --git a/backends/visca.md b/backends/visca.md index 101aa20..7b1bcc3 100644 --- a/backends/visca.md +++ b/backends/visca.md @@ -37,7 +37,9 @@ Each instance exposes the following channels * `home`: Return to home position * `memory`: Call memory (if incoming event value is greater than 0.9) * `store`: Store current pan/tilt/zoom setup to memory (if incoming event value is greater than 0.9) -* `move.left`, `move.right`, `move.up`, `move.down`: Relative movement with the currently set `panspeed` and `tiltspeed` +* `move.left`, `move.right`, `move.up`, `move.down`: Move relative to the current position. Set speed is multiplied by the event value. +* `move.x`, `move.y`: Move relative to the current position along the specified axis. Set speed is multiplied by the event value scaled to the full range (ie. `0.0` to `0.5` moves in one direction, `0.5` to `1.0` in the other). + Example mappings: @@ -45,6 +47,8 @@ Example mappings: control.pan > visca.pan control.tilt > visca.tilt control.btn1 > visca.memory1 +control.stick_x > visca.move.x +control.stick_y > visca.move.y ``` #### Compatability list -- cgit v1.2.3 From 17551fe56c8dc20630811b1726559b00cb911abd Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 28 Jun 2021 22:28:47 +0200 Subject: Fix Coverity CIDs 371602, 355843 --- backends/mqtt.c | 2 +- backends/rtpmidi.c | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backends/mqtt.c b/backends/mqtt.c index 47042f1..da4bf38 100644 --- a/backends/mqtt.c +++ b/backends/mqtt.c @@ -765,7 +765,7 @@ static int mqtt_handle_publish(instance* inst, uint8_t type, uint8_t* variable_h 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); + size_t topic_length = min(mqtt_pop_utf8(variable_header, length, &topic), length); property_offset = payload_offset = topic_length + 2 + ((qos > 0) ? 2 : 0); if(data->mqtt_version == 0x05){ diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 922075e..d349e6f 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1299,8 +1299,10 @@ static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){ }; //send to ipv4 and ipv6 mcasts - sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); - sendto(cfg.mdns4_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)); + if((sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)) != len) + | (sendto(cfg.mdns4_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)) != len)){ + LOG("Failed to transmit mDNS frame"); + } return 0; } -- cgit v1.2.3 From 979ca16917cfca16b42f6f0464ff6c5cd41473b1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 28 Jun 2021 22:30:02 +0200 Subject: Improve windows build --- backends/Makefile | 3 +++ backends/libmmbackend.c | 8 ++++++-- backends/python.c | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/backends/Makefile b/backends/Makefile index 6c46007..be870d6 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -93,6 +93,9 @@ lua.dll: LDLIBS += -L../ -llua53 # info from this one seems to include the actual interpreter library only on some systems, which makes it worse than useless. python.so: CFLAGS += $(shell python3-config --includes || pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") python.so: LDLIBS += $(shell pkg-config --libs python3-embed || python3-config --libs || pkg-config --libs python3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +python.so: LDFLAGS += $(shell python3-config --ldflags || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +python.dll: CFLAGS += $(shell python3-config --includes || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +python.dll: LDLIBS += -L../ -lpython3 # Generic rules on how to build .SO/.DLL's from C and CPP sources %.so :: %.c %.h $(BACKEND_LIB) diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index bad048c..18611e1 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -3,6 +3,10 @@ #define LOGPF(format, ...) fprintf(stderr, "libmmbe\t" format "\n", __VA_ARGS__) #define LOG(message) fprintf(stderr, "libmmbe\t%s\n", (message)) +#ifndef _WIN32 + #define closesocket close +#endif + int mmbackend_strdup(char** dest, char* src){ if(*dest){ free(*dest); @@ -186,14 +190,14 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin if(listener){ status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); if(status < 0){ - close(fd); + closesocket(fd); continue; } } else{ status = connect(fd, addr_it->ai_addr, addr_it->ai_addrlen); if(status < 0){ - close(fd); + closesocket(fd); continue; } } diff --git a/backends/python.c b/backends/python.c index c1a21be..9c0caa1 100644 --- a/backends/python.c +++ b/backends/python.c @@ -1,6 +1,10 @@ #define BACKEND_NAME "python" //#define DEBUG +#ifdef _WIN32 + #include +#endif + #define PY_SSIZE_T_CLEAN #include #include -- cgit v1.2.3 From 12142b4808a84eb9328213997be085cb583c101c Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 29 Jun 2021 19:13:50 +0200 Subject: Update development notes --- DEVELOPMENT.md | 47 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 3fc2268..a2ec6a2 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,8 +1,7 @@ # MIDIMonster development guide This document serves as a reference for contributors interested in the low-level implementation -of the MIDIMonster. It is currently a work in progress and will be extended as problems come -up and need solving ;) +of the MIDIMonster. It will be extended as problems come up and need solving ;) ## Basics @@ -44,19 +43,55 @@ 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 +## 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 +## Build pipeline * The primary build pipeline is `make` -# Architecture +## Architecture +* Strive to make backends platform-portable + * If that is not possible, try to keep the backend configuration compatible to other backends implementing the same protocol * If there is significant potential for sharing functionality between backends, consider implementing it in `libmmbackend` +* Place a premium on keeping the MIDIMonster a lightweight tool in terms of installed dependencies and core functionality + * If possible, prefer a local implementation to one which requires additional (dynamic) dependencies -# Debugging +## Language & Communication +* All visible communication (ie. error messages, debug messages) should be complete, correct english sentences +* Strive for each output to have a concrete benefit or information to the reader + * Corollary: If nothing happens, don't send output + * Debug messages are somewhat exempt from this guideline +* For error messages, give enough context to reasonably allow the user to either track down the problem or report a meaningful issue + +# Packaging + +Packaging the MIDIMonster for release in distributions is an important task. It facilitates easy access to +the MIDIMonster functionality to a wide audience. This section is not strictly relevant for development, but touches +on some of the same principles. + +As the MIDIMonster is a tool designed for interfacing between several different protocols, applications and +other tools, to use "all" functionality of the MIDIMonster would imply installing additional software the user +might not actually need. This runs counter to our goal of staying a lightweight tool for translation and control. + +The recommended way to package the MIDIMonster for binary distribution would be to split the build artifacts into +multiple packages, separating out the heavier dependencies into separately installable units. If that is not an option, +marking external dependencies of backends as `optional` or `recommended` should be preferred to having them required +to be installed. + +Some backends have been marked optional in the repository and are only built when using `make full`. + +The recommended grouping into packaging units is as follows (without regard to platform compatibility, which +may further impact the grouping): + +* Package `midimonster`: Core, Backends `evdev`, `artnet`, `osc`, `loopback`, `sacn`, `maweb`, `openpixelcontrol`, `rtpmidi`, `visca`, `mqtt` + * External dependencies: `libevdev`, `openssl` +* Package `midimonster-programming`: Backends `lua`, `python` + * External dependencies: `liblua`, `python3` +* Package `midimonster-media`: `midi`, `jack`, `ola` + * External dependencies: `libasound2`, `libjack-jackd2`, `libola` -- cgit v1.2.3 From 6f089c7beeae6ff56bab626bc9cee52800e3b155 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Wed, 30 Jun 2021 01:59:55 +0200 Subject: Move to Jenkins CI, introduce new CI script --- assets/ci-config | 73 ++++++++++ assets/ci-config.yml | 157 --------------------- assets/ci.sh | 377 +++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 351 insertions(+), 256 deletions(-) create mode 100644 assets/ci-config delete mode 100644 assets/ci-config.yml diff --git a/assets/ci-config b/assets/ci-config new file mode 100644 index 0000000..5e8df68 --- /dev/null +++ b/assets/ci-config @@ -0,0 +1,73 @@ +#!/usr/bin/env groovy + +/* + * This Jenkinsfile is intended to run on https://ci.spacecdn.de and may fail anywhere else. + * It makes assumptions about plugins being installed, labels mapping to nodes that can build what is needed, etc. + */ + +def buildTypes = ['linux', 'windows'] +def builds = [:] + +//if(env.TAG_NAME) { +// buildTypes.add("debian") +//} + +buildTypes.each{ + builds["$it"] = { + node() { + skipDefaultCheckout() + stage('Checkout') { + checkout scm + } + + stage("$it Build"){ + sh label: "Build", script: "./assets/ci.sh --target=build-$it --deploy" + } + + stage('Stash artifacts') { + stash includes: "deployment/$it/*", name: "$it", allowEmpty: 'false' + } + } + } +} + +def deploy = { + node(){ + skipDefaultCheckout() + stage('Deploy') { + buildTypes.each{ + unstash "$it" + } + archiveArtifacts artifacts: 'deployment/*/*', onlyIfSuccessful: true, fingerprint: true + } + } +} + +builds.Test = { + node() { + skipDefaultCheckout() + stage('Checkout') { + checkout scm + } + stage('Test') { + catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { + sh label: "Check Spelling", script: './assets/ci.sh --target=check-spelling' + } + catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { + sh label: "Check Codespelling", script: './assets/ci.sh --target=check-codespelling' + } + catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { + sh label: "Analyze Complexity", script: './assets/ci.sh --target=analyze-complexity' + } + catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { + sh label: "Analyze Shellscripts", script: './assets/ci.sh--target=analyze-shellscript' + } + catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { + sh label: "Code Statistics", script: '../assets/ci.sh --target=stats' + } + } + } +} + +parallel builds +deploy.call() \ No newline at end of file diff --git a/assets/ci-config.yml b/assets/ci-config.yml deleted file mode 100644 index 9fbe236..0000000 --- a/assets/ci-config.yml +++ /dev/null @@ -1,157 +0,0 @@ -language: c -group: edge -os: linux -dist: bionic - -before_script: - - export -f travis_fold - - export OS="$TRAVIS_OS_NAME" - -script: - - "bash .ci.sh" - -addons: - apt: - packages: &core_build - # This is all the bits we need to enable all options - - libasound2-dev - - libevdev-dev - - libola-dev - - libjack-jackd2-dev - - liblua5.3-dev - - python3-dev - - libssl-dev - - lintian - packages: &core_build_gpp_latest - - *core_build - - gcc-8 - - g++-8 - packages: &core_build_clang_latest - - *core_build - - clang-6.0 - packages: &core_build_windows - - *core_build - - mingw-w64 - packages: &linters - - python3 - - python3-pip - - lintian - - codespell - - shellcheck - - cloc - -jobs: - fast_finish: true - include: - - os: linux - dist: bionic - compiler: clang - env: TASK='compile' - addons: - apt: - packages: - - *core_build_clang_latest - - os: linux - dist: bionic - compiler: gcc - env: TASK='compile' - addons: - apt: - packages: - - *core_build_gpp_latest - - os: linux - dist: bionic - compiler: mingw32-gcc - env: - - TASK='windows' - - CC='x86_64-w64-mingw32-gcc' - addons: - apt: - packages: - - *core_build_windows - - os: linux - dist: bionic - compiler: clang - env: TASK='sanitize' - addons: - apt: - packages: - - *core_build_clang_latest - - os: osx - osx_image: xcode10.2 - compiler: clang - env: - - TASK='compile' - - os: osx - osx_image: xcode10.2 - compiler: clang - env: - - TASK='sanitize' - - os: linux - dist: bionic - env: TASK='codesmell' - addons: - apt: - packages: - - *linters - - os: linux - dist: bionic - env: TASK='spellcheck' - addons: - apt: - packages: - - *linters - allow_failures: - - os: linux - dist: bionic - env: TASK='codesmell' - - os: linux - dist: bionic - env: TASK='spellcheck' - -env: - global: - # No colours in terminal (to reduce log file size) - - TERM=dumb - # Parallel make build - - MAKEFLAGS="-j 4" - -cache: - apt: true - -before_install: -# Travis clones with --branch, which omits tags. Since we use them for the version string at build time, fetch them - - git pull --tags - - printf "This is %s on %s\n" "$(git describe)" "$TRAVIS_OS_NAME" - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi -# 'brew install' sometimes returns non-zero for some arcane reason. Executing 'true' resets the exit code and allows Travis to continue building... -# Travis seems to have Python 2.7 installed by default, which for some reason prevents pkg-config from reading python3.pc - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ola lua openssl jack python3; brew link --overwrite python; true; fi -# OpenSSL is not a proper install due to some Apple bull, so provide additional locations via the environment... -# Additionally, newer versions of this "recipe" seem to use the name 'openssl@1.1' instead of plain 'openssl' and there seems to be -# no way to programmatically get the link and include paths. Genius! Hardcoding the new version for the time being... - - export CFLAGS="$CFLAGS -I/usr/local/opt/openssl@1.1/include" - - export LDFLAGS="$LDFLAGS -L/usr/local/opt/openssl@1.1/lib" -#Use the latest clang if we're compiling with clang - - if [ "$TRAVIS_OS_NAME" == "linux" -a "$CC" = "clang" ]; then export CC="clang-6.0"; export CXX="clang-6.0"; fi -# Download libraries to link with for Windows - - if [ "$TASK" == "windows" ]; then wget "https://downloads.sourceforge.net/project/luabinaries/5.3.5/Windows%20Libraries/Dynamic/lua-5.3.5_Win64_dllw6_lib.zip" -O lua53.zip; unzip lua53.zip lua53.dll; fi - -notifications: - irc: - channels: - - "irc.hackint.org#midimonster" - on_success: change # default: always - on_failure: always # default: always - nick: mm_ci - use_notice: true - -deploy: - provider: releases - file_glob: true - token: $GITHUB_TOKEN - file: ./deployment/* - skip_cleanup: true - draft: true - on: - tags: true diff --git a/assets/ci.sh b/assets/ci.sh index 4a646a9..94b8bed 100755 --- a/assets/ci.sh +++ b/assets/ci.sh @@ -1,134 +1,313 @@ #!/bin/bash +# shellcheck disable=SC2001,SC2181 -# Check for Travis and use the provided fold method if detected -if declare -f travis_fold > /dev/null; then - ci_fold(){ - travis_fold "$1" "$2" - } +################################################ SETUP ################################################ +dep_build_core=( + libasound2-dev + libevdev-dev + liblua5.3-dev + libola-dev + libjack-jackd2-dev + python3-dev + libssl-dev + build-essential + pkg-config + git +) + +dep_build_win=( + mingw-w64 +) + +dep_build_debian=( + git-buildpackage + debhelper +) + +exitcode="0" + +############################################## FUNCTIONS ############################################## + +ARGS(){ + for i in "$@"; do + case "$i" in + --target=*|-t=*) + TARGETS="${i#*=}" + ;; + --deploy) + deploy="1" + ;; + --deps) + install_deps="1" + ;; + -v|--verbose) + verbose="1" + ;; + -af|--allow-failure) + allow_failure="1" + ;; + -h|--help|*) + print_help + exit "0" + ;; + esac + shift + done + [[ -z $TARGETS ]] && print_help && printf "\nNo target specified!\n" && exit "1" # If no target(s) are specified exit. +} + +print_help() { + printf "Usage: %s [OPTIONS]\n\n" "$0" + printf -- "-t=, \t--target=, \n\n" + printf -- "--deploy\tPackage release/nightly versions to the ./deployment/\$target directory.\n" + printf -- "--deps\t\tCheck and install all dependencies needed for the specified target without the need to manualy run the dependency install targets/s.\n" + printf -- "-af, --allow-failure\tAlways exit with code 0.\n" + printf -- "-v, --verbose\tEnables detailed log output.\n\n" + printf "Valid test targets are: \t\"check-spelling\" - \"1\", \"check-codespelling\" - \"2\", \"analyze-complexity\" - \"3\", \"analyze-shellscript\" - \"4\", \"stats\" - \"5\".\n" + printf "Valid build targets are: \t\"build-linux\" - \"10\", \"build-windows\" - \"11\", \"build-debian\" - \"12\".\n" + printf "Valid dependency install targets are: \t\"deps-linux\", \"deps-windows\", \"deps-debian\", \"deps-osx\" \"deps-tests\", \"deps-all\".\n\n" +} + +install_dependencies(){ + start_apt update -y -qq > /dev/null || error_handler "There was an error doing apt update." + for dependency in "$@"; do + if [ "$(dpkg-query -W -f='${Status}' "$dependency" 2>/dev/null | grep -c "ok installed")" -eq 0 ]; then + deps+=("$dependency") # Add not installed dependency to the "to be installed array". + else + [[ -n $verbose ]] && 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. + [[ -n $verbose ]] && echo "All dependencies are fulfilled." # Dependency array empty! Not running apt! else - ci_fold(){ - printf -- "-- %s stage %s --\n" "$1" "$2" - } + [[ -z $verbose ]] && echo "Starting dependency installation." + [[ -n $verbose ]] && echo "Then following dependencies are going to be installed:" # Dependency array contains items. Running apt. + [[ -n $verbose ]] && echo "${deps[@]}" | sed 's/ /, /g' + start_apt install -y -qq --no-install-suggests --no-install-recommends "${deps[@]}" > /dev/null || error_handler "There was an error doing dependency installation!" fi + [[ -n $verbose ]] && printf "\n" +} -if [ -z "$OS" ]; then - OS="linux" -fi +start_apt(){ + i="0" + if command -v fuser &> /dev/null; then + while fuser /var/lib/dpkg/lock >/dev/null 2>&1 ; do + [ "$i" -eq "0" ] && printf "\nWaiting for other software managers to finish" + [ "$i" -le "16" ] && printf "." # Print a max of 16 dots if waiting. + ((i=i+1)) + sleep "1s" + done + [ "$i" -ge "1" ] && printf "ready!\n" + fi + DEBIAN_FRONTEND=noninteractive apt-get "$@" +} + +# Build targets and corresponding deployment. + +build-linux(){ + [[ -n $install_deps ]] && install_dependencies "${dep_build_core[@]}" + make full +} + +build-linux-deploy(){ + #printf "\nLinux Deployment started..\n" + mkdir -p ./deployment/linux/backends + mkdir -p ./deployment/linux/docs + cp ./midimonster ./deployment/linux/ + cp ./backends/*.so ./deployment/linux/backends/ + cp ./monster.cfg ./deployment/linux/monster.cfg + cp ./backends/*.md ./deployment/linux/docs/ + cp -r ./configs ./deployment/linux/ + cd ./deployment/linux || error_handler "Error doing cd to ./deployment" + filename="midimonster-$(git describe)-$OS.tgz" + touch "$filename" && tar --exclude=*.tgz -czf "$filename" "./" + find . ! -iname "*.zip" ! -iname "*.tgz" -delete +} + +build-windows(){ + [[ -n $install_deps ]] && install_dependencies "${dep_build_core[@]}" "${dep_build_win[@]}" + make windows +} + +build-windows-deploy(){ + #printf "\nWindows Deployment started..\n" + mkdir -p ./deployment/windows/backends + mkdir -p ./deployment/windows/docs + strip midimonster.exe backends/*.dll # Strip the Windows binaries as they become huge quickly. + cp ./midimonster.exe ./deployment/windows/ + cp ./backends/*.dll ./deployment/windows/backends/ + cp ./backends/*.dll.disabled ./deployment/windows/backends/ + cp ./monster.cfg ./deployment/windows/monster.cfg + cp ./backends/*.md ./deployment/windows/docs/ + cp -r ./configs ./deployment/windows/ + cd ./deployment/windows || error_handler "Error doing cd to ./deployment/windows" + zip -r "./midimonster-$(git describe)-windows.zip" "./" + find . ! -iname "*.zip" ! -iname "*.tgz" -delete +} + +build-debian(){ + [[ -n $install_deps ]] && install_dependencies "${dep_build_core[@]}" "${dep_build_debian[@]}" + git checkout debian/master + gbp buildpackage +} -if [ "$TASK" = "spellcheck" ]; then - result=0 - # Create list of files to be spellchecked - spellcheck_files=$(find . -type f | grep -v ".git/") +build-debian-deploy(){ + #printf "\nDebian Package Deployment started..\n" + mkdir -p ./deployment/debian/ + cp ./*.deb ./deployment/debian/ +} - # Run spellintian to find spelling errors - sl_results=$(xargs spellintian 2>&1 <<< "$spellcheck_files") +# Tests +ckeck-spelling(){ # Check spelling. + [[ -n $install_deps ]] && install_dependencies "lintian" + spellcheck_files=$(find . -type f | grep -v ".git/") # Create list of files to be spellchecked. + sl_results=$(xargs spellintian 2>&1 <<< "$spellcheck_files") # Run spellintian to find spelling errors sl_errors=$(wc -l <<< "$sl_results") - sl_errors_dups=$((grep "\(duplicate word\)" | wc -l) <<< "$sl_results") - sl_errors_nodups=$((grep -v "\(duplicate word\)" | wc -l) <<< "$sl_results") + sl_errors_dups=$( (grep -c "\(duplicate word\)") <<< "$sl_results") + sl_errors_nodups=$( (grep -cv "\(duplicate word\)") <<< "$sl_results") - if [ "$sl_errors" -ne 0 ]; then + if [ "$sl_errors" -gt "1" ]; then printf "Spellintian found %s errors (%s spelling, %s duplicate words):\n\n" "$sl_errors" "$sl_errors_nodups" "$sl_errors_dups" printf "%s\n\n" "$sl_results" - result=1 + exitcode=1 else printf "Spellintian reports no errors\n" fi +} - # Run codespell to find some more +check-codespelling(){ # Check code for common misspellings. + [[ -n $install_deps ]] && install_dependencies "codespell" + spellcheck_files=$(find . -type f | grep -v ".git/") # Create list of files to be spellchecked. cs_results=$(xargs codespell --quiet 2 <<< "$spellcheck_files" 2>&1) cs_errors=$(wc -l <<< "$cs_results") - if [ "$cs_errors" -ne 0 ]; then + if [ "$cs_errors" -gt "1" ]; then printf "Codespell found %s errors:\n\n" "$cs_errors" printf "%s\n\n" "$cs_results" - result=1 + exitcode=1 else printf "Codespell reports no errors\n" fi - exit "$result" -elif [ "$TASK" = "codesmell" ]; then - result=0 +} - if [ -z "$(which lizard)" ]; then +analyze-complexity(){ # code complexity analyser. + [[ -n $install_deps ]] && install_dependencies "python3" "python3-pip" + if [ -z "$(which ~/.local/bin/lizard)" ]; then printf "Installing lizard...\n" - pip3 install lizard + pip3 install lizard >/dev/null fi + printf "Running lizard for code complexity analysis\n" + ~/.local/bin/lizard ./ + if [ "$?" -ne "0" ]; then + exitcode=1 + fi +} - # Run shellcheck for all shell scripts - printf "Running shellcheck...\n" +analyze-shellscript(){ # Shellscript analysis tool. + [[ -n $install_deps ]] && install_dependencies "shellcheck" + printf "Running shellcheck:\n" shell_files="$(find . -type f -iname \*.sh)" xargs shellcheck -Cnever -s bash <<< "$shell_files" if [ "$?" -ne "0" ]; then - result=1 + exitcode=1 fi +} - # Run cloc for some stats - printf "Code statistics:\n\n" +stats(){ # Code statistics. + [[ -n $install_deps ]] && install_dependencies "cloc" + printf "Code statistics:\n" cloc ./ +} - # Run lizard for the project - printf "Running lizard for code complexity analysis\n" - lizard ./ - if [ "$?" -ne "0" ]; then - result=1 - fi +target_queue(){ + printf "\n" + IFS=',|.' read -ra Queue <<< "$TARGETS" + for i in "${Queue[@]}"; do + case "$i" in + check-spelling|1) + ckeck-spelling + ;; + check-codespelling|2) + check-codespelling + ;; + analyze-complexity|3) + analyze-complexity + ;; + analyze-shellscript|4) + analyze-shellscript + ;; + stats|5) + stats + ;; + build-linux|10) + OS="linux" + build-linux + [[ -n $deploy ]] && build-linux-deploy # Deploy build artifacts if the deploy flag is set. + ;; + build-windows|build-win|11) + build-windows + [[ -n $deploy ]] && build-windows-deploy # Deploy build artifacts if the deploy flag is set. + ;; + build-debian|build-deb|12) + build-debian + [[ -n $deploy ]] && build-debian-deploy # Deploy build artifacts if the deploy flag is set. + ;; + build-osx|13) + OS="osx" + printf "\nNot implemented yet!\n" + #build-linux + #[[ -n $deploy ]] && build-linux-deploy # Deploy build artifacts if the deploy flag is set. + ;; + deps-linux) + # Target to install all needed dependencies for linux builds. + install_dependencies "${dep_build_core[@]}" + ;; + deps-windows|deps-win) + # Target to install all needed dependencies for windows builds. + install_dependencies "${dep_build_core[@]}" "${dep_build_win[@]}" + ;; + deps-debian|deps-deb) + # Target to install all needed dependencies for debian packaging. + install_dependencies "${dep_build_core[@]}" "${dep_build_debian[@]}" + ;; + deps-osx) + # Target to install all needed dependencies for osx. + printf "\nNot implemented yet!\n" + ;; + deps-tests) + install_dependencies "lintian" "codespell" "python3" "python3-pip" "shellcheck" "cloc" + # Install lizard if not found. + if [ -z "$(which ~/.local/bin/lizard)" ]; then + pip3 install lizard >/dev/null + fi + ;; + deps-all) + # Target to install all needed dependencies for this ci script. + install_dependencies "${dep_build_core[@]}" "${dep_build_win[@]}" "${dep_build_debian[@]}" "lintian" "codespell" "python3" "python3-pip" "shellcheck" "cloc" + ;; + *) + printf "Target '%s' not valid!\n" "$i" + ;; + esac + printf "\n" + done +} - exit "$result" -elif [ "$TASK" = "sanitize" ]; then - # Run sanitized compile - ci_fold start "make_sanitize" - if ! make sanitize; then - printf "Failed to build\n" - exit 1 - fi - ci_fold end "make_sanitize" -elif [ "$TASK" = "windows" ]; then - ci_fold start "make_windows" - if ! make windows; then - printf "Failed to build\n" - exit 1 - fi - make -C backends lua.dll - ci_fold end "make_windows" - if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then - ci_fold start "deploy_windows" - mkdir ./deployment - mkdir ./deployment/backends - mkdir ./deployment/docs - # Strip the Windows binaries as they become huge quickly - strip midimonster.exe backends/*.dll - cp ./midimonster.exe ./deployment/ - cp ./backends/*.dll ./deployment/backends/ - cp ./backends/*.dll.disabled ./deployment/backends/ - cp ./monster.cfg ./deployment/monster.cfg - cp ./backends/*.md ./deployment/docs/ - cp -r ./configs ./deployment/ - cd ./deployment - zip -r "./midimonster-$(git describe)-windows.zip" "./" - find . ! -iname '*.zip' -delete - ci_fold end "deploy_windows" - fi -else - # Otherwise compile as normal - ci_fold start "make" - if ! make full; then - printf "Failed to build\n" - exit 1 - fi - ci_fold end "make" - if [ "$(git describe)" == "$(git describe --abbrev=0)" ] || [ -n "$DEPLOY" ]; then - ci_fold start "deploy_unix" - mkdir ./deployment - mkdir ./deployment/backends - mkdir ./deployment/docs - cp ./midimonster ./deployment/ - cp ./backends/*.so ./deployment/backends/ - cp ./monster.cfg ./deployment/monster.cfg - cp ./backends/*.md ./deployment/docs/ - cp -r ./configs ./deployment/ - cd ./deployment - tar czf "midimonster-$(git describe)-$OS.tgz" "./" - find . ! -iname '*.tgz' -delete - ci_fold end "deploy_unix" - fi -fi +error_handler(){ + [[ -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" +} + +################################################ Main ################################################# +trap error_handler SIGINT SIGTERM + +ARGS "$@" # Parse arguments. +target_queue # Start requestet targets. + +# Allow failure handler. +[[ -z $allow_failure ]] && exit "$exitcode" +exit "0" \ No newline at end of file -- cgit v1.2.3 From 5bd2e806cf9493681d948615cdeacb0cd3f07524 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Wed, 30 Jun 2021 02:31:46 +0200 Subject: Add CI badge to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 696a46f..f2c9518 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ MIDIMonster Logo [![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168) +[![CI Pipeline Status](https://ci.spacecdn.de/buildStatus/icon?job=midimonster%2Fmaster)](https://ci.spacecdn.de/blue/organizations/jenkins/midimonster/activity) [![IRC Channel](https://static.midimonster.net/hackint-badge.svg)](https://webirc.hackint.org/#irc://irc.hackint.org/#midimonster) Named for its scary math, the MIDIMonster is a universal control and translation -- cgit v1.2.3 From f16f7db86662fcdbf45b6373257c90c824b0b4b0 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 30 Jun 2021 02:53:23 +0200 Subject: Fix CI test step invocation --- assets/ci-config | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/ci-config b/assets/ci-config index 5e8df68..ac94661 100644 --- a/assets/ci-config +++ b/assets/ci-config @@ -60,14 +60,14 @@ builds.Test = { sh label: "Analyze Complexity", script: './assets/ci.sh --target=analyze-complexity' } catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { - sh label: "Analyze Shellscripts", script: './assets/ci.sh--target=analyze-shellscript' + sh label: "Analyze Shellscripts", script: './assets/ci.sh --target=analyze-shellscript' } catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { - sh label: "Code Statistics", script: '../assets/ci.sh --target=stats' + sh label: "Code Statistics", script: './assets/ci.sh --target=stats' } } } } parallel builds -deploy.call() \ No newline at end of file +deploy.call() -- cgit v1.2.3