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 +++++++++++ 3 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 backends/wininput.c create mode 100644 backends/wininput.h (limited to 'backends') 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; -- 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 (limited to 'backends') 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(-) (limited to 'backends') 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 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(-) (limited to 'backends') 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 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(-) (limited to 'backends') 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 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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 --- backends/openpixelcontrol.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') 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 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(-) (limited to 'backends') 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 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 + 3 files changed, 38 insertions(+), 3 deletions(-) (limited to 'backends') 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`). -- 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(+) (limited to 'backends') 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 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 (limited to 'backends') 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 --- backends/Makefile | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'backends') 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 (limited to 'backends') 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(-) (limited to 'backends') 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 --- backends/evdev.c | 1 + 1 file changed, 1 insertion(+) (limited to 'backends') 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(-) (limited to 'backends') 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 ++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) (limited to 'backends') 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. -- 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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 --- backends/artnet.md | 3 +++ backends/sacn.md | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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 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(-) (limited to 'backends') 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 + 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'backends') 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; -- 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(-) (limited to 'backends') 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(+) (limited to 'backends') 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 --- backends/Makefile | 6 +++--- backends/visca.c | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'backends') 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(+) (limited to 'backends') 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 (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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 --- backends/jack.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++- backends/jack.h | 10 ++++++++ backends/jack.md | 7 ++++-- backends/winmidi.c | 2 +- 4 files changed, 87 insertions(+), 4 deletions(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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 (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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 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(-) (limited to 'backends') 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 --- backends/mqtt.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') 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 --- backends/mqtt.md | 4 ++-- backends/osc.c | 3 ++- backends/osc.md | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) (limited to 'backends') 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 -- 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 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') 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 -- 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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(-) (limited to 'backends') 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