From 871bc1568f94ee4026fd64df062572b91d45d462 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 4 Jan 2020 18:39:04 +0100 Subject: Add input_channel call to Lua backend --- backends/lua.c | 22 ++++++++++++++++++++++ backends/lua.md | 6 +++--- 2 files changed, 25 insertions(+), 3 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index ee9e03f..510fc72 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -9,6 +9,7 @@ #endif #define LUA_REGISTRY_KEY "_midimonster_lua_instance" +#define LUA_REGISTRY_CURRENT_CHANNEL "_midimonster_lua_channel" static size_t timers = 0; static lua_timer* timer = NULL; @@ -189,6 +190,7 @@ static int lua_callback_interval(lua_State* interpreter){ if(lua_gettable(interpreter, LUA_REGISTRYINDEX) == LUA_TNUMBER){ //already interval'd reference = luaL_checkinteger(interpreter, 4); + DBGPF("Updating interval to %" PRIu64 " msec", interval); } else if(interval){ //get a reference to the function @@ -199,6 +201,8 @@ static int lua_callback_interval(lua_State* interpreter){ lua_pushvalue(interpreter, 1); lua_pushinteger(interpreter, reference); lua_settable(interpreter, LUA_REGISTRYINDEX); + + DBGPF("Registered interval with %" PRIu64 " msec", interval); } //find matching timer @@ -273,6 +277,12 @@ static int lua_callback_output_value(lua_State* interpreter){ return lua_callback_value(interpreter, 0); } +static int lua_callback_input_channel(lua_State* interpreter){ + lua_pushstring(interpreter, LUA_REGISTRY_CURRENT_CHANNEL); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + return 1; +} + static int lua_configure(char* option, char* value){ LOG("No backend configuration possible"); return 1; @@ -320,6 +330,7 @@ static instance* lua_instance(){ lua_register(data->interpreter, "interval", lua_callback_interval); lua_register(data->interpreter, "input_value", lua_callback_input_value); lua_register(data->interpreter, "output_value", lua_callback_output_value); + lua_register(data->interpreter, "input_channel", lua_callback_input_channel); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -374,6 +385,11 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ data->input[c[n]->ident] = v[n].normalised; //call lua channel handlers if present if(data->reference[c[n]->ident] != LUA_NOREF){ + //push the channel name + lua_pushstring(data->interpreter, LUA_REGISTRY_CURRENT_CHANNEL); + lua_pushstring(data->interpreter, data->channel_name[c[n]->ident]); + lua_settable(data->interpreter, LUA_REGISTRYINDEX); + lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]); lua_pushnumber(data->interpreter, v[n].normalised); if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ @@ -382,6 +398,11 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ } } } + + //clear the channel name + lua_pushstring(data->interpreter, LUA_REGISTRY_CURRENT_CHANNEL); + lua_pushnil(data->interpreter); + lua_settable(data->interpreter, LUA_REGISTRYINDEX); return 0; } @@ -422,6 +443,7 @@ static int lua_handle(size_t num, managed_fd* fds){ timer[n].delta %= timer[n].interval; lua_rawgeti(timer[n].interpreter, LUA_REGISTRYINDEX, timer[n].reference); lua_pcall(timer[n].interpreter, 0, 0, 0); + DBGPF("Calling interval timer function %" PRIsize_t, n); } } } diff --git a/backends/lua.md b/backends/lua.md index f38e189..650fdb9 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -16,7 +16,7 @@ The following functions are provided within the Lua interpreter for interaction | `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms) | | `input_value(string)` | `input_value("foo")` | Get the last input value on a channel | | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | - +| `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | Example script: ``` @@ -58,8 +58,8 @@ lua1.foo > lua2.bar #### Known bugs / problems -Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`) as an -input channel name to a Lua instance will not call any handler functions. +Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`, `input_channel`) +as an input channel name to a Lua instance will not call any handler functions. Using these names as arguments to the output and value interface functions works as intended. Output values will not trigger corresponding input event handlers unless the channel is mapped -- cgit v1.2.3 From d79adcabbfee009c65a6664ca245e17ba6da83bd Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 6 Jan 2020 02:53:36 +0100 Subject: Fix winmidi detect value output --- backends/winmidi.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'backends') diff --git a/backends/winmidi.c b/backends/winmidi.c index 0722ca2..c2aee2f 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -263,7 +263,7 @@ static int winmidi_handle(size_t num, managed_fd* fds){ backend_config.event[u].inst->name, backend_config.event[u].channel.fields.channel, winmidi_type_name(backend_config.event[u].channel.fields.type), - backend_config.event[u].value); + backend_config.event[u].value.normalised); } else{ LOGPF("Incoming data on channel %s.ch%d.%s%d, value %f", @@ -271,7 +271,7 @@ static int winmidi_handle(size_t num, managed_fd* fds){ backend_config.event[u].channel.fields.channel, winmidi_type_name(backend_config.event[u].channel.fields.type), backend_config.event[u].channel.fields.control, - backend_config.event[u].value); + backend_config.event[u].value.normalised); } } chan = mm_channel(backend_config.event[u].inst, backend_config.event[u].channel.label, 0); @@ -396,7 +396,7 @@ static int winmidi_match_input(char* prefix){ for(n = 0; n < inputs; n++){ midiInGetDevCaps(n, &input_caps, sizeof(MIDIINCAPS)); if(!prefix){ - printf("\tID %d: %s", n, input_caps.szPname); + LOGPF("\tID %d: %s", n, input_caps.szPname); } else if(!strncmp(input_caps.szPname, prefix, strlen(prefix))){ LOGPF("Selected input device %s (ID %" PRIsize_t ") for name %s", input_caps.szPname, n, prefix); @@ -429,7 +429,7 @@ static int winmidi_match_output(char* prefix){ for(n = 0; n < outputs; n++){ midiOutGetDevCaps(n, &output_caps, sizeof(MIDIOUTCAPS)); if(!prefix){ - printf("\tID %d: %s", n, output_caps.szPname); + LOGPF("\tID %d: %s", n, output_caps.szPname); } else if(!strncmp(output_caps.szPname, prefix, strlen(prefix))){ LOGPF("Selected output device %s (ID %" PRIsize_t " for name %s", output_caps.szPname, n, prefix); -- cgit v1.2.3 From 78b21a9ac3f975f35ec7b61108531e1495eb91c0 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 12 Jan 2020 17:34:14 +0100 Subject: Rework instance creation --- backends/artnet.c | 14 ++++---------- backends/artnet.h | 2 +- backends/evdev.c | 13 ++++--------- backends/evdev.h | 2 +- backends/jack.c | 12 +++--------- backends/jack.h | 2 +- backends/loopback.c | 15 +++++---------- backends/loopback.h | 2 +- backends/lua.c | 13 ++++--------- backends/lua.h | 2 +- backends/maweb.c | 13 ++++--------- backends/maweb.h | 2 +- backends/midi.c | 11 +++-------- backends/midi.h | 2 +- backends/ola.cpp | 16 +++++----------- backends/ola.h | 2 +- backends/osc.c | 11 +++-------- backends/osc.h | 2 +- backends/sacn.c | 11 +++-------- backends/sacn.h | 2 +- backends/winmidi.c | 15 +++++---------- backends/winmidi.h | 2 +- 22 files changed, 54 insertions(+), 112 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 0bd1a32..ed426b0 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -94,23 +94,17 @@ static int artnet_configure(char* option, char* value){ return 1; } -static instance* artnet_instance(){ - artnet_instance_data* data = NULL; - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - - data = calloc(1, sizeof(artnet_instance_data)); +static int artnet_instance(instance* inst){ + artnet_instance_data* data = calloc(1, sizeof(artnet_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } data->net = default_net; inst->impl = data; - return inst; + return 0; } static int artnet_configure_instance(instance* inst, char* option, char* value){ diff --git a/backends/artnet.h b/backends/artnet.h index 59bd53f..1efdee6 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -6,7 +6,7 @@ MM_PLUGIN_API int init(); static int artnet_configure(char* option, char* value); static int artnet_configure_instance(instance* instance, char* option, char* value); -static instance* artnet_instance(); +static int artnet_instance(instance* inst); static channel* artnet_channel(instance* instance, char* spec, uint8_t flags); static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v); static int artnet_handle(size_t num, managed_fd* fds); diff --git a/backends/evdev.c b/backends/evdev.c index 4725ef7..af5ec74 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -63,16 +63,11 @@ static int evdev_configure(char* option, char* value) { return 1; } -static instance* evdev_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int evdev_instance(instance* inst){ evdev_instance_data* data = calloc(1, sizeof(evdev_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } data->input_fd = -1; @@ -81,12 +76,12 @@ static instance* evdev_instance(){ if(!data->output_proto){ LOG("Failed to initialize libevdev output prototype device"); free(data); - return NULL; + return 1; } #endif inst->impl = data; - return inst; + return 0; } static int evdev_attach(instance* inst, evdev_instance_data* data, char* node){ diff --git a/backends/evdev.h b/backends/evdev.h index 0c877fc..e896d2d 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -11,7 +11,7 @@ MM_PLUGIN_API int init(); static int evdev_configure(char* option, char* value); static int evdev_configure_instance(instance* instance, char* option, char* value); -static instance* evdev_instance(); +static int evdev_instance(instance* inst); static channel* evdev_channel(instance* instance, char* spec, uint8_t flags); static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v); static int evdev_handle(size_t num, managed_fd* fds); diff --git a/backends/jack.c b/backends/jack.c index d7f68c4..c862096 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -334,19 +334,13 @@ static int mmjack_configure_instance(instance* inst, char* option, char* value){ return 0; } -static instance* mmjack_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int mmjack_instance(instance* inst){ inst->impl = calloc(1, sizeof(mmjack_instance_data)); if(!inst->impl){ LOG("Failed to allocate memory"); - return NULL; + return 1; } - - return inst; + return 0; } static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){ diff --git a/backends/jack.h b/backends/jack.h index 66c66db..03ce052 100644 --- a/backends/jack.h +++ b/backends/jack.h @@ -5,7 +5,7 @@ MM_PLUGIN_API int init(); static int mmjack_configure(char* option, char* value); static int mmjack_configure_instance(instance* inst, char* option, char* value); -static instance* mmjack_instance(); +static int mmjack_instance(instance* inst); static channel* mmjack_channel(instance* inst, char* spec, uint8_t flags); static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v); static int mmjack_handle(size_t num, managed_fd* fds); diff --git a/backends/loopback.c b/backends/loopback.c index 085d1df..eaecdb4 100644 --- a/backends/loopback.c +++ b/backends/loopback.c @@ -34,19 +34,14 @@ static int loopback_configure_instance(instance* inst, char* option, char* value return 0; } -static instance* loopback_instance(){ - instance* i = mm_instance(); - if(!i){ - return NULL; - } - - i->impl = calloc(1, sizeof(loopback_instance_data)); - if(!i->impl){ +static int loopback_instance(instance* inst){ + inst->impl = calloc(1, sizeof(loopback_instance_data)); + if(!inst->impl){ LOG("Failed to allocate memory"); - return NULL; + return 1; } - return i; + return 0; } static channel* loopback_channel(instance* inst, char* spec, uint8_t flags){ diff --git a/backends/loopback.h b/backends/loopback.h index c508d72..cfb2e19 100644 --- a/backends/loopback.h +++ b/backends/loopback.h @@ -3,7 +3,7 @@ MM_PLUGIN_API int init(); static int loopback_configure(char* option, char* value); static int loopback_configure_instance(instance* inst, char* option, char* value); -static instance* loopback_instance(); +static int loopback_instance(instance* inst); static channel* loopback_channel(instance* inst, char* spec, uint8_t flags); static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v); static int loopback_handle(size_t num, managed_fd* fds); diff --git a/backends/lua.c b/backends/lua.c index 510fc72..7d20fb1 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -304,16 +304,11 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ return 1; } -static instance* lua_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int lua_instance(instance* inst){ lua_instance_data* data = calloc(1, sizeof(lua_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } //load the interpreter @@ -321,7 +316,7 @@ static instance* lua_instance(){ if(!data->interpreter){ LOG("Failed to initialize interpreter"); free(data); - return NULL; + return 1; } luaL_openlibs(data->interpreter); @@ -338,7 +333,7 @@ static instance* lua_instance(){ lua_settable(data->interpreter, LUA_REGISTRYINDEX); inst->impl = data; - return inst; + return 0; } static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ diff --git a/backends/lua.h b/backends/lua.h index 75f03c4..ebe2046 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -12,7 +12,7 @@ MM_PLUGIN_API int init(); static int lua_configure(char* option, char* value); static int lua_configure_instance(instance* inst, char* option, char* value); -static instance* lua_instance(); +static int lua_instance(instance* inst); static channel* lua_channel(instance* inst, char* spec, uint8_t flags); static int lua_set(instance* inst, size_t num, channel** c, channel_value* v); static int lua_handle(size_t num, managed_fd* fds); diff --git a/backends/maweb.c b/backends/maweb.c index f81ab46..6a006bd 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -205,16 +205,11 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){ return 1; } -static instance* maweb_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int maweb_instance(instance* inst){ maweb_instance_data* data = calloc(1, sizeof(maweb_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } data->fd = -1; @@ -222,12 +217,12 @@ static instance* maweb_instance(){ if(!data->buffer){ LOG("Failed to allocate memory"); free(data); - return NULL; + return 1; } data->allocated = MAWEB_RECV_CHUNK; inst->impl = data; - return inst; + return 0; } static channel* maweb_channel(instance* inst, char* spec, uint8_t flags){ diff --git a/backends/maweb.h b/backends/maweb.h index 50b777a..80835d9 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -3,7 +3,7 @@ MM_PLUGIN_API int init(); static int maweb_configure(char* option, char* value); static int maweb_configure_instance(instance* inst, char* option, char* value); -static instance* maweb_instance(); +static int maweb_instance(instance* inst); static channel* maweb_channel(instance* inst, char* spec, uint8_t flags); static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v); static int maweb_handle(size_t num, managed_fd* fds); diff --git a/backends/midi.c b/backends/midi.c index 11d759d..f73ebb4 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -69,19 +69,14 @@ static int midi_configure(char* option, char* value){ return 1; } -static instance* midi_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int midi_instance(instance* inst){ inst->impl = calloc(1, sizeof(midi_instance_data)); if(!inst->impl){ LOG("Failed to allocate memory"); - return NULL; + return 1; } - return inst; + return 0; } static int midi_configure_instance(instance* inst, char* option, char* value){ diff --git a/backends/midi.h b/backends/midi.h index 66a02bc..dcee010 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -3,7 +3,7 @@ MM_PLUGIN_API int init(); static int midi_configure(char* option, char* value); static int midi_configure_instance(instance* instance, char* option, char* value); -static instance* midi_instance(); +static int midi_instance(instance* inst); static channel* midi_channel(instance* instance, char* spec, uint8_t flags); static int midi_set(instance* inst, size_t num, channel** c, channel_value* v); static int midi_handle(size_t num, managed_fd* fds); diff --git a/backends/ola.cpp b/backends/ola.cpp index 09d68c9..106dbd5 100644 --- a/backends/ola.cpp +++ b/backends/ola.cpp @@ -40,21 +40,15 @@ static int ola_configure(char* option, char* value){ return 1; } -static instance* ola_instance(){ - ola_instance_data* data = NULL; - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - - data = (ola_instance_data*)calloc(1, sizeof(ola_instance_data)); +static int ola_instance(instance* inst){ + ola_instance_data* data = (ola_instance_data*) calloc(1, sizeof(ola_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } inst->impl = data; - return inst; + return 0; } static int ola_configure_instance(instance* inst, char* option, char* value){ @@ -188,7 +182,7 @@ static void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dm else{ chan = mm_channel(inst, p, 0); } - + if(!chan){ LOGPF("Active channel %" PRIsize_t " on %s not known to core", p, inst->name); return; diff --git a/backends/ola.h b/backends/ola.h index 083e971..68244ec 100644 --- a/backends/ola.h +++ b/backends/ola.h @@ -7,7 +7,7 @@ extern "C" { MM_PLUGIN_API int init(); static int ola_configure(char* option, char* value); static int ola_configure_instance(instance* instance, char* option, char* value); - static instance* ola_instance(); + static int ola_instance(instance* inst); static channel* ola_channel(instance* instance, char* spec, uint8_t flags); static int ola_set(instance* inst, size_t num, channel** c, channel_value* v); static int ola_handle(size_t num, managed_fd* fds); diff --git a/backends/osc.c b/backends/osc.c index 7b9a5a5..754c290 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -562,21 +562,16 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ return 1; } -static instance* osc_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int osc_instance(instance* inst){ osc_instance_data* data = calloc(1, sizeof(osc_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } data->fd = -1; inst->impl = data; - return inst; + return 0; } static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags){ diff --git a/backends/osc.h b/backends/osc.h index f8ff3ff..ec75e3f 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -10,7 +10,7 @@ MM_PLUGIN_API int init(); static int osc_configure(char* option, char* value); static int osc_configure_instance(instance* inst, char* option, char* value); -static instance* osc_instance(); +static int osc_instance(instance* inst); static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags); static int osc_set(instance* inst, size_t num, channel** c, channel_value* v); static int osc_handle(size_t num, managed_fd* fds); diff --git a/backends/sacn.c b/backends/sacn.c index ff2b61e..5096123 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -195,19 +195,14 @@ static int sacn_configure_instance(instance* inst, char* option, char* value){ return 1; } -static instance* sacn_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int sacn_instance(instance* inst){ inst->impl = calloc(1, sizeof(sacn_instance_data)); if(!inst->impl){ LOG("Failed to allocate memory"); - return NULL; + return 1; } - return inst; + return 0; } static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ diff --git a/backends/sacn.h b/backends/sacn.h index c8d11e9..ac59441 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -3,7 +3,7 @@ MM_PLUGIN_API int init(); static int sacn_configure(char* option, char* value); static int sacn_configure_instance(instance* instance, char* option, char* value); -static instance* sacn_instance(); +static int sacn_instance(instance* inst); static channel* sacn_channel(instance* instance, char* spec, uint8_t flags); static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v); static int sacn_handle(size_t num, managed_fd* fds); diff --git a/backends/winmidi.c b/backends/winmidi.c index c2aee2f..ad9b02d 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -95,19 +95,14 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value) return 1; } -static instance* winmidi_instance(){ - instance* i = mm_instance(); - if(!i){ - return NULL; - } - - i->impl = calloc(1, sizeof(winmidi_instance_data)); - if(!i->impl){ +static int winmidi_instance(instance* inst){ + inst->impl = calloc(1, sizeof(winmidi_instance_data)); + if(!inst->impl){ LOG("Failed to allocate memory"); - return NULL; + return 1; } - return i; + return 0; } static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ diff --git a/backends/winmidi.h b/backends/winmidi.h index 81e7439..4c740ea 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -3,7 +3,7 @@ MM_PLUGIN_API int init(); static int winmidi_configure(char* option, char* value); static int winmidi_configure_instance(instance* inst, char* option, char* value); -static instance* winmidi_instance(); +static int winmidi_instance(instance* inst); static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags); static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int winmidi_handle(size_t num, managed_fd* fds); -- cgit v1.2.3 From 588d5445fcedcd3b2c33abeb3d4262b2540c670c Mon Sep 17 00:00:00 2001 From: Spacelord Date: Wed, 29 Jan 2020 19:56:40 +0100 Subject: Minor fix in maweb backend documentation --- backends/maweb.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/maweb.md b/backends/maweb.md index 45dc778..8ce65ef 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -1,7 +1,7 @@ ### The `maweb` backend This backend connects directly with the integrated *MA Web Remote* of MA Lighting consoles and OnPC -instances (GrandMA2 / GrandMA2 OnPC / GrandMA Dot2 / GrandMA Dot2 OnPC). +instances (GrandMA2 / GrandMA2 OnPC / Dot2 / Dot2 OnPC). It grants read-write access to the console's playback controls as well as write access to most command line and control keys. @@ -32,7 +32,7 @@ Web Remote. Set a web remote password using the option below the activation sett The per-instance command line mode may be one of `remote`, `console` or `downgrade`. The first option handles command keys with a "virtual" commandline belonging to the Web Remote connection. Any commands entered are not visible on the main console. The `console` mode is only available with GrandMA2 remotes and injects key events -into the main console. This mode also supports additional hardkeys that are only available on GrandMA consoles. +into the main console. This mode also supports additional hardkeys that are only available on GrandMA2 consoles. When connected to a dot2 console while this mode is active, the use of commandline keys will not be possible. With the `downgrade` mode, keys are handled on the console if possible, falling back to remote handling if not. -- cgit v1.2.3 From 8ebc4311185e3329622ad883dde54409fa1c8350 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 9 Feb 2020 11:50:41 +0100 Subject: Mention release page in installer section, move images around --- backends/lua.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/lua.md b/backends/lua.md index 650fdb9..d01a8c6 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -3,7 +3,7 @@ The `lua` backend provides a flexible programming environment, allowing users to route and manipulate events using the Lua programming language. -Every instance has it's own interpreter state which can be loaded with custom handler scripts. +Every instance has its own interpreter state which can be loaded with custom handler 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. -- cgit v1.2.3 From dd91621ccee033550312683293b5bf40c3599053 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 23 Feb 2020 18:20:12 +0100 Subject: Implement OpenPixelControl output --- backends/Makefile | 11 +- backends/openpixelcontrol.c | 353 +++++++++++++++++++++++++++++++++++++++++++ backends/openpixelcontrol.h | 48 ++++++ backends/openpixelcontrol.md | 49 ++++++ 4 files changed, 459 insertions(+), 2 deletions(-) create mode 100644 backends/openpixelcontrol.c create mode 100644 backends/openpixelcontrol.h create mode 100644 backends/openpixelcontrol.md (limited to 'backends') diff --git a/backends/Makefile b/backends/Makefile index 656e6b6..191a495 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full LINUX_BACKENDS = midi.so evdev.so -WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o @@ -36,6 +36,10 @@ sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: LDLIBS += -lws2_32 +openpixelcontrol.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +openpixelcontrol.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +openpixelcontrol.dll: LDLIBS += -lws2_32 + maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB) maweb.so: LDLIBS = -lssl maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) @@ -59,6 +63,9 @@ lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 || lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") lua.dll: LDLIBS += -L../libs -llua53 +python.so: CFLAGS += $(shell pkg-config --cflags python3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +python.so: CFLAGS += $(shell pkg-config --libs python3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") + %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c new file mode 100644 index 0000000..062c015 --- /dev/null +++ b/backends/openpixelcontrol.c @@ -0,0 +1,353 @@ +#define BACKEND_NAME "openpixelcontrol" + +#include + +#include "libmmbackend.h" +#include "openpixelcontrol.h" + +/* + * TODO handle destination close/unregister/reopen + */ + +MM_PLUGIN_API int init(){ + backend openpixel = { + .name = BACKEND_NAME, + .conf = openpixel_configure, + .create = openpixel_instance, + .conf_instance = openpixel_configure_instance, + .channel = openpixel_channel, + .handle = openpixel_set, + .process = openpixel_handle, + .start = openpixel_start, + .shutdown = openpixel_shutdown + }; + + //register backend + if(mm_backend_register(openpixel)){ + LOG("Failed to register backend"); + return 1; + } + return 0; +} + +static int openpixel_configure(char* option, char* value){ + //no global configuration + LOG("No backend configuration possible"); + return 1; +} + +static int openpixel_configure_instance(instance* inst, char* option, char* value){ + char* host = NULL, *port = NULL; + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + + //FIXME this should store the destination/listen address and establish on _start + if(!strcmp(option, "destination")){ + mmbackend_parse_hostspec(value, &host, &port, NULL); + if(!host || !port){ + LOGPF("Invalid destination address specified for instance %s", inst->name); + return 1; + } + + data->dest_fd = mmbackend_socket(host, port, SOCK_STREAM, 0, 0); + if(data->dest_fd >= 0){ + return 0; + } + return 1; + } + if(!strcmp(option, "listen")){ + mmbackend_parse_hostspec(value, &host, &port, NULL); + if(!host || !port){ + LOGPF("Invalid listen address specified for instance %s", inst->name); + return 1; + } + + data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0); + if(data->listen_fd >= 0 && listen(data->listen_fd, SOMAXCONN)){ + return 0; + } + return 1; + } + else if(!strcmp(option, "mode")){ + if(!strcmp(value, "16bit")){ + data->mode = rgb16; + return 0; + } + else if(!strcmp(value, "8bit")){ + data->mode = rgb8; + return 0; + } + LOGPF("Unknown instance mode %s\n", value); + return 1; + } + + LOGPF("Unknown instance option %s for instance %s", option, inst->name); + return 1; +} + +static int openpixel_instance(instance* inst){ + openpixel_instance_data* data = calloc(1, sizeof(openpixel_instance_data)); + inst->impl = data; + if(!inst->impl){ + LOG("Failed to allocate memory"); + return 1; + } + + data->dest_fd = -1; + data->listen_fd = -1; + return 0; +} + +static ssize_t openpixel_buffer_find(openpixel_instance_data* data, uint8_t strip, uint8_t input){ + ssize_t n = 0; + + for(n = 0; n < data->buffers; n++){ + if(data->buffer[n].strip == strip + && (data->buffer[n].flags & OPENPIXEL_INPUT) >= input){ + return n; + } + } + return -1; +} + +static int openpixel_buffer_extend(openpixel_instance_data* data, uint8_t strip, uint8_t input, uint8_t length){ + ssize_t buffer = openpixel_buffer_find(data, strip, input); + length = (length % 3) ? ((length / 3) + 1) * 3 : length; + size_t bytes_required = (data->mode == rgb8) ? length : length * 2; + if(buffer < 0){ + //allocate new buffer + data->buffer = realloc(data->buffer, (data->buffers + 1) * sizeof(openpixel_buffer)); + if(!data->buffer){ + data->buffers = 0; + LOG("Failed to allocate memory"); + return -1; + } + + buffer = data->buffers; + data->buffers++; + + data->buffer[buffer].strip = strip; + data->buffer[buffer].flags = input ? OPENPIXEL_INPUT : 0; + data->buffer[buffer].bytes = 0; + data->buffer[buffer].data.u8 = NULL; + } + + if(data->buffer[buffer].bytes < bytes_required){ + //resize buffer + data->buffer[buffer].data.u8 = realloc(data->buffer[buffer].data.u8, bytes_required); + if(!data->buffer[buffer].data.u8){ + data->buffer[buffer].bytes = 0; + LOG("Failed to allocate memory"); + return 1; + } + //FIXME might want to memset() only newly allocated channels + memset(data->buffer[buffer].data.u8, 0, bytes_required); + data->buffer[buffer].bytes = bytes_required; + } + return 0; +} + +static channel* openpixel_channel(instance* inst, char* spec, uint8_t flags){ + uint32_t strip = 0, channel = 0; + char* token = spec; + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + + //read strip index if supplied + if(!strncmp(spec, "strip", 5)){ + strip = strtoul(spec + 5, &token, 10); + //skip the dot + token++; + } + + //read (and calculate) channel index + if(!strncmp(token, "channel", 7)){ + channel = strtoul(token + 7, NULL, 10); + } + else if(!strncmp(token, "red", 3)){ + channel = strtoul(token + 3, NULL, 10) * 3 - 2; + } + else if(!strncmp(token, "green", 5)){ + channel = strtoul(token + 5, NULL, 10) * 3 - 1; + } + else if(!strncmp(token, "blue", 4)){ + channel = strtoul(token + 4, NULL, 10) * 3; + } + + if(!channel){ + LOGPF("Invalid channel specification %s", spec); + return NULL; + } + + //check channel direction + if(flags & mmchannel_input){ + //strip 0 (bcast) can not be mapped as input + if(!strip){ + LOGPF("Broadcast channel %s.%s can not be mapped as an input", inst->name, spec); + return NULL; + } + if(data->listen_fd < 0){ + LOGPF("Channel %s mapped as input, but instance %s is not accepting input", spec, inst->name); + return NULL; + } + + if(openpixel_buffer_extend(data, strip, 1, channel)){ + return NULL; + } + } + + if(flags & mmchannel_output){ + if(data->dest_fd < 0){ + LOGPF("Channel %s mapped as output, but instance %s is not sending output", spec, inst->name); + return NULL; + } + + if(openpixel_buffer_extend(data, strip, 0, channel)){ + return NULL; + } + } + + return mm_channel(inst, ((uint64_t) strip) << 32 | channel, 1); +} + +static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* v){ + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + size_t u, p; + ssize_t buffer; + uint32_t strip, channel; + openpixel_header hdr; + + for(u = 0; u < num; u++){ + //read strip/channel + strip = c[u]->ident >> 32; + channel = c[u]->ident & 0xFFFFFFFF; + channel--; + + //find the buffer + buffer = openpixel_buffer_find(data, strip, 0); + if(buffer < 0){ + LOGPF("No buffer for channel %s.%d.%d\n", inst->name, strip, channel); + continue; + } + + //mark buffer for output + data->buffer[buffer].flags |= OPENPIXEL_MARK; + + //update data + switch(data->mode){ + case rgb8: + data->buffer[buffer].data.u8[channel] = ((uint8_t)(v[u].normalised * 255.0)); + break; + case rgb16: + data->buffer[buffer].data.u16[channel] = ((uint16_t)(v[u].normalised * 65535.0)); + break; + } + + if(strip == 0){ + //update values in all other output strips, dont mark + for(p = 0; p < data->buffers; p++){ + if(!(data->buffer[p].flags & OPENPIXEL_INPUT)){ + //check whether the buffer is large enough + if(data->mode == rgb8 && data->buffer[p].bytes >= channel){ + data->buffer[p].data.u8[channel] = ((uint8_t)(v[u].normalised * 255.0)); + } + else if(data->mode == rgb16 && data->buffer[p].bytes >= channel * 2){ + data->buffer[p].data.u16[channel] = ((uint16_t)(v[u].normalised * 65535.0)); + } + } + } + } + } + + //send updated strips + for(u = 0; u < data->buffers; u++){ + if(!(data->buffer[u].flags & OPENPIXEL_INPUT) && (data->buffer[u].flags & OPENPIXEL_MARK)){ + //remove mark + data->buffer[u].flags &= ~OPENPIXEL_MARK; + + //prepare header + hdr.strip = data->buffer[u].strip; + hdr.mode = data->mode; + hdr.length = htobe16(data->buffer[u].bytes); + + //output data + if(mmbackend_send(data->dest_fd, (uint8_t*) &hdr, sizeof(hdr)) + || mmbackend_send(data->dest_fd, data->buffer[u].data.u8, data->buffer[u].bytes)){ + return 1; + } + } + } + return 0; +} + +static int openpixel_handle(size_t num, managed_fd* fds){ + //TODO handle bcast + return 0; +} + +static int openpixel_start(size_t n, instance** inst){ + int rv = -1; + size_t u, nfds = 0; + openpixel_instance_data* data = NULL; + + for(u = 0; u < n; u++){ + data = (openpixel_instance_data*) inst[u]->impl; + + //register fds + if(data->dest_fd >= 0){ + if(mm_manage_fd(data->dest_fd, BACKEND_NAME, 1, inst[u])){ + LOGPF("Failed to register destination descriptor for instance %s with core", inst[u]->name); + goto bail; + } + nfds++; + } + if(data->listen_fd >= 0){ + if(mm_manage_fd(data->listen_fd, BACKEND_NAME, 1, inst[u])){ + LOGPF("Failed to register host descriptor for instance %s with core", inst[u]->name); + goto bail; + } + nfds++; + } + } + + LOGPF("Registered %" PRIsize_t " descriptors to core", nfds); + rv = 0; +bail: + return rv; +} + +static int openpixel_shutdown(size_t n, instance** inst){ + size_t u, p; + openpixel_instance_data* data = NULL; + + for(u = 0; u < n; u++){ + data = (openpixel_instance_data*) inst[u]->impl; + + //shutdown all clients + for(p = 0; p < data->clients; p++){ + if(data->client_fd[p] >= 0){ + close(data->client_fd[p]); + } + } + free(data->client_fd); + free(data->bytes_left); + + //close all configured fds + if(data->listen_fd >= 0){ + close(data->listen_fd); + } + if(data->dest_fd >= 0){ + close(data->dest_fd); + } + + //free all buffers + for(p = 0; p < data->buffers; p++){ + free(data->buffer[p].data.u8); + } + free(data->buffer); + + free(data); + inst[u]->impl = NULL; + } + + LOG("Backend shut down"); + return 0; +} diff --git a/backends/openpixelcontrol.h b/backends/openpixelcontrol.h new file mode 100644 index 0000000..f1061ea --- /dev/null +++ b/backends/openpixelcontrol.h @@ -0,0 +1,48 @@ +#include "midimonster.h" + +MM_PLUGIN_API int init(); +static int openpixel_configure(char* option, char* value); +static int openpixel_configure_instance(instance* inst, char* option, char* value); +static int openpixel_instance(instance* inst); +static channel* openpixel_channel(instance* inst, char* spec, uint8_t flags); +static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* v); +static int openpixel_handle(size_t num, managed_fd* fds); +static int openpixel_start(size_t n, instance** inst); +static int openpixel_shutdown(size_t n, instance** inst); + +#define OPENPIXEL_INPUT 1 +#define OPENPIXEL_MARK 2 + +typedef struct /*_data_buffer*/ { + uint8_t strip; + uint8_t flags; + uint16_t bytes; + union { + uint16_t* u16; + uint8_t* u8; + } data; +} openpixel_buffer; + +#pragma pack(push, 1) +typedef struct /*_openpixel_hdr*/ { + uint8_t strip; + uint8_t mode; + uint16_t length; +} openpixel_header; +#pragma pack(pop) + +typedef struct { + enum { + rgb8 = 0, + rgb16 = 2 + } mode; + + size_t buffers; + openpixel_buffer* buffer; + + int dest_fd; + int listen_fd; + size_t clients; + int* client_fd; + size_t* bytes_left; +} openpixel_instance_data; diff --git a/backends/openpixelcontrol.md b/backends/openpixelcontrol.md new file mode 100644 index 0000000..ce60278 --- /dev/null +++ b/backends/openpixelcontrol.md @@ -0,0 +1,49 @@ +### The `openpixelcontrol` backend + +This backend provides read-write access to the TCP-based OpenPixelControl protocol, +used for controlling intelligent RGB led strips. + +This backend can both control a remote OpenPixelControl server as well as receive data +from OpenPixelControl clients. + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `destination` | `10.11.12.1 9001` | none | Destination for output data. Setting this option enables the instance for output | +| `listen` | `10.11.12.2 9002` | none | Local address to wait for client connections on. Setting this enables the instance for input | +| `mode` | `16bit` | `8bit` | RGB channel resolution | + +#### Channel specification + +Each instance can control up to 255 strips of RGB LED lights. The OpenPixelControl specification +confusingly calls these strips "channels". + +Strip `0` acts as a "broadcast" strip, setting values on all other strips at once. +Consequently, components on strip 0 can only be mapped as output channels to a destination +(setting components on all strips there), not as input channels. When such messages are received from +a client, the corresponding mapped component channels on all strips will receive events. + +Every single component of any LED on any string can be mapped as an individual MIDIMonster channel. +The components are laid out as sequences of Red - Green - Blue value triplets. + +Channels can be specified by their sequential index (one-based). + +Example mapping (data from Strip 2 LED 66's green component is mapped to the blue component of LED 2 on strip 1): +``` +strip1.channel6 < strip2.channel200 +``` + +Additionally, channels may be referred to by their color component and LED index: +``` +strip1.blue2 < strip2.green66 +``` + +#### Known bugs / problems + +If the connection is lost, it is currently not reestablished and may cause exit the MIDIMonster entirely. +Thisi behaviour may be changed in future releases. -- cgit v1.2.3 From 0a59a6bd47597d4577c0a550779d2e352127d186 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 25 Feb 2020 23:14:55 +0100 Subject: Implement quiet mode for maweb backend --- backends/maweb.c | 17 +++++++++++++++-- backends/maweb.md | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 6a006bd..6861d75 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -15,10 +15,15 @@ #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 last_update = 0; static uint64_t updates_inflight = 0; +static uint64_t quiet_mode = 0; static maweb_command_key cmdline_keys[] = { {"PREV", 109, 0, 1}, {"SET", 108, 1, 0, 1}, {"NEXT", 110, 0, 1}, @@ -139,6 +144,10 @@ static int maweb_configure(char* option, char* value){ update_interval = strtoul(value, NULL, 10); return 0; } + else if(!strcmp(option, "quiet")){ + quiet_mode = strtoul(value, NULL, 10); + return 0; + } LOGPF("Unknown backend configuration option %s", option); return 1; @@ -457,7 +466,9 @@ static int maweb_request_playbacks(instance* inst){ size_t page_index = 0, view = 3, channel = 0, offsets[3], channel_offset, channels; if(updates_inflight){ - LOGPF("Skipping update request, %" PRIu64 " updates still inflight", updates_inflight); + if(quiet_mode < 1){ + LOGPF("Skipping update request, %" PRIu64 " updates still inflight - consider raising the interval time", updates_inflight); + } return 0; } @@ -588,7 +599,9 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le data->login = 0; return 0; } - LOGPF("Session id is now %" PRId64, data->session); + if(quiet_mode < 2){ + LOGPF("Session id is now %" PRId64, data->session); + } } if(json_obj_bool(payload, "forceLogin", 0)){ diff --git a/backends/maweb.md b/backends/maweb.md index 8ce65ef..eddf1a5 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -19,6 +19,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) | +| `quiet` | `1` | `0` | Turn off some warning messages, for use by experts | #### Instance configuration -- cgit v1.2.3 From 1b3878956f02e274c480815774f9c6f39d65117f Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 26 Feb 2020 00:04:55 +0100 Subject: Use (spell-)lintian from package repositories in CI --- backends/loopback.c | 1 + 1 file changed, 1 insertion(+) (limited to 'backends') diff --git a/backends/loopback.c b/backends/loopback.c index eaecdb4..4274832 100644 --- a/backends/loopback.c +++ b/backends/loopback.c @@ -102,6 +102,7 @@ static int loopback_shutdown(size_t n, instance** inst){ } free(data->name); free(inst[u]->impl); + inst[u]->impl = NULL; } LOG("Backend shut down"); -- cgit v1.2.3 From e1fcd4d11cfdbad54470b2cce98d8b749464ec00 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 27 Feb 2020 00:33:22 +0100 Subject: Implement OpenPixelControl server mode (8bit) --- backends/libmmbackend.c | 4 + backends/openpixelcontrol.c | 220 +++++++++++++++++++++++++++++++++++++++++-- backends/openpixelcontrol.h | 15 ++- backends/openpixelcontrol.md | 9 +- 4 files changed, 236 insertions(+), 12 deletions(-) (limited to 'backends') diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index ffa403b..b9513ac 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -153,7 +153,11 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin int mmbackend_send(int fd, uint8_t* data, size_t length){ ssize_t total = 0, sent; while(total < length){ + #ifndef LIBMMBACKEND_TCP_TORTURE sent = send(fd, data + total, length - total, 0); + #else + sent = send(fd, data + total, 1, 0); + #endif if(sent < 0){ LOGPF("Failed to send: %s", strerror(errno)); return 1; diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 062c015..3a94a12 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -52,6 +52,7 @@ static int openpixel_configure_instance(instance* inst, char* option, char* valu if(data->dest_fd >= 0){ return 0; } + LOGPF("Failed to connect to server for instance %s", inst->name); return 1; } if(!strcmp(option, "listen")){ @@ -62,9 +63,10 @@ static int openpixel_configure_instance(instance* inst, char* option, char* valu } data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0); - if(data->listen_fd >= 0 && listen(data->listen_fd, SOMAXCONN)){ + if(data->listen_fd >= 0 && !listen(data->listen_fd, SOMAXCONN)){ return 0; } + LOGPF("Failed to bind server descriptor for instance %s", inst->name); return 1; } else if(!strcmp(option, "mode")){ @@ -103,15 +105,22 @@ static ssize_t openpixel_buffer_find(openpixel_instance_data* data, uint8_t stri for(n = 0; n < data->buffers; n++){ if(data->buffer[n].strip == strip && (data->buffer[n].flags & OPENPIXEL_INPUT) >= input){ + DBGPF("Using allocated %s buffer for requested strip %d, size %d", input ? "input" : "output", strip, data->buffer[n].bytes); return n; } } + DBGPF("Instance has no %s buffer for requested strip %d", input ? "input" : "output", strip); return -1; } -static int openpixel_buffer_extend(openpixel_instance_data* data, uint8_t strip, uint8_t input, uint8_t length){ +static int openpixel_buffer_extend(openpixel_instance_data* data, uint8_t strip, uint8_t input, uint16_t length){ ssize_t buffer = openpixel_buffer_find(data, strip, input); + + //length is in component-channels, round it to the nearest rgb-triplet + //this guarantees that any allocated buffer has at least three bytes, which is important to parts of the receive handler length = (length % 3) ? ((length / 3) + 1) * 3 : length; + + //calculate required buffer length size_t bytes_required = (data->mode == rgb8) ? length : length * 2; if(buffer < 0){ //allocate new buffer @@ -242,7 +251,7 @@ static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* } if(strip == 0){ - //update values in all other output strips, dont mark + //update values in all other output strips, don't mark for(p = 0; p < data->buffers; p++){ if(!(data->buffer[p].flags & OPENPIXEL_INPUT)){ //check whether the buffer is large enough @@ -278,8 +287,204 @@ static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* return 0; } +static int openpixel_client_new(instance* inst, int fd){ + if(fd < 0){ + return 1; + } + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + size_t u; + + //mark nonblocking + #ifdef _WIN32 + unsigned long flags = 1; + if(ioctlsocket(fd, FIONBIO, &flags)){ + #else + int flags = fcntl(fd, F_GETFL, 0); + if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ + #endif + LOGPF("Failed to set client descriptor on %s nonblocking", inst->name); + close(fd); + return 0; + } + + //find a client block + for(u = 0; u < data->clients; u++){ + if(data->client[u].fd <= 0){ + break; + } + } + + //if no free slot, make one + if(u == data->clients){ + data->client = realloc(data->client, (data->clients + 1) * sizeof(openpixel_client)); + if(!data->client){ + data->clients = 0; + LOG("Failed to allocate memory"); + return 1; + } + data->clients++; + } + + data->client[u].fd = fd; + data->client[u].buffer = -1; + data->client[u].offset = 0; + + return mm_manage_fd(fd, BACKEND_NAME, 1, inst); +} + +static int openpixel_client_handle(instance* inst, int fd){ + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + uint8_t buffer[8192]; + size_t c = 0, offset = 0, u; + ssize_t bytes_left = 0; + channel* chan = NULL; + channel_value val; + + for(c = 0; c < data->clients; c++){ + if(data->client[c].fd == fd){ + break; + } + } + + if(c == data->clients){ + LOGPF("Unknown client descriptor signaled on %s", inst->name); + return 1; + } + + //FIXME might want to read until EAGAIN + ssize_t bytes = recv(fd, buffer, sizeof(buffer), 0); + if(bytes <= 0){ + if(bytes < 0){ + LOGPF("Failed to receive from client: %s", strerror(errno)); + } + + //close the connection + close(fd); + data->client[c].fd = -1; + //unmanage the fd + mm_manage_fd(fd, BACKEND_NAME, 0, NULL); + return 0; + } + + for(bytes_left = bytes - offset; bytes_left > 0; bytes_left = bytes - offset){ + if(data->client[c].buffer == -1){ + //read a header + DBGPF("Reading %" PRIsize_t " bytes to header at offset %" PRIsize_t ", header size %" PRIsize_t ", %" PRIsize_t " bytes left", min(sizeof(openpixel_header) - data->client[c].offset, bytes_left), data->client[c].offset, sizeof(openpixel_header), bytes_left); + memcpy(((uint8_t*) (&data->client[c].hdr)) + data->client[c].offset, buffer + offset, min(sizeof(openpixel_header) - data->client[c].offset, bytes_left)); + + //if done, resolve buffer + if(sizeof(openpixel_header) - data->client[c].offset < bytes_left){ + data->client[c].buffer = openpixel_buffer_find(data, data->client[c].hdr.strip, 1); + //if no buffer or mode mismatch, ignore data + if(data->client[c].buffer < 0 + || data->mode != data->client[c].hdr.mode){ + data->client[c].buffer = -2; //mark for ignore + } + data->client[c].left = be16toh(data->client[c].hdr.length); + data->client[c].offset = 0; + } + //if not, update client offset + else{ + data->client[c].offset += bytes_left; + } + + //update scan offset + offset += min(sizeof(openpixel_header) - data->client[c].offset, bytes_left); + } + else{ + //read data + if(data->client[c].buffer == -2){ + //ignore data + offset += min(data->client[c].left, bytes_left); + data->client[c].offset += min(data->client[c].left, bytes_left); + data->client[c].left -= min(data->client[c].left, bytes_left); + } + else{ + if(data->mode == rgb8){ + for(u = 0; u < bytes_left; u++){ + //if over buffer length, ignore + if(u + data->client[c].offset >= data->buffer[data->client[c].buffer].bytes){ + data->client[c].buffer = -2; + break; + } + + //FIXME if at start of trailing non-multiple of 3, ignore + + //update changed channels + if(data->buffer[data->client[c].buffer].data.u8[u + data->client[c].offset] != buffer[offset + u]){ + data->buffer[data->client[c].buffer].data.u8[u + data->client[c].offset] = buffer[offset + u]; + chan = mm_channel(inst, ((uint64_t) data->client[c].hdr.strip << 32) | (u + data->client[c].offset + 1), 0); + if(chan){ + //push event + val.raw.u64 = buffer[offset + u]; + val.normalised = (double) buffer[offset + u] / 255.0; + if(mm_channel_event(chan, val)){ + LOG("Failed to push channel event to core"); + //FIXME err out here + } + } + } + } + + //update offsets + offset += u; + data->client[c].offset += u; + data->client[c].left -= u; + } + else{ + //TODO byte-order conversion may be on recv boundary + //if over buffer length, ignore + //skip non-multiple-of 6 trailing data + } + } + + //end of data, return to reading headers + if(data->client[c].left == 0){ + data->client[c].buffer = -1; + data->client[c].offset = 0; + data->client[c].left = 0; + } + } + } + + return 0; +} + static int openpixel_handle(size_t num, managed_fd* fds){ - //TODO handle bcast + size_t u; + instance* inst = NULL; + openpixel_instance_data* data = NULL; + uint8_t buffer[8192]; + ssize_t bytes; + + for(u = 0; u < num; u++){ + inst = (instance*) fds[u].impl; + data = (openpixel_instance_data*) inst->impl; + + if(fds[u].fd == data->dest_fd){ + //destination fd ready to read + //since the protocol does not define any responses, the connection was probably closed + bytes = recv(data->dest_fd, buffer, sizeof(buffer), 0); + if(bytes <= 0){ + LOGPF("Output descriptor closed on instance %s", inst->name); + //unmanage the fd to give the core some rest + mm_manage_fd(data->dest_fd, BACKEND_NAME, 0, NULL); + } + else{ + LOGPF("Unhandled response data on %s (%" PRIsize_t" bytes)", inst->name, bytes); + } + } + else if(fds[u].fd == data->listen_fd){ + //listen fd ready to read, accept a new client + if(openpixel_client_new(inst, accept(data->listen_fd, NULL, NULL))){ + return 1; + } + } + else{ + //handle client input + openpixel_client_handle(inst, fds[u].fd); + } + } return 0; } @@ -323,12 +528,11 @@ static int openpixel_shutdown(size_t n, instance** inst){ //shutdown all clients for(p = 0; p < data->clients; p++){ - if(data->client_fd[p] >= 0){ - close(data->client_fd[p]); + if(data->client[p].fd>= 0){ + close(data->client[p].fd); } } - free(data->client_fd); - free(data->bytes_left); + free(data->client); //close all configured fds if(data->listen_fd >= 0){ diff --git a/backends/openpixelcontrol.h b/backends/openpixelcontrol.h index f1061ea..658bbf0 100644 --- a/backends/openpixelcontrol.h +++ b/backends/openpixelcontrol.h @@ -31,6 +31,18 @@ typedef struct /*_openpixel_hdr*/ { } openpixel_header; #pragma pack(pop) +typedef struct /*_openpixel_client*/ { + int fd; + ssize_t buffer; + openpixel_header hdr; + size_t offset; + size_t left; + union { + uint8_t u8[2]; + uint16_t u16; + } boundary; +} openpixel_client; + typedef struct { enum { rgb8 = 0, @@ -43,6 +55,5 @@ typedef struct { int dest_fd; int listen_fd; size_t clients; - int* client_fd; - size_t* bytes_left; + openpixel_client* client; } openpixel_instance_data; diff --git a/backends/openpixelcontrol.md b/backends/openpixelcontrol.md index ce60278..6dd38bc 100644 --- a/backends/openpixelcontrol.md +++ b/backends/openpixelcontrol.md @@ -45,5 +45,10 @@ strip1.blue2 < strip2.green66 #### Known bugs / problems -If the connection is lost, it is currently not reestablished and may cause exit the MIDIMonster entirely. -Thisi behaviour may be changed in future releases. +If the connection is lost, it is currently not reestablished and may cause the MIDIMonster to exit entirely. +This behaviour may be changed in future releases. + +While acting as an OpenPixelControl server, the backend allows multiple clients to connect. +This may lead to confusing data output when multiple clients are trying to control the same strip. + +16 bit server mode is not implemented yet. This will be fixed in a future release. -- cgit v1.2.3 From f49c46c184ecdd5209f1fd9df47daed0acfae728 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 28 Feb 2020 18:53:45 +0100 Subject: Update README to reflect all backends, add notes for building Lua on Windows --- backends/lua.md | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'backends') diff --git a/backends/lua.md b/backends/lua.md index d01a8c6..b936a99 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -64,3 +64,13 @@ Using these names as arguments to the output and value interface functions works Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. + +To build (and run) the `lua` backend on Windows, a compiled version of the Lua library is required. +For various reasons (legal, separations of concern, not wanting to ship binary data in the repository), +you will need to acquire a copy of `lua53.dll` (for example by downloading it from the [luabinaries +project](http://luabinaries.sourceforge.net/download.html). + +To build the `lua` backend for Windows, place `lua53.dll` in a subdirectory `libs/` in the project root +and run `make lua.dll` inside the `backends/` directory. + +At runtime, Windows searches for the file in the same directory as `midimonster.exe`. -- cgit v1.2.3 From cff53b6fb1996a24dbdef3657e4fac6558913c33 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 28 Feb 2020 18:54:35 +0100 Subject: Restructure openpixelcontrol --- backends/openpixelcontrol.c | 214 ++++++++++++++++++++++++++------------------ backends/openpixelcontrol.h | 2 +- 2 files changed, 127 insertions(+), 89 deletions(-) (limited to 'backends') diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 3a94a12..d386c26 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "openpixelcontrol" +#define DEBUG #include @@ -217,12 +218,37 @@ static channel* openpixel_channel(instance* inst, char* spec, uint8_t flags){ return mm_channel(inst, ((uint64_t) strip) << 32 | channel, 1); } +static int openpixel_output_data(openpixel_instance_data* data){ + size_t u; + openpixel_header hdr; + + //send updated strips + for(u = 0; u < data->buffers; u++){ + if(!(data->buffer[u].flags & OPENPIXEL_INPUT) && (data->buffer[u].flags & OPENPIXEL_MARK)){ + //remove mark + data->buffer[u].flags &= ~OPENPIXEL_MARK; + + //prepare header + hdr.strip = data->buffer[u].strip; + hdr.mode = data->mode; + hdr.length = htobe16(data->buffer[u].bytes); + + //output data + if(mmbackend_send(data->dest_fd, (uint8_t*) &hdr, sizeof(hdr)) + || mmbackend_send(data->dest_fd, data->buffer[u].data.u8, data->buffer[u].bytes)){ + return 1; + } + } + } + + return 0; +} + static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* v){ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; size_t u, p; ssize_t buffer; uint32_t strip, channel; - openpixel_header hdr; for(u = 0; u < num; u++){ //read strip/channel @@ -266,25 +292,7 @@ static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* } } - //send updated strips - for(u = 0; u < data->buffers; u++){ - if(!(data->buffer[u].flags & OPENPIXEL_INPUT) && (data->buffer[u].flags & OPENPIXEL_MARK)){ - //remove mark - data->buffer[u].flags &= ~OPENPIXEL_MARK; - - //prepare header - hdr.strip = data->buffer[u].strip; - hdr.mode = data->mode; - hdr.length = htobe16(data->buffer[u].bytes); - - //output data - if(mmbackend_send(data->dest_fd, (uint8_t*) &hdr, sizeof(hdr)) - || mmbackend_send(data->dest_fd, data->buffer[u].data.u8, data->buffer[u].bytes)){ - return 1; - } - } - } - return 0; + return openpixel_output_data(data); } static int openpixel_client_new(instance* inst, int fd){ @@ -329,17 +337,97 @@ static int openpixel_client_new(instance* inst, int fd){ data->client[u].buffer = -1; data->client[u].offset = 0; + LOGPF("New client on instance %s", inst->name); return mm_manage_fd(fd, BACKEND_NAME, 1, inst); } -static int openpixel_client_handle(instance* inst, int fd){ +static ssize_t openpixel_client_pixeldata(instance* inst, openpixel_client* client, uint8_t* buffer, size_t bytes_left){ openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; - uint8_t buffer[8192]; - size_t c = 0, offset = 0, u; - ssize_t bytes_left = 0; + size_t u; channel* chan = NULL; channel_value val; + if(client->buffer == -2){ + //ignore data + u = min(client->left, bytes_left); + client->offset += u; + client->left -= u; + return u; + } + else{ + if(data->mode == rgb8){ + for(u = 0; u < bytes_left; u++){ + //if over buffer length, ignore + if(u + client->offset >= data->buffer[client->buffer].bytes){ + client->buffer = -2; + break; + } + + //FIXME if at start of trailing non-multiple of 3, ignore + + //update changed channels + if(data->buffer[client->buffer].data.u8[u + client->offset] != buffer[u]){ + data->buffer[client->buffer].data.u8[u + client->offset] = buffer[u]; + chan = mm_channel(inst, ((uint64_t) client->hdr.strip << 32) | (u + client->offset + 1), 0); + if(chan){ + //push event + val.raw.u64 = buffer[u]; + val.normalised = (double) buffer[u] / 255.0; + if(mm_channel_event(chan, val)){ + LOG("Failed to push channel event to core"); + } + } + } + } + + //update offsets + client->offset += u; + client->left -= u; + return u; + } + else{ + //TODO byte-order conversion may be on recv boundary + //if over buffer length, ignore + //skip non-multiple-of 6 trailing data + } + } + return -1; +} + +static ssize_t openpixel_client_headerdata(instance* inst, openpixel_client* client, uint8_t* buffer, size_t bytes_left){ + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + size_t bytes_consumed = min(sizeof(openpixel_header) - client->offset, bytes_left); + + DBGPF("Reading %" PRIsize_t " bytes to header at offset %" PRIsize_t ", header size %" PRIsize_t ", %" PRIsize_t " bytes left", bytes_consumed, client->offset, sizeof(openpixel_header), bytes_left); + memcpy(((uint8_t*) (&client->hdr)) + client->offset, buffer, bytes_consumed); + + //if done, resolve buffer + if(sizeof(openpixel_header) - client->offset <= bytes_left){ + client->buffer = openpixel_buffer_find(data, client->hdr.strip, 1); + //TODO handle broadcast strip input + //if no buffer or mode mismatch, ignore data + if(client->buffer < 0 + || data->mode != client->hdr.mode){ + client->buffer = -2; //mark for ignore + } + client->left = be16toh(client->hdr.length); + client->offset = 0; + } + //if not, update client offset + else{ + client->offset += bytes_consumed; + } + + //update scan offset + return bytes_consumed; +} + +static int openpixel_client_handle(instance* inst, int fd){ + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + uint8_t buffer[8192]; + size_t c = 0, offset = 0; + ssize_t bytes_left = 0, bytes_handled; + for(c = 0; c < data->clients; c++){ if(data->client[c].fd == fd){ break; @@ -361,81 +449,27 @@ static int openpixel_client_handle(instance* inst, int fd){ //close the connection close(fd); data->client[c].fd = -1; + //unmanage the fd + LOGPF("Client disconnected on %s", inst->name); mm_manage_fd(fd, BACKEND_NAME, 0, NULL); return 0; } + DBGPF("Received %" PRIsize_t " bytes on %s", bytes, inst->name); for(bytes_left = bytes - offset; bytes_left > 0; bytes_left = bytes - offset){ if(data->client[c].buffer == -1){ //read a header - DBGPF("Reading %" PRIsize_t " bytes to header at offset %" PRIsize_t ", header size %" PRIsize_t ", %" PRIsize_t " bytes left", min(sizeof(openpixel_header) - data->client[c].offset, bytes_left), data->client[c].offset, sizeof(openpixel_header), bytes_left); - memcpy(((uint8_t*) (&data->client[c].hdr)) + data->client[c].offset, buffer + offset, min(sizeof(openpixel_header) - data->client[c].offset, bytes_left)); - - //if done, resolve buffer - if(sizeof(openpixel_header) - data->client[c].offset < bytes_left){ - data->client[c].buffer = openpixel_buffer_find(data, data->client[c].hdr.strip, 1); - //if no buffer or mode mismatch, ignore data - if(data->client[c].buffer < 0 - || data->mode != data->client[c].hdr.mode){ - data->client[c].buffer = -2; //mark for ignore - } - data->client[c].left = be16toh(data->client[c].hdr.length); - data->client[c].offset = 0; + bytes_handled = openpixel_client_headerdata(inst, data->client + c, buffer + offset, bytes_left); + if(bytes_handled < 0){ + //FIXME handle errors } - //if not, update client offset - else{ - data->client[c].offset += bytes_left; - } - - //update scan offset - offset += min(sizeof(openpixel_header) - data->client[c].offset, bytes_left); } else{ //read data - if(data->client[c].buffer == -2){ - //ignore data - offset += min(data->client[c].left, bytes_left); - data->client[c].offset += min(data->client[c].left, bytes_left); - data->client[c].left -= min(data->client[c].left, bytes_left); - } - else{ - if(data->mode == rgb8){ - for(u = 0; u < bytes_left; u++){ - //if over buffer length, ignore - if(u + data->client[c].offset >= data->buffer[data->client[c].buffer].bytes){ - data->client[c].buffer = -2; - break; - } - - //FIXME if at start of trailing non-multiple of 3, ignore - - //update changed channels - if(data->buffer[data->client[c].buffer].data.u8[u + data->client[c].offset] != buffer[offset + u]){ - data->buffer[data->client[c].buffer].data.u8[u + data->client[c].offset] = buffer[offset + u]; - chan = mm_channel(inst, ((uint64_t) data->client[c].hdr.strip << 32) | (u + data->client[c].offset + 1), 0); - if(chan){ - //push event - val.raw.u64 = buffer[offset + u]; - val.normalised = (double) buffer[offset + u] / 255.0; - if(mm_channel_event(chan, val)){ - LOG("Failed to push channel event to core"); - //FIXME err out here - } - } - } - } - - //update offsets - offset += u; - data->client[c].offset += u; - data->client[c].left -= u; - } - else{ - //TODO byte-order conversion may be on recv boundary - //if over buffer length, ignore - //skip non-multiple-of 6 trailing data - } + bytes_handled = openpixel_client_pixeldata(inst, data->client + c, buffer + offset, bytes_left); + if(bytes_handled < 0){ + //FIXME handle errors } //end of data, return to reading headers @@ -445,7 +479,9 @@ static int openpixel_client_handle(instance* inst, int fd){ data->client[c].left = 0; } } + offset += bytes_handled; } + DBGPF("Processing done on %s", inst->name); return 0; } @@ -482,7 +518,9 @@ static int openpixel_handle(size_t num, managed_fd* fds){ } else{ //handle client input - openpixel_client_handle(inst, fds[u].fd); + if(openpixel_client_handle(inst, fds[u].fd)){ + return 1; + } } } return 0; diff --git a/backends/openpixelcontrol.h b/backends/openpixelcontrol.h index 658bbf0..63e9664 100644 --- a/backends/openpixelcontrol.h +++ b/backends/openpixelcontrol.h @@ -33,7 +33,7 @@ typedef struct /*_openpixel_hdr*/ { typedef struct /*_openpixel_client*/ { int fd; - ssize_t buffer; + ssize_t buffer; /* -1 header, -2 ignore, -3 bcast */ openpixel_header hdr; size_t offset; size_t left; -- cgit v1.2.3 From d574abf032da350f1208d9480d24c51187bc7c63 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 28 Feb 2020 19:00:58 +0100 Subject: Minor documentation fix --- backends/lua.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/lua.md b/backends/lua.md index b936a99..d2552e0 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -65,9 +65,10 @@ Using these names as arguments to the output and value interface functions works Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. -To build (and run) the `lua` backend on Windows, a compiled version of the Lua library is required. +To build (and run) the `lua` backend on Windows, a compiled version of the Lua 5.3 library is required. For various reasons (legal, separations of concern, not wanting to ship binary data in the repository), -you will need to acquire a copy of `lua53.dll` (for example by downloading it from the [luabinaries +the MIDIMonster project can not provide this file within this repository. +You will need to acquire a copy of `lua53.dll`, for example by downloading it from the [luabinaries project](http://luabinaries.sourceforge.net/download.html). To build the `lua` backend for Windows, place `lua53.dll` in a subdirectory `libs/` in the project root -- cgit v1.2.3 From a103c7a7dcabdb84399dce2a1ed517299d0f5414 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 28 Feb 2020 19:21:45 +0100 Subject: Do not build openpixelcontrol with debug output --- backends/openpixelcontrol.c | 1 - 1 file changed, 1 deletion(-) (limited to 'backends') diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index d386c26..0f590c6 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -1,5 +1,4 @@ #define BACKEND_NAME "openpixelcontrol" -#define DEBUG #include -- cgit v1.2.3 From 454e757f740b74d714e9fa47bfa4954cb30e67ba Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 28 Feb 2020 21:10:45 +0100 Subject: Implement openpixelcontrol 16bit server mode --- backends/openpixelcontrol.c | 35 ++++++++++++++++++++++++++++++++--- backends/openpixelcontrol.md | 2 -- 2 files changed, 32 insertions(+), 5 deletions(-) (limited to 'backends') diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 0f590c6..62cdb68 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -385,9 +385,38 @@ static ssize_t openpixel_client_pixeldata(instance* inst, openpixel_client* clie return u; } else{ - //TODO byte-order conversion may be on recv boundary - //if over buffer length, ignore - //skip non-multiple-of 6 trailing data + for(u = 0; u < bytes_left; u++){ + //if over buffer length, ignore + if(u + client->offset >= data->buffer[client->buffer].bytes){ + client->buffer = -2; + break; + } + + //if at start of trailing non-multiple of 6, ignore + if((client->offset + u) >= (client->offset + client->left) - ((client->offset + client->left) % 6)){ + client->buffer = -2; + break; + } + + //byte-order conversion may be on message boundary, do it via a buffer + client->boundary.u8[(client->offset + u) % 2] = buffer[u]; + + //detect and update changed channels + if((client->offset + u) % 2 + && data->buffer[client->buffer].data.u16[(u + client->offset) / 2] != be16toh(client->boundary.u16)){ + data->buffer[client->buffer].data.u16[(u + client->offset) / 2] = be16toh(client->boundary.u16); + chan = mm_channel(inst, ((uint64_t) client->hdr.strip << 32) | ((u + client->offset) / 2 + 1), 0); + if(chan){ + //push event + val.raw.u64 = be16toh(client->boundary.u16);; + val.normalised = (double) val.raw.u64 / 65535.0; + if(mm_channel_event(chan, val)){ + LOG("Failed to push channel event to core"); + } + } + + } + } } } return -1; diff --git a/backends/openpixelcontrol.md b/backends/openpixelcontrol.md index 6dd38bc..5a8686f 100644 --- a/backends/openpixelcontrol.md +++ b/backends/openpixelcontrol.md @@ -50,5 +50,3 @@ This behaviour may be changed in future releases. While acting as an OpenPixelControl server, the backend allows multiple clients to connect. This may lead to confusing data output when multiple clients are trying to control the same strip. - -16 bit server mode is not implemented yet. This will be fixed in a future release. -- cgit v1.2.3 From 9c564af18dc3faad8910bfe14b45ed4ab884d797 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 29 Feb 2020 15:35:08 +0100 Subject: Keep console alive when exiting as last process on Windows --- backends/lua.c | 1 + 1 file changed, 1 insertion(+) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 7d20fb1..955341a 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -457,6 +457,7 @@ static int lua_start(size_t n, instance** inst){ if(strcmp(data->channel_name[p], "output") && strcmp(data->channel_name[p], "input_value") && strcmp(data->channel_name[p], "output_value") + && strcmp(data->channel_name[p], "input_channel") && strcmp(data->channel_name[p], "interval")){ lua_getglobal(data->interpreter, data->channel_name[p]); data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); -- cgit v1.2.3 From 1618f49ff5dc317968e385e5cbc4aae32e8fb67b Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 1 Mar 2020 15:02:33 +0100 Subject: Fix some errors in the Lua documentation --- backends/lua.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'backends') diff --git a/backends/lua.md b/backends/lua.md index d2552e0..ce82a4d 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -1,7 +1,7 @@ ### The `lua` backend -The `lua` backend provides a flexible programming environment, allowing users to route and manipulate -events using the Lua programming language. +The `lua` backend provides a flexible programming environment, allowing users to route, generate +and manipulate events using the Lua scripting language. Every instance has its own interpreter state which can be loaded with custom handler scripts. @@ -45,7 +45,7 @@ The `lua` backend does not take any global configuration. |---------------|-----------------------|-----------------------|-----------------------| | `script` | `script.lua` | none | Lua source file (relative to configuration file)| -A single instance may have multiple `source` options specified, which will all be read cumulatively. +A single instance may have multiple `script` options specified, which will all be read cumulatively. #### Channel specification @@ -63,7 +63,7 @@ as an input channel name to a Lua instance will not call any handler functions. Using these names as arguments to the output and value interface functions works as intended. Output values will not trigger corresponding input event handlers unless the channel is mapped -back in the MIDIMonster configuration. +back in the MIDIMonster configuration. This is intentional. To build (and run) the `lua` backend on Windows, a compiled version of the Lua 5.3 library is required. For various reasons (legal, separations of concern, not wanting to ship binary data in the repository), -- cgit v1.2.3 From fe84e353f2580315804319438b1951752249a9ee Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 3 Mar 2020 23:11:14 +0100 Subject: Implement python backend --- backends/Makefile | 2 +- backends/python.c | 419 +++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/python.h | 25 ++++ backends/python.md | 63 ++++++++ 4 files changed, 508 insertions(+), 1 deletion(-) create mode 100644 backends/python.c create mode 100644 backends/python.h create mode 100644 backends/python.md (limited to 'backends') diff --git a/backends/Makefile b/backends/Makefile index 191a495..366d4b4 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full LINUX_BACKENDS = midi.so evdev.so WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o diff --git a/backends/python.c b/backends/python.c new file mode 100644 index 0000000..9fad0ae --- /dev/null +++ b/backends/python.c @@ -0,0 +1,419 @@ +#define BACKEND_NAME "python" + +#define PY_SSIZE_T_CLEAN +#include +#include +#include "python.h" + +#define MMPY_INSTANCE_KEY "midimonster_instance" + +/* + * TODO might want to export the full MM_API set to python at some point + */ + +static PyThreadState* python_main = NULL; +static wchar_t* program_name = NULL; + +MM_PLUGIN_API int init(){ + backend python = { + .name = BACKEND_NAME, + .conf = python_configure, + .create = python_instance, + .conf_instance = python_configure_instance, + .channel = python_channel, + .handle = python_set, + .process = python_handle, + .start = python_start, + .shutdown = python_shutdown + }; + + //register backend + if(mm_backend_register(python)){ + LOG("Failed to register backend"); + return 1; + } + return 0; +} + +static int python_configure(char* option, char* value){ + LOG("No backend configuration possible"); + return 1; +} + +static int python_prepend_str(PyObject* list, char* str){ + if(!list || !str){ + return 1; + } + + PyObject* item = PyUnicode_FromString(str); + if(!item){ + return 1; + } + + if(PyList_Insert(list, 0, item) < 0){ + Py_DECREF(item); + return 1; + } + Py_DECREF(item); + return 0; +} + +static PyObject* mmpy_output(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + const char* channel_name = NULL; + channel* chan = NULL; + channel_value val = { + 0 + }; + size_t u; + + if(!PyArg_ParseTuple(args, "sd", &channel_name, &val.normalised)){ + return NULL; + } + + val.normalised = clamp(val.normalised, 1.0, 0.0); + + for(u = 0; u < data->channels; u++){ + if(!strcmp(data->channel[u].name, channel_name)){ + DBGPF("Setting channel %s.%s to %f", inst->name, channel_name, val.normalised); + chan = mm_channel(inst, u, 0); + //this should never happen + if(!chan){ + LOGPF("Failed to fetch parsed channel %s.%s", inst->name, channel_name); + break; + } + data->channel[u].out = val.normalised; + mm_channel_event(chan, val); + break; + } + } + + if(u == data->channels){ + DBGPF("Output on unknown channel %s.%s, no event pushed", inst->name, channel_name); + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* mmpy_channel_value(PyObject* self, PyObject* args, uint8_t in){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + const char* channel_name = NULL; + size_t u; + + if(!PyArg_ParseTuple(args, "s", &channel_name)){ + return NULL; + } + + for(u = 0; u < data->channels; u++){ + if(!strcmp(data->channel[u].name, channel_name)){ + return PyFloat_FromDouble(in ? data->channel[u].in : data->channel[u].out); + } + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* mmpy_current_handler(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + + if(data->current_channel){ + return PyUnicode_FromString(data->current_channel->name); + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* mmpy_output_value(PyObject* self, PyObject* args){ + return mmpy_channel_value(self, args, 0); +} + +static PyObject* mmpy_input_value(PyObject* self, PyObject* args){ + return mmpy_channel_value(self, args, 1); +} + +static int mmpy_exec(PyObject* module) { + instance** inst = (instance**) PyModule_GetState(module); + //FIXME actually use interpreter dict (from python 3.8) here at some point + PyObject* capsule = PyDict_GetItemString(PyThreadState_GetDict(), MMPY_INSTANCE_KEY); + if(capsule && inst){ + *inst = PyCapsule_GetPointer(capsule, NULL); + return 0; + } + + //TODO raise exception + return -1; +} + +static int python_configure_instance(instance* inst, char* option, char* value){ + python_instance_data* data = (python_instance_data*) inst->impl; + PyObject* module = NULL; + + //load python script + if(!strcmp(option, "module")){ + //swap to interpreter + PyEval_RestoreThread(data->interpreter); + //import the module + module = PyImport_ImportModule(value); + if(!module){ + LOGPF("Failed to import module %s to instance %s", value, inst->name); + PyErr_Print(); + } + Py_XDECREF(module); + PyEval_ReleaseThread(data->interpreter); + return 0; + } + + LOGPF("Unknown instance parameter %s for instance %s", option, inst->name); + return 1; +} + +static PyObject* mmpy_init(){ + static PyModuleDef_Slot mmpy_slots[] = { + {Py_mod_exec, (void*) mmpy_exec}, + {0} + }; + + static PyMethodDef mmpy_methods[] = { + {"output", mmpy_output, METH_VARARGS, "Output a channel event"}, + {"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel"}, + {"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel"}, + {"current", mmpy_current_handler, METH_VARARGS, "Get the name of the currently executing channel handler"}, + {0} + }; + + static struct PyModuleDef mmpy = { + PyModuleDef_HEAD_INIT, + "midimonster", + NULL, /*doc size*/ + sizeof(instance*), + mmpy_methods, + mmpy_slots + }; + + //single-phase init + //return PyModule_Create(&mmpy); + + //multi-phase init + return PyModuleDef_Init(&mmpy); +} + +static int python_instance(instance* inst){ + python_instance_data* data = calloc(1, sizeof(python_instance_data)); + PyObject* interpreter_dict = NULL; + char current_directory[8192]; + if(!data){ + LOG("Failed to allocate memory"); + return 1; + } + + //lazy-init because we need the interpreter running before _start, + //but don't want it running if no instances are defined + if(!python_main){ + LOG("Initializing main python interpreter"); + if(PyImport_AppendInittab("midimonster", &mmpy_init)){ + LOG("Failed to extend python inittab for main interpreter"); + } + program_name = Py_DecodeLocale("midimonster", NULL); + Py_SetProgramName(program_name); + //initialize python + Py_InitializeEx(0); + //create, acquire and release the GIL + PyEval_InitThreads(); + python_main = PyEval_SaveThread(); + } + + //acquire the GIL before creating a new interpreter + PyEval_RestoreThread(python_main); + //create subinterpreter for new instance + data->interpreter = Py_NewInterpreter(); + + //push cwd as import path + if(getcwd(current_directory, sizeof(current_directory))){ + if(python_prepend_str(PySys_GetObject("path"), current_directory)){ + LOG("Failed to push current working directory to python"); + goto bail; + } + } + + //push the instance pointer for later module initialization + //FIXME python 3.8 introduces interpreter_dict = PyInterpreterState_GetDict(data->interpreter->interp); + //for now use thread state... + interpreter_dict = PyThreadState_GetDict(); + if(!interpreter_dict){ + LOG("Failed to access per-interpreter data storage"); + goto bail; + } + //FIXME this might leak a reference to the capsule + if(PyDict_SetItemString(interpreter_dict, MMPY_INSTANCE_KEY, PyCapsule_New(inst, NULL, NULL))){ + LOG("Failed to set per-interpreter instance pointer"); + goto bail; + } + + //NewInterpreter leaves us with the GIL, drop it + PyEval_ReleaseThread(data->interpreter); + inst->impl = data; + return 0; + +bail: + if(data->interpreter){ + PyEval_ReleaseThread(data->interpreter); + } + free(data); + return 1; +} + +static channel* python_channel(instance* inst, char* spec, uint8_t flags){ + python_instance_data* data = (python_instance_data*) inst->impl; + size_t u; + + for(u = 0; u < data->channels; u++){ + if(!strcmp(data->channel[u].name, spec)){ + break; + } + } + + if(u == data->channels){ + data->channel = realloc(data->channel, (data->channels + 1) * sizeof(mmpython_channel)); + if(!data->channel){ + data->channels = 0; + LOG("Failed to allocate memory"); + return NULL; + } + memset(data->channel + u, 0, sizeof(mmpython_channel)); + + data->channel[u].name = strdup(spec); + if(!data->channel[u].name){ + LOG("Failed to allocate memory"); + return NULL; + } + data->channels++; + } + + return mm_channel(inst, u, 1); +} + +static int python_set(instance* inst, size_t num, channel** c, channel_value* v){ + python_instance_data* data = (python_instance_data*) inst->impl; + mmpython_channel* chan = NULL; + PyObject* result = NULL; + size_t u; + + //swap to interpreter + PyEval_RestoreThread(data->interpreter); + + for(u = 0; u < num; u++){ + chan = data->channel + c[u]->ident; + + //update input value buffer + chan->in = v[u].normalised; + + //call handler if present + if(chan->handler){ + DBGPF("Calling handler for %s.%s", inst->name, chan->name); + data->current_channel = chan; + result = PyObject_CallFunction(chan->handler, "d", chan->in); + Py_XDECREF(result); + data->current_channel = NULL; + DBGPF("Done with handler for %s.%s", inst->name, chan->name); + } + } + + PyEval_ReleaseThread(data->interpreter); + return 0; +} + +static int python_handle(size_t num, managed_fd* fds){ + //TODO implement some kind of intervaling functionality before people get it in their heads to start `import threading` + return 0; +} + +static int python_start(size_t n, instance** inst){ + python_instance_data* data = NULL; + PyObject* module = NULL; + size_t u, p; + char* module_name = NULL, *channel_name = NULL; + + //resolve channel references to handler functions + for(u = 0; u < n; u++){ + data = (python_instance_data*) inst[u]->impl; + + //switch to interpreter + PyEval_RestoreThread(data->interpreter); + for(p = 0; p < data->channels; p++){ + module = PyImport_AddModule("__main__"); + channel_name = data->channel[p].name; + module_name = strchr(channel_name, '.'); + if(module_name){ + *module_name = 0; + //returns borrowed reference + module = PyImport_AddModule(channel_name); + + if(!module){ + LOGPF("Module %s for qualified channel %s.%s is not loaded on instance %s", channel_name, channel_name, module_name + 1, inst[u]->name); + return 1; + } + + *module_name = '.'; + channel_name = module_name + 1; + } + + //returns new reference + data->channel[p].handler = PyObject_GetAttrString(module, channel_name); + } + + //release interpreter + PyEval_ReleaseThread(data->interpreter); + } + return 0; +} + +static int python_shutdown(size_t n, instance** inst){ + size_t u, p; + python_instance_data* data = NULL; + + //clean up channels + //this needs to be done before stopping the interpreters, + //because the handler references are refcounted + for(u = 0; u < n; u++){ + data = (python_instance_data*) inst[u]->impl; + for(p = 0; p < data->channels; p++){ + free(data->channel[p].name); + Py_XDECREF(data->channel[p].handler); + } + free(data->channel); + //do not free data here, needed for shutting down interpreters + } + + if(python_main){ + //just used to lock the GIL + PyEval_RestoreThread(python_main); + + for(u = 0; u < n; u++){ + data = (python_instance_data*) inst[u]->impl; + DBGPF("Shutting down interpreter for instance %s", inst[u]->name); + //swap to interpreter and end it, GIL is held after this but state is NULL + PyThreadState_Swap(data->interpreter); + PyErr_Clear(); + //PyThreadState_Clear(data->interpreter); + Py_EndInterpreter(data->interpreter); + + free(data); + } + + //shut down main interpreter + PyThreadState_Swap(python_main); + if(Py_FinalizeEx()){ + LOG("Failed to destroy python interpreters"); + } + PyMem_RawFree(program_name); + } + + LOG("Backend shut down"); + return 0; +} diff --git a/backends/python.h b/backends/python.h new file mode 100644 index 0000000..10411ca --- /dev/null +++ b/backends/python.h @@ -0,0 +1,25 @@ +#include "midimonster.h" + +MM_PLUGIN_API int init(); +static int python_configure(char* option, char* value); +static int python_configure_instance(instance* inst, char* option, char* value); +static int python_instance(instance* inst); +static channel* python_channel(instance* inst, char* spec, uint8_t flags); +static int python_set(instance* inst, size_t num, channel** c, channel_value* v); +static int python_handle(size_t num, managed_fd* fds); +static int python_start(size_t n, instance** inst); +static int python_shutdown(size_t n, instance** inst); + +typedef struct /*_python_channel_data*/ { + char* name; + PyObject* handler; + double in; + double out; +} mmpython_channel; + +typedef struct /*_python_instance_data*/ { + PyThreadState* interpreter; + size_t channels; + mmpython_channel* channel; + mmpython_channel* current_channel; +} python_instance_data; diff --git a/backends/python.md b/backends/python.md new file mode 100644 index 0000000..5f81e70 --- /dev/null +++ b/backends/python.md @@ -0,0 +1,63 @@ +### The `python` backend + +The `python` backend provides a flexible programming environment, allowing users +to route, generate and manipulate channel events using the Python 3 scripting languge. + +Every instance has its own interpreter, which can be loaded with multiple Python modules. +These modules may contain member functions accepting a single `float` parameter, which can +then be used as target channels. For each incoming event, the handler function is called. + +To interact with the MIDIMonster core, import the `midimonster` module from within your module. + +The `midimonster` module provides the following functions: + +| Function | Usage example | Description | +|-------------------------------|---------------------------------------|-----------------------------------------------| +| `output(string, float)` | `midimonster.output("foo", 0.75)` | Output a value event to a channel | +| `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel | +| `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel | +| `current()` | `print(midimonster.current())` | Returns the name of the input channel whose handler function is currently running or `None` if the interpreter was called from another context | + +Example Python module: +``` +import midimonster + +def in1(value): + midimonster.output("out1", 1 - value) +``` + +Input values range between 0.0 and 1.0, output values are clamped to the same range. + +#### Global configuration + +The `python` backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------------------------------| +| `module` | `my_handlers.py` | none | (Path to) Python module source file, relative to configuration file location | + +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). + +#### Channel specification + +Channel names may be any valid Python function name. To call handler functions in a module, +specify the channel as the functions qualified path (by prefixing it with the module name and a dot). + +Example mappings: +``` +py1.my_handlers.in1 < py1.foo +py1.out1 > py2.module.handler +``` + +#### Known bugs / problems + +Output values will not trigger corresponding input event handlers unless the channel is mapped +back in the MIDIMonster configuration. This is intentional. + +Importing a Python module named `midimonster` may cause problems and is unsupported. + +There is currently no functionality for cyclic execution. This may be implemented in a future +release. -- cgit v1.2.3 From 2e689e8852dc985249b214a9db98c1221dfc689f Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Mar 2020 01:20:39 +0100 Subject: Clean up & retab CI script, fix typo --- backends/python.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/python.md b/backends/python.md index 5f81e70..fae3139 100644 --- a/backends/python.md +++ b/backends/python.md @@ -1,7 +1,7 @@ ### The `python` backend The `python` backend provides a flexible programming environment, allowing users -to route, generate and manipulate channel events using the Python 3 scripting languge. +to route, generate and manipulate channel events using the Python 3 scripting language. Every instance has its own interpreter, which can be loaded with multiple Python modules. These modules may contain member functions accepting a single `float` parameter, which can -- cgit v1.2.3 From fc0afb039ddd4ba8f6161f2c9026e196f6d7ddc3 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Mar 2020 01:29:38 +0100 Subject: Try to make OSX build the python backend --- backends/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/Makefile b/backends/Makefile index 366d4b4..e31ff24 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -63,8 +63,8 @@ lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 || lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") lua.dll: LDLIBS += -L../libs -llua53 -python.so: CFLAGS += $(shell pkg-config --cflags python3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") -python.so: CFLAGS += $(shell pkg-config --libs python3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +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\"") %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) -- cgit v1.2.3 From 90b5655a6eb3837ad6f984ce4ffcd3e9aa7480ce Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 5 Mar 2020 00:03:12 +0100 Subject: Implement ArtNet rate-limiting to approx. 44 pps --- backends/artnet.c | 52 +++++++++++++++++++++++++++++++++++++++++----------- backends/artnet.h | 15 ++++++++++++--- backends/artnet.md | 3 --- 3 files changed, 53 insertions(+), 17 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index ed426b0..77f03dd 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -9,6 +9,7 @@ #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; @@ -37,7 +38,6 @@ static int artnet_listener(char* host, char* port){ artnet_fd[artnet_fds].fd = fd; artnet_fd[artnet_fds].output_instances = 0; artnet_fd[artnet_fds].output_instance = NULL; - artnet_fd[artnet_fds].last_frame = NULL; artnet_fds++; return 0; } @@ -52,6 +52,7 @@ MM_PLUGIN_API int init(){ .handle = artnet_set, .process = artnet_handle, .start = artnet_start, + .interval = artnet_interval, .shutdown = artnet_shutdown }; @@ -68,6 +69,13 @@ MM_PLUGIN_API int init(){ return 0; } +static uint32_t artnet_interval(){ + if(next_frame){ + return next_frame; + } + return ARTNET_KEEPALIVE_INTERVAL; +} + static int artnet_configure(char* option, char* value){ char* host = NULL, *port = NULL, *fd_opts = NULL; if(!strcmp(option, "net")){ @@ -211,7 +219,8 @@ static int artnet_transmit(instance* inst){ //update last frame timestamp for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){ if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){ - artnet_fd[data->fd_index].last_frame[u] = mm_timestamp(); + artnet_fd[data->fd_index].output_instance[u].last_frame = mm_timestamp(); + artnet_fd[data->fd_index].output_instance[u].mark = 0; } } return 0; @@ -226,7 +235,6 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) return 0; } - //FIXME maybe introduce minimum frame interval for(u = 0; u < num; u++){ if(IS_WIDE(data->data.map[c[u]->ident])){ uint32_t val = v[u].normalised * ((double) 0xFFFF); @@ -248,6 +256,21 @@ 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){ + break; + } + } + + //check output rate limit, request next frame + if(mm_timestamp() - artnet_fd[data->fd_index].output_instance[u].last_frame < ARTNET_FRAME_TIMEOUT){ + artnet_fd[data->fd_index].output_instance[u].mark = 1; + if(!next_frame || next_frame < mm_timestamp() - artnet_fd[data->fd_index].output_instance[u].last_frame){ + next_frame = mm_timestamp() - artnet_fd[data->fd_index].output_instance[u].last_frame; + } + return 0; + } return artnet_transmit(inst); } @@ -325,15 +348,23 @@ static int artnet_handle(size_t num, managed_fd* fds){ instance* inst = NULL; artnet_pkt* frame = (artnet_pkt*) recv_buf; - //transmit keepalive frames + //transmit keepalive & synthesized frames + next_frame = 0; for(u = 0; u < artnet_fds; u++){ for(c = 0; c < artnet_fd[u].output_instances; c++){ - if(timestamp - artnet_fd[u].last_frame[c] >= ARTNET_KEEPALIVE_INTERVAL){ + if(timestamp - artnet_fd[u].output_instance[c].last_frame >= ARTNET_KEEPALIVE_INTERVAL //timeout + || (artnet_fd[u].output_instance[c].mark && timestamp - artnet_fd[u].output_instance[c].last_frame >= ARTNET_FRAME_TIMEOUT)){ //synthesized frame inst = mm_instance_find(BACKEND_NAME, artnet_fd[u].output_instance[c].label); if(inst){ artnet_transmit(inst); } } + + //update next_frame + if(artnet_fd[u].output_instance[c].mark + && (!next_frame || next_frame > mm_timestamp() - artnet_fd[u].output_instance[c].last_frame)){ + next_frame = mm_timestamp() - artnet_fd[u].output_instance[c].last_frame; + } } } @@ -407,15 +438,15 @@ 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_instance_id)); - artnet_fd[data->fd_index].last_frame = realloc(artnet_fd[data->fd_index].last_frame, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(uint64_t)); + 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)); - if(!artnet_fd[data->fd_index].output_instance || !artnet_fd[data->fd_index].last_frame){ + if(!artnet_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] = id; - artnet_fd[data->fd_index].last_frame[artnet_fd[data->fd_index].output_instances] = 0; + 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; artnet_fd[data->fd_index].output_instances++; } @@ -443,7 +474,6 @@ static int artnet_shutdown(size_t n, instance** inst){ for(p = 0; p < artnet_fds; p++){ close(artnet_fd[p].fd); free(artnet_fd[p].output_instance); - free(artnet_fd[p].last_frame); } free(artnet_fd); diff --git a/backends/artnet.h b/backends/artnet.h index 1efdee6..aac73fe 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -4,6 +4,7 @@ #include "midimonster.h" MM_PLUGIN_API int init(); +static uint32_t artnet_interval(); static int artnet_configure(char* option, char* value); static int artnet_configure_instance(instance* instance, char* option, char* value); static int artnet_instance(instance* inst); @@ -16,7 +17,10 @@ static int artnet_shutdown(size_t n, instance** inst); #define ARTNET_PORT "6454" #define ARTNET_VERSION 14 #define ARTNET_RECV_BUF 4096 -#define ARTNET_KEEPALIVE_INTERVAL 2000 + +#define ARTNET_KEEPALIVE_INTERVAL 1000 +//limit transmit rate to at most 44 packets per second (1000/44 ~= 22) +#define ARTNET_FRAME_TIMEOUT 20 #define MAP_COARSE 0x0200 #define MAP_FINE 0x0400 @@ -52,11 +56,16 @@ typedef union /*_artnet_instance_id*/ { uint64_t label; } artnet_instance_id; +typedef struct /*_artnet_fd_universe*/ { + uint64_t label; + uint64_t last_frame; + uint8_t mark; +} artnet_output_universe; + typedef struct /*_artnet_fd*/ { int fd; size_t output_instances; - artnet_instance_id* output_instance; - uint64_t* last_frame; + artnet_output_universe* output_instance; } artnet_descriptor; #pragma pack(push, 1) diff --git a/backends/artnet.md b/backends/artnet.md index 90a7697..7e1ecff 100644 --- a/backends/artnet.md +++ b/backends/artnet.md @@ -36,6 +36,3 @@ net1.1+2 > net2.5+123 A normal channel that is part of a wide channel can not be mapped individually. #### Known bugs / problems - -The minimum inter-frame-time is disregarded, as the packet rate is determined by the rate of incoming -channel events. \ No newline at end of file -- cgit v1.2.3 From 335196b3d3c80ee4bbe0985fd9a1f8ab5464a27c Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 5 Mar 2020 23:46:08 +0100 Subject: Fix artnet rate limiting --- backends/artnet.c | 19 ++++++++++++------- backends/artnet.h | 3 ++- 2 files changed, 14 insertions(+), 8 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 77f03dd..f1b8c0e 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -227,6 +227,7 @@ static int artnet_transmit(instance* inst){ } static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v){ + uint32_t frame_delta = 0; size_t u, mark = 0; artnet_instance_data* data = (artnet_instance_data*) inst->impl; @@ -263,11 +264,12 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) } } + frame_delta = mm_timestamp() - artnet_fd[data->fd_index].output_instance[u].last_frame; //check output rate limit, request next frame - if(mm_timestamp() - artnet_fd[data->fd_index].output_instance[u].last_frame < ARTNET_FRAME_TIMEOUT){ + if(frame_delta < ARTNET_FRAME_TIMEOUT){ artnet_fd[data->fd_index].output_instance[u].mark = 1; - if(!next_frame || next_frame < mm_timestamp() - artnet_fd[data->fd_index].output_instance[u].last_frame){ - next_frame = mm_timestamp() - artnet_fd[data->fd_index].output_instance[u].last_frame; + if(!next_frame || next_frame > (ARTNET_KEEPALIVE_INTERVAL - frame_delta)){ + next_frame = (ARTNET_KEEPALIVE_INTERVAL - frame_delta); } return 0; } @@ -340,6 +342,7 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ static int artnet_handle(size_t num, managed_fd* fds){ size_t u, c; uint64_t timestamp = mm_timestamp(); + uint32_t synthesize_delta = 0; ssize_t bytes_read; char recv_buf[ARTNET_RECV_BUF]; artnet_instance_id inst_id = { @@ -352,8 +355,10 @@ static int artnet_handle(size_t num, managed_fd* fds){ next_frame = 0; for(u = 0; u < artnet_fds; u++){ for(c = 0; c < artnet_fd[u].output_instances; c++){ - if(timestamp - artnet_fd[u].output_instance[c].last_frame >= ARTNET_KEEPALIVE_INTERVAL //timeout - || (artnet_fd[u].output_instance[c].mark && timestamp - artnet_fd[u].output_instance[c].last_frame >= ARTNET_FRAME_TIMEOUT)){ //synthesized frame + synthesize_delta = timestamp - artnet_fd[u].output_instance[c].last_frame; + if((artnet_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); if(inst){ artnet_transmit(inst); @@ -362,8 +367,8 @@ static int artnet_handle(size_t num, managed_fd* fds){ //update next_frame if(artnet_fd[u].output_instance[c].mark - && (!next_frame || next_frame > mm_timestamp() - artnet_fd[u].output_instance[c].last_frame)){ - next_frame = mm_timestamp() - artnet_fd[u].output_instance[c].last_frame; + && (!next_frame || next_frame > ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta)){ + next_frame = ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta; } } } diff --git a/backends/artnet.h b/backends/artnet.h index aac73fe..d83999d 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -20,7 +20,8 @@ static int artnet_shutdown(size_t n, instance** inst); #define ARTNET_KEEPALIVE_INTERVAL 1000 //limit transmit rate to at most 44 packets per second (1000/44 ~= 22) -#define ARTNET_FRAME_TIMEOUT 20 +#define ARTNET_FRAME_TIMEOUT 15 +#define ARTNET_SYNTHESIZE_MARGIN 10 #define MAP_COARSE 0x0200 #define MAP_FINE 0x0400 -- cgit v1.2.3 From f12fb1e2f88e29c8060dfe673b65a315a84c1b29 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 6 Mar 2020 00:44:26 +0100 Subject: Implement (optional) rate-limiting for sACN --- backends/sacn.c | 78 ++++++++++++++++++++++++++++++++++++++++++-------------- backends/sacn.h | 17 +++++++++--- backends/sacn.md | 4 +-- 3 files changed, 74 insertions(+), 25 deletions(-) (limited to 'backends') diff --git a/backends/sacn.c b/backends/sacn.c index 5096123..6c08b5a 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -28,12 +28,14 @@ static struct /*_sacn_global_config*/ { size_t fds; sacn_fd* fd; uint64_t last_announce; + uint32_t next_frame; } global_cfg = { .source_name = "MIDIMonster", .cid = {'M', 'I', 'D', 'I', 'M', 'o', 'n', 's', 't', 'e', 'r'}, .fds = 0, .fd = NULL, - .last_announce = 0 + .last_announce = 0, + .next_frame = 0 }; MM_PLUGIN_API int init(){ @@ -46,6 +48,7 @@ MM_PLUGIN_API int init(){ .handle = sacn_set, .process = sacn_handle, .start = sacn_start, + .interval = sacn_interval, .shutdown = sacn_shutdown }; @@ -63,6 +66,13 @@ MM_PLUGIN_API int init(){ return 0; } +static uint32_t sacn_interval(){ + if(global_cfg.next_frame){ + return global_cfg.next_frame; + } + return SACN_KEEPALIVE_INTERVAL; +} + static int sacn_listener(char* host, char* port, uint8_t flags){ int fd = -1, yes = 1; if(global_cfg.fds >= MAX_FDS){ @@ -87,7 +97,6 @@ static int sacn_listener(char* host, char* port, uint8_t flags){ global_cfg.fd[global_cfg.fds].fd = fd; global_cfg.fd[global_cfg.fds].universes = 0; global_cfg.fd[global_cfg.fds].universe = NULL; - global_cfg.fd[global_cfg.fds].last_frame = NULL; if(flags & mcast_loop){ //set IP_MCAST_LOOP to allow local applications to receive output @@ -190,6 +199,10 @@ static int sacn_configure_instance(instance* inst, char* option, char* value){ data->unicast_input = strtoul(value, NULL, 10); return 0; } + else if(!strcmp(option, "realtime")){ + data->realtime = strtoul(value, NULL, 10); + return 0; + } LOGPF("Unknown instance configuration option %s for instance %s", option, inst->name); return 1; @@ -289,10 +302,11 @@ static int sacn_transmit(instance* inst){ LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); } - //update last transmit timestamp + //update last transmit timestamp, unmark instance for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){ - if(global_cfg.fd[data->fd_index].universe[u] == data->uni){ - global_cfg.fd[data->fd_index].last_frame[u] = mm_timestamp(); + if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){ + global_cfg.fd[data->fd_index].universe[u].last_frame = mm_timestamp(); + global_cfg.fd[data->fd_index].universe[u].mark = 0; } } return 0; @@ -300,6 +314,7 @@ static int sacn_transmit(instance* inst){ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t u, mark = 0; + uint32_t frame_delta = 0; sacn_instance_data* data = (sacn_instance_data*) inst->impl; if(!num){ @@ -333,6 +348,25 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ //send packet if required if(mark){ + if(!data->realtime){ + //find output instance data + 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; + } + } + + frame_delta = mm_timestamp() - global_cfg.fd[data->fd_index].universe[u].last_frame; + + //check if ratelimiting engaged + if(frame_delta < SACN_FRAME_TIMEOUT){ + global_cfg.fd[data->fd_index].universe[u].mark = 1; + if(!global_cfg.next_frame || global_cfg.next_frame > (SACN_KEEPALIVE_INTERVAL - frame_delta)){ + global_cfg.next_frame = (SACN_KEEPALIVE_INTERVAL - frame_delta); + } + return 0; + } + } sacn_transmit(inst); } @@ -468,6 +502,7 @@ static void sacn_discovery(size_t fd){ static int sacn_handle(size_t num, managed_fd* fds){ size_t u, c; uint64_t timestamp = mm_timestamp(); + uint32_t synthesize_delta = 0; ssize_t bytes_read; char recv_buf[SACN_RECV_BUF]; instance* inst = NULL; @@ -477,7 +512,7 @@ static int sacn_handle(size_t num, managed_fd* fds){ sacn_frame_root* frame = (sacn_frame_root*) recv_buf; sacn_frame_data* data = (sacn_frame_data*) (recv_buf + sizeof(sacn_frame_root)); - if(mm_timestamp() - global_cfg.last_announce > SACN_DISCOVERY_TIMEOUT){ + if(timestamp - global_cfg.last_announce > SACN_DISCOVERY_TIMEOUT){ //send universe discovery pdu for(u = 0; u < global_cfg.fds; u++){ if(global_cfg.fd[u].universes){ @@ -487,17 +522,28 @@ static int sacn_handle(size_t num, managed_fd* fds){ global_cfg.last_announce = timestamp; } - //check for keepalive frames + //check for keepalive frames, synthesize frames if necessary for(u = 0; u < global_cfg.fds; u++){ for(c = 0; c < global_cfg.fd[u].universes; c++){ - if(timestamp - global_cfg.fd[u].last_frame[c] >= SACN_KEEPALIVE_INTERVAL){ + synthesize_delta = timestamp - global_cfg.fd[u].universe[c].last_frame; + + if((global_cfg.fd[u].universe[c].mark + && synthesize_delta >= SACN_FRAME_TIMEOUT + SACN_SYNTHESIZE_MARGIN) + || synthesize_delta >= SACN_KEEPALIVE_INTERVAL){ instance_id.fields.fd_index = u; - instance_id.fields.uni = global_cfg.fd[u].universe[c]; + instance_id.fields.uni = global_cfg.fd[u].universe[c].universe; inst = mm_instance_find(BACKEND_NAME, instance_id.label); if(inst){ sacn_transmit(inst); } } + + //update next frame request + if(global_cfg.fd[u].universe[c].mark + && (!global_cfg.next_frame || global_cfg.next_frame > SACN_FRAME_TIMEOUT + SACN_SYNTHESIZE_MARGIN - synthesize_delta)){ + global_cfg.next_frame = SACN_FRAME_TIMEOUT + SACN_SYNTHESIZE_MARGIN - synthesize_delta; + } + } } @@ -557,7 +603,6 @@ static int sacn_start(size_t n, instance** inst){ if(!global_cfg.fds){ LOG("Failed to start, no descriptors bound"); - free(inst); return 1; } @@ -590,13 +635,15 @@ static int sacn_start(size_t n, instance** inst){ if(data->xmit_prio){ //add to list of advertised universes for this fd - global_cfg.fd[data->fd_index].universe = realloc(global_cfg.fd[data->fd_index].universe, (global_cfg.fd[data->fd_index].universes + 1) * sizeof(uint16_t)); + global_cfg.fd[data->fd_index].universe = realloc(global_cfg.fd[data->fd_index].universe, (global_cfg.fd[data->fd_index].universes + 1) * sizeof(sacn_output_universe)); if(!global_cfg.fd[data->fd_index].universe){ LOG("Failed to allocate memory"); goto bail; } - global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes] = data->uni; + global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes].universe = data->uni; + global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes].last_frame = 0; + global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes].mark = 0; global_cfg.fd[data->fd_index].universes++; //generate multicast destination address if none set @@ -612,12 +659,6 @@ static int sacn_start(size_t n, instance** inst){ LOGPF("Registering %" PRIsize_t " descriptors to core", global_cfg.fds); for(u = 0; u < global_cfg.fds; u++){ - //allocate memory for storing last frame transmission timestamp - global_cfg.fd[u].last_frame = calloc(global_cfg.fd[u].universes, sizeof(uint64_t)); - if(!global_cfg.fd[u].last_frame){ - LOG("Failed to allocate memory"); - goto bail; - } if(mm_manage_fd(global_cfg.fd[u].fd, BACKEND_NAME, 1, (void*) u)){ goto bail; } @@ -638,7 +679,6 @@ static int sacn_shutdown(size_t n, instance** inst){ for(p = 0; p < global_cfg.fds; p++){ close(global_cfg.fd[p].fd); free(global_cfg.fd[p].universe); - free(global_cfg.fd[p].last_frame); } free(global_cfg.fd); LOG("Backend shut down"); diff --git a/backends/sacn.h b/backends/sacn.h index ac59441..4642e59 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -1,6 +1,7 @@ #include "midimonster.h" MM_PLUGIN_API int init(); +static uint32_t sacn_interval(); static int sacn_configure(char* option, char* value); static int sacn_configure_instance(instance* instance, char* option, char* value); static int sacn_instance(instance* inst); @@ -12,7 +13,11 @@ static int sacn_shutdown(size_t n, instance** inst); #define SACN_PORT "5568" #define SACN_RECV_BUF 8192 -#define SACN_KEEPALIVE_INTERVAL 2000 +//spec 6.6.2.1 +#define SACN_KEEPALIVE_INTERVAL 1000 +//spec 6.6.1 +#define SACN_FRAME_TIMEOUT 15 +#define SACN_SYNTHESIZE_MARGIN 10 #define SACN_DISCOVERY_TIMEOUT 9000 #define SACN_PDU_MAGIC "ASC-E1.17\0\0\0" @@ -35,6 +40,7 @@ typedef struct /*_sacn_universe_model*/ { typedef struct /*_sacn_instance_model*/ { uint16_t uni; + uint8_t realtime; uint8_t xmit_prio; uint8_t cid_filter[16]; uint8_t filter_enabled; @@ -54,11 +60,16 @@ typedef union /*_sacn_instance_id*/ { uint64_t label; } sacn_instance_id; +typedef struct /*_sacn_output_universe*/ { + uint16_t universe; + uint64_t last_frame; + uint8_t mark; +} sacn_output_universe; + typedef struct /*_sacn_socket*/ { int fd; size_t universes; - uint16_t* universe; - uint64_t* last_frame; + sacn_output_universe* universe; } sacn_fd; #pragma pack(push, 1) diff --git a/backends/sacn.md b/backends/sacn.md index f5f1db4..598f430 100644 --- a/backends/sacn.md +++ b/backends/sacn.md @@ -26,6 +26,7 @@ This has the side effect of mirroring the output of instances on those descripto | `destination` | `10.2.2.2` | Universe multicast | Destination address for unicast output. If unset, the multicast destination for the specified universe is used. | | `from` | `0xAA 0xBB` ... | none | 16-byte input source CID filter. Setting this option filters the input stream for this universe. | | `unicast` | `1` | `0` | Prevent this instance from joining its universe multicast group | +| `realtime` | `1` | `0` | Disable the recommended rate-limiting (approx. 44 packets per second) for this instance | Note that instances accepting multicast input also process unicast frames directed at them, while instances in `unicast` mode will not receive multicast frames. @@ -50,9 +51,6 @@ A normal channel that is part of a wide channel can not be mapped individually. The DMX start code of transmitted and received universes is fixed as `0`. -The (upper) limit on packet transmission rate mandated by section 6.6.1 of the sACN specification is disregarded. -The rate of packet transmission is influenced by the rate of incoming mapped events on the instance. - Universe synchronization is currently not supported, though this feature may be implemented in the future. To use multicast input, all networking hardware in the path must support the IGMPv2 protocol. -- cgit v1.2.3 From 1b3f7610d47ba5464e7aa2e16b5b1a27b7d0f5a3 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 6 Mar 2020 23:46:43 +0100 Subject: Implement openpixelcontrol broadcast (strip 0) input --- backends/openpixelcontrol.c | 191 +++++++++++++++++++++++++++++-------------- backends/openpixelcontrol.md | 3 + 2 files changed, 131 insertions(+), 63 deletions(-) (limited to 'backends') diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 62cdb68..ca395d3 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "openpixelcontrol" +#define DEBUG #include @@ -115,7 +116,7 @@ static ssize_t openpixel_buffer_find(openpixel_instance_data* data, uint8_t stri static int openpixel_buffer_extend(openpixel_instance_data* data, uint8_t strip, uint8_t input, uint16_t length){ ssize_t buffer = openpixel_buffer_find(data, strip, input); - + //length is in component-channels, round it to the nearest rgb-triplet //this guarantees that any allocated buffer has at least three bytes, which is important to parts of the receive handler length = (length % 3) ? ((length / 3) + 1) * 3 : length; @@ -340,12 +341,86 @@ static int openpixel_client_new(instance* inst, int fd){ return mm_manage_fd(fd, BACKEND_NAME, 1, inst); } -static ssize_t openpixel_client_pixeldata(instance* inst, openpixel_client* client, uint8_t* buffer, size_t bytes_left){ - openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; +static size_t openpixel_strip_pixeldata8(instance* inst, openpixel_client* client, uint8_t* data, openpixel_buffer* buffer, size_t bytes_left){ + channel* chan = NULL; + channel_value val; size_t u; + + for(u = 0; u < bytes_left; u++){ + //if over buffer length, ignore + if(u + client->offset >= buffer->bytes){ + client->buffer = -2; + break; + } + + //FIXME if at start of trailing non-multiple of 3, ignore + + //update changed channels + if(buffer->data.u8[u + client->offset] != data[u]){ + buffer->data.u8[u + client->offset] = data[u]; + chan = mm_channel(inst, ((uint64_t) buffer->strip << 32) | (u + client->offset + 1), 0); + if(chan){ + //push event + val.raw.u64 = data[u]; + val.normalised = (double) data[u] / 255.0; + if(mm_channel_event(chan, val)){ + LOG("Failed to push channel event to core"); + } + } + } + } + return u; +} + +static size_t openpixel_strip_pixeldata16(instance* inst, openpixel_client* client, uint8_t* data, openpixel_buffer* buffer, size_t bytes_left){ channel* chan = NULL; channel_value val; + size_t u; + for(u = 0; u < bytes_left; u++){ + //if over buffer length, ignore + if(u + client->offset >= buffer->bytes){ + client->buffer = -2; + break; + } + + //if at start of trailing non-multiple of 6, ignore + if((client->offset + u) >= (client->offset + client->left) - ((client->offset + client->left) % 6)){ + client->buffer = -2; + break; + } + + //byte-order conversion may be on message boundary, do it via a buffer + client->boundary.u8[(client->offset + u) % 2] = data[u]; + + //detect and update changed channels + if((client->offset + u) % 2 + && buffer->data.u16[(u + client->offset) / 2] != be16toh(client->boundary.u16)){ + buffer->data.u16[(u + client->offset) / 2] = be16toh(client->boundary.u16); + chan = mm_channel(inst, ((uint64_t) buffer->strip << 32) | ((u + client->offset) / 2 + 1), 0); + if(chan){ + //push event + val.raw.u64 = be16toh(client->boundary.u16);; + val.normalised = (double) val.raw.u64 / 65535.0; + if(mm_channel_event(chan, val)){ + LOG("Failed to push channel event to core"); + } + } + + } + } + return u; +} + +static ssize_t openpixel_client_pixeldata(instance* inst, openpixel_client* client, uint8_t* buffer, size_t bytes_left){ + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + openpixel_client temp_client = { + .fd = -1 + }; + ssize_t u, p; + uint8_t processing_done = 1; + + //ignore data if(client->buffer == -2){ //ignore data u = min(client->left, bytes_left); @@ -353,71 +428,55 @@ static ssize_t openpixel_client_pixeldata(instance* inst, openpixel_client* clie client->left -= u; return u; } - else{ - if(data->mode == rgb8){ - for(u = 0; u < bytes_left; u++){ - //if over buffer length, ignore - if(u + client->offset >= data->buffer[client->buffer].bytes){ - client->buffer = -2; - break; + //handle broadcast data + else if(client->buffer == -3){ + //iterate all input strips + for(p = 0; p < data->buffers; p++){ + if(data->buffer[p].flags & OPENPIXEL_INPUT){ + //prepare temporary client + temp_client.buffer = p; + temp_client.hdr = client->hdr; + temp_client.hdr.strip = data->buffer[p].strip; + temp_client.offset = client->offset; + temp_client.left = client->left; + + //run processing on strip + if(data->mode == rgb8){ + openpixel_strip_pixeldata8(inst, &temp_client, buffer, data->buffer + p, bytes_left); } - - //FIXME if at start of trailing non-multiple of 3, ignore - - //update changed channels - if(data->buffer[client->buffer].data.u8[u + client->offset] != buffer[u]){ - data->buffer[client->buffer].data.u8[u + client->offset] = buffer[u]; - chan = mm_channel(inst, ((uint64_t) client->hdr.strip << 32) | (u + client->offset + 1), 0); - if(chan){ - //push event - val.raw.u64 = buffer[u]; - val.normalised = (double) buffer[u] / 255.0; - if(mm_channel_event(chan, val)){ - LOG("Failed to push channel event to core"); - } - } + else{ + openpixel_strip_pixeldata16(inst, &temp_client, buffer, data->buffer + p, bytes_left); + } + if(temp_client.buffer != -2){ + processing_done = 0; } } - - //update offsets - client->offset += u; - client->left -= u; - return u; } - else{ - for(u = 0; u < bytes_left; u++){ - //if over buffer length, ignore - if(u + client->offset >= data->buffer[client->buffer].bytes){ - client->buffer = -2; - break; - } - - //if at start of trailing non-multiple of 6, ignore - if((client->offset + u) >= (client->offset + client->left) - ((client->offset + client->left) % 6)){ - client->buffer = -2; - break; - } - //byte-order conversion may be on message boundary, do it via a buffer - client->boundary.u8[(client->offset + u) % 2] = buffer[u]; - - //detect and update changed channels - if((client->offset + u) % 2 - && data->buffer[client->buffer].data.u16[(u + client->offset) / 2] != be16toh(client->boundary.u16)){ - data->buffer[client->buffer].data.u16[(u + client->offset) / 2] = be16toh(client->boundary.u16); - chan = mm_channel(inst, ((uint64_t) client->hdr.strip << 32) | ((u + client->offset) / 2 + 1), 0); - if(chan){ - //push event - val.raw.u64 = be16toh(client->boundary.u16);; - val.normalised = (double) val.raw.u64 / 65535.0; - if(mm_channel_event(chan, val)){ - LOG("Failed to push channel event to core"); - } - } + //if all strips report being done, ignore the rest of the data + if(processing_done){ + client->buffer = -2; + } - } - } + //remove data + u = min(client->left, bytes_left); + client->offset += u; + client->left -= u; + return u; + } + //process data + else{ + if(data->mode == rgb8){ + u = openpixel_strip_pixeldata8(inst, client, buffer, data->buffer + client->buffer, bytes_left); + } + else{ + u = openpixel_strip_pixeldata16(inst, client, buffer, data->buffer + client->buffer, bytes_left); } + + //update offsets + client->offset += u; + client->left -= u; + return u; } return -1; } @@ -431,8 +490,14 @@ static ssize_t openpixel_client_headerdata(instance* inst, openpixel_client* cli //if done, resolve buffer if(sizeof(openpixel_header) - client->offset <= bytes_left){ - client->buffer = openpixel_buffer_find(data, client->hdr.strip, 1); - //TODO handle broadcast strip input + //if broadcast strip, mark broadcast + if(client->hdr.strip == 0 + && data->mode == client->hdr.mode){ + client->buffer = -3; + } + else{ + client->buffer = openpixel_buffer_find(data, client->hdr.strip, 1); + } //if no buffer or mode mismatch, ignore data if(client->buffer < 0 || data->mode != client->hdr.mode){ diff --git a/backends/openpixelcontrol.md b/backends/openpixelcontrol.md index 5a8686f..d09d412 100644 --- a/backends/openpixelcontrol.md +++ b/backends/openpixelcontrol.md @@ -50,3 +50,6 @@ This behaviour may be changed in future releases. While acting as an OpenPixelControl server, the backend allows multiple clients to connect. This may lead to confusing data output when multiple clients are trying to control the same strip. + +When acting as a 16bit OpenPixelControl server, input on the broadcast strip (strip 0) may cause erratic +value events on a few channels, especially with longer strips and inputs. -- cgit v1.2.3 From 3a53a29df4c762a1ea3891be24615f74469b6be2 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 6 Mar 2020 23:47:03 +0100 Subject: Minor fixes --- backends/sacn.c | 1 + 1 file changed, 1 insertion(+) (limited to 'backends') diff --git a/backends/sacn.c b/backends/sacn.c index 6c08b5a..54cc7b5 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -523,6 +523,7 @@ static int sacn_handle(size_t num, managed_fd* fds){ } //check for keepalive frames, synthesize frames if necessary + global_cfg.next_frame = 0; for(u = 0; u < global_cfg.fds; u++){ for(c = 0; c < global_cfg.fd[u].universes; c++){ synthesize_delta = timestamp - global_cfg.fd[u].universe[c].last_frame; -- cgit v1.2.3 From d72d7cc6366f901eb9503bac613e3bc280ac8fc5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 7 Mar 2020 00:01:35 +0100 Subject: Whitespace fixes --- backends/artnet.c | 2 +- backends/openpixelcontrol.c | 1 - backends/sacn.c | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index f1b8c0e..9fac332 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -356,7 +356,7 @@ static int artnet_handle(size_t num, managed_fd* fds){ 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 + if((artnet_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); diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index ca395d3..d9d7d4b 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -1,5 +1,4 @@ #define BACKEND_NAME "openpixelcontrol" -#define DEBUG #include diff --git a/backends/sacn.c b/backends/sacn.c index 54cc7b5..79ffb46 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -223,7 +223,7 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ char* spec_next = spec; unsigned chan_a = strtoul(spec, &spec_next, 10), chan_b = 0; - + //range check if(!chan_a || chan_a > 512){ LOGPF("Channel out of range on instance %s: %s", inst->name, spec); @@ -540,7 +540,7 @@ static int sacn_handle(size_t num, managed_fd* fds){ } //update next frame request - if(global_cfg.fd[u].universe[c].mark + if(global_cfg.fd[u].universe[c].mark && (!global_cfg.next_frame || global_cfg.next_frame > SACN_FRAME_TIMEOUT + SACN_SYNTHESIZE_MARGIN - synthesize_delta)){ global_cfg.next_frame = SACN_FRAME_TIMEOUT + SACN_SYNTHESIZE_MARGIN - synthesize_delta; } -- cgit v1.2.3 From 413ff0c6eb83fc603f96f870769e50c6234bd171 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 7 Mar 2020 11:31:12 +0100 Subject: Fix openpixelcontrol edge case --- backends/openpixelcontrol.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'backends') diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index d9d7d4b..b5885b0 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "openpixelcontrol" +#define DEBUG #include @@ -422,10 +423,9 @@ static ssize_t openpixel_client_pixeldata(instance* inst, openpixel_client* clie //ignore data if(client->buffer == -2){ //ignore data - u = min(client->left, bytes_left); - client->offset += u; - client->left -= u; - return u; + client->offset += bytes_left; + client->left -= bytes_left; + return bytes_left; } //handle broadcast data else if(client->buffer == -3){ @@ -559,7 +559,7 @@ static int openpixel_client_handle(instance* inst, int fd){ } else{ //read data - bytes_handled = openpixel_client_pixeldata(inst, data->client + c, buffer + offset, bytes_left); + bytes_handled = openpixel_client_pixeldata(inst, data->client + c, buffer + offset, min(bytes_left, data->client[c].left)); if(bytes_handled < 0){ //FIXME handle errors } -- cgit v1.2.3 From dde2cca435b2c314a08024825b1daffa6437b947 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 7 Mar 2020 14:11:26 +0100 Subject: Update interval() documentation --- backends/lua.md | 2 +- backends/openpixelcontrol.c | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/lua.md b/backends/lua.md index ce82a4d..0c592b7 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -13,7 +13,7 @@ The following functions are provided within the Lua interpreter for interaction | Function | Usage example | Description | |-------------------------------|-------------------------------|---------------------------------------| | `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel | -| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms) | +| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function | | `input_value(string)` | `input_value("foo")` | Get the last input value on a channel | | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | | `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index b5885b0..168e077 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -1,5 +1,4 @@ #define BACKEND_NAME "openpixelcontrol" -#define DEBUG #include -- cgit v1.2.3 From f9829ae90d4017940047b561e412c6eb7f431adb Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Mar 2020 12:13:38 +0100 Subject: Implement timestamp() callback for Lua --- backends/lua.c | 16 +++++++++++----- backends/lua.md | 5 +++-- 2 files changed, 14 insertions(+), 7 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 955341a..9a8091e 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -75,7 +75,7 @@ static int lua_update_timerfd(){ size_t n = 0; #ifdef MMBACKEND_LUA_TIMERFD struct itimerspec timer_config = { - 0 + {0} }; #endif @@ -260,7 +260,7 @@ static int lua_callback_value(lua_State* interpreter, uint8_t input){ //find correct channel & return value for(n = 0; n < data->channels; n++){ if(!strcmp(channel_name, data->channel_name[n])){ - lua_pushnumber(data->interpreter, (input) ? data->input[n] : data->output[n]); + lua_pushnumber(interpreter, (input) ? data->input[n] : data->output[n]); return 1; } } @@ -283,6 +283,11 @@ static int lua_callback_input_channel(lua_State* interpreter){ return 1; } +static int lua_callback_timestamp(lua_State* interpreter){ + lua_pushnumber(interpreter, mm_timestamp()); + return 1; +} + static int lua_configure(char* option, char* value){ LOG("No backend configuration possible"); return 1; @@ -326,6 +331,7 @@ static int lua_instance(instance* inst){ lua_register(data->interpreter, "input_value", lua_callback_input_value); lua_register(data->interpreter, "output_value", lua_callback_output_value); lua_register(data->interpreter, "input_channel", lua_callback_input_channel); + lua_register(data->interpreter, "timestamp", lua_callback_timestamp); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -417,9 +423,6 @@ static int lua_handle(size_t num, managed_fd* fds){ return 1; } #else - if(!last_timestamp){ - last_timestamp = mm_timestamp(); - } delta = mm_timestamp() - last_timestamp; last_timestamp = mm_timestamp(); #endif @@ -458,6 +461,7 @@ static int lua_start(size_t n, instance** inst){ && strcmp(data->channel_name[p], "input_value") && strcmp(data->channel_name[p], "output_value") && strcmp(data->channel_name[p], "input_channel") + && strcmp(data->channel_name[p], "timestamp") && strcmp(data->channel_name[p], "interval")){ lua_getglobal(data->interpreter, data->channel_name[p]); data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); @@ -474,6 +478,8 @@ static int lua_start(size_t n, instance** inst){ if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ return 1; } + #else + last_timestamp = mm_timestamp(); #endif return 0; } diff --git a/backends/lua.md b/backends/lua.md index 0c592b7..db4cf39 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -17,6 +17,7 @@ The following functions are provided within the Lua interpreter for interaction | `input_value(string)` | `input_value("foo")` | Get the last input value on a channel | | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | | `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | +| `timestamp()` | `print(timestamp())` | Returns the core timestamp for this iteration with millisecond resolution. This is not a performance timer, but intended for timeouting, etc | Example script: ``` @@ -58,8 +59,8 @@ lua1.foo > lua2.bar #### Known bugs / problems -Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`, `input_channel`) -as an input channel name to a Lua instance will not call any handler functions. +Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`, `input_channel`, +`timestamp`) as an input channel name to a Lua instance will not call any handler functions. Using these names as arguments to the output and value interface functions works as intended. Output values will not trigger corresponding input event handlers unless the channel is mapped -- cgit v1.2.3 From 38d3724b2af3c2b08c548326797c2421b054c846 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Mar 2020 13:24:33 +0100 Subject: Implement python intervaling and socket management --- backends/python.c | 294 +++++++++++++++++++++++++++++++++++++++++++++++++++-- backends/python.h | 19 ++++ backends/python.md | 44 +++++++- 3 files changed, 346 insertions(+), 11 deletions(-) (limited to 'backends') diff --git a/backends/python.c b/backends/python.c index 9fad0ae..70c2548 100644 --- a/backends/python.c +++ b/backends/python.c @@ -7,13 +7,14 @@ #define MMPY_INSTANCE_KEY "midimonster_instance" -/* - * TODO might want to export the full MM_API set to python at some point - */ - static PyThreadState* python_main = NULL; static wchar_t* program_name = NULL; +static uint64_t last_timestamp = 0; +static uint32_t timer_interval = 0; +static size_t intervals = 0; +static mmpy_timer* interval = NULL; + MM_PLUGIN_API int init(){ backend python = { .name = BACKEND_NAME, @@ -24,6 +25,7 @@ MM_PLUGIN_API int init(){ .handle = python_set, .process = python_handle, .start = python_start, + .interval = python_interval, .shutdown = python_shutdown }; @@ -35,6 +37,57 @@ MM_PLUGIN_API int init(){ return 0; } +static uint32_t python_interval(){ + size_t u = 0; + uint32_t next_timer = 1000; + + if(timer_interval){ + for(u = 0; u < intervals; u++){ + if(interval[u].interval && + interval[u].interval - interval[u].delta < next_timer){ + next_timer = interval[u].interval - interval[u].delta; + } + } + DBGPF("Next timer fires in %" PRIu32, next_timer); + return next_timer; + } + + return 1000; +} + +static void python_timer_recalculate(){ + uint64_t next_interval = 0, gcd, residual; + size_t u; + + //find lower interval bounds + for(u = 0; u < intervals; u++){ + if(interval[u].interval && (!next_interval || interval[u].interval < next_interval)){ + next_interval = interval[u].interval; + } + } + + if(next_interval){ + for(u = 0; u < intervals; u++){ + if(interval[u].interval){ + //calculate gcd of current interval and this timers interval + gcd = interval[u].interval; + while(gcd){ + residual = next_interval % gcd; + next_interval = gcd; + gcd = residual; + } + + //10msec is absolute lower limit and minimum gcd due to rounding + if(next_interval == 10){ + break; + } + } + } + } + + timer_interval = next_interval; +} + static int python_configure(char* option, char* value){ LOG("No backend configuration possible"); return 1; @@ -64,7 +117,7 @@ static PyObject* mmpy_output(PyObject* self, PyObject* args){ const char* channel_name = NULL; channel* chan = NULL; channel_value val = { - 0 + {0} }; size_t u; @@ -137,6 +190,163 @@ static PyObject* mmpy_input_value(PyObject* self, PyObject* args){ return mmpy_channel_value(self, args, 1); } +static PyObject* mmpy_timestamp(PyObject* self, PyObject* args){ + return PyLong_FromUnsignedLong(mm_timestamp()); +} + +static PyObject* mmpy_interval(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + unsigned long updated_interval = 0; + PyObject* reference = NULL; + size_t u; + + if(!PyArg_ParseTuple(args, "Ok", &reference, &updated_interval)){ + return NULL; + } + + if(!PyCallable_Check(reference)){ + PyErr_SetString(PyExc_TypeError, "interval() requires a callable"); + return NULL; + } + + //round interval + if(updated_interval % 10 < 5){ + updated_interval -= updated_interval % 10; + } + else{ + updated_interval += (10 - (updated_interval % 10)); + } + + //find reference + for(u = 0; u < intervals; u++){ + if(interval[u].interpreter == data->interpreter + && PyObject_RichCompareBool(reference, interval[u].reference, Py_EQ) == 1){ + DBGPF("Updating interval to %" PRIu64 " msec", updated_interval); + break; + } + } + + //register new interval + if(u == intervals && updated_interval){ + //create new interval slot + DBGPF("Registering interval with %" PRIu64 " msec", updated_interval); + interval = realloc(interval, (intervals + 1) * sizeof(mmpy_timer)); + if(!interval){ + intervals = 0; + LOG("Failed to allocate memory"); + return NULL; + } + Py_INCREF(reference); + interval[intervals].delta = 0; + interval[intervals].reference = reference; + interval[intervals].interpreter = data->interpreter; + intervals++; + } + + //update if existing or created + if(u < intervals){ + interval[u].interval = updated_interval; + python_timer_recalculate(); + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + PyObject* handler = NULL, *sock = NULL, *fileno = NULL; + size_t u = 0, last_free = 0; + int fd = -1; + + if(!PyArg_ParseTuple(args, "OO", &handler, &sock)){ + return NULL; + } + + if(handler != Py_None && !PyCallable_Check(handler)){ + PyErr_SetString(PyExc_TypeError, "manage() requires either None or a callable"); + return NULL; + } + + fileno = PyObject_CallMethod(sock, "fileno", NULL); + if(!fileno || fileno == Py_None || !PyLong_Check(fileno)){ + PyErr_SetString(PyExc_TypeError, "manage() requires a socket-like object"); + return NULL; + } + + fd = PyLong_AsLong(fileno); + if(fd < 0){ + PyErr_SetString(PyExc_TypeError, "manage() requires a (connected) socket-like object"); + return NULL; + } + + //check if this socket instance was already registered + last_free = data->sockets; + for(u = 0; u < data->sockets; u++){ + if(!data->socket[u].socket){ + last_free = u; + } + else if(PyObject_RichCompareBool(sock, data->socket[u].socket, Py_EQ) == 1){ + break; + } + } + + if(u < data->sockets){ + //modify existing socket + Py_XDECREF(data->socket[u].handler); + if(handler != Py_None){ + DBGPF("Updating handler for fd %d on %s", fd, inst->name); + data->socket[u].handler = handler; + Py_INCREF(handler); + } + else{ + DBGPF("Unregistering fd %d on %s", fd, inst->name); + mm_manage_fd(data->socket[u].fd, BACKEND_NAME, 0, NULL); + Py_XDECREF(data->socket[u].socket); + data->socket[u].handler = NULL; + data->socket[u].socket = NULL; + data->socket[u].fd = -1; + } + } + else if(handler != Py_None){ + //check that the fd is not already registered with another socket instance + for(u = 0; u < data->sockets; u++){ + if(data->socket[u].fd == fd){ + //FIXME this might also raise an exception + LOGPF("Descriptor already registered with another socket on instance %s", inst->name); + Py_INCREF(Py_None); + return Py_None; + } + } + + DBGPF("Registering new fd %d on %s", fd, inst->name); + if(last_free == data->sockets){ + //allocate a new socket instance + data->socket = realloc(data->socket, (data->sockets + 1) * sizeof(mmpy_socket)); + if(!data->socket){ + data->sockets = 0; + LOG("Failed to allocate memory"); + return NULL; + } + data->sockets++; + } + + //store new reference + //FIXME check this for errors + mm_manage_fd(fd, BACKEND_NAME, 1, inst); + data->socket[last_free].fd = fd; + Py_INCREF(handler); + data->socket[last_free].handler = handler; + Py_INCREF(sock); + data->socket[last_free].socket = sock; + } + + Py_INCREF(Py_None); + return Py_None; +} + static int mmpy_exec(PyObject* module) { instance** inst = (instance**) PyModule_GetState(module); //FIXME actually use interpreter dict (from python 3.8) here at some point @@ -146,7 +356,7 @@ static int mmpy_exec(PyObject* module) { return 0; } - //TODO raise exception + PyErr_SetString(PyExc_AssertionError, "Failed to pass instance pointer for initialization"); return -1; } @@ -184,6 +394,9 @@ static PyObject* mmpy_init(){ {"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel"}, {"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel"}, {"current", mmpy_current_handler, METH_VARARGS, "Get the name of the currently executing channel handler"}, + {"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"}, {0} }; @@ -324,12 +537,66 @@ static int python_set(instance* inst, size_t num, channel** c, channel_value* v) } } + //release interpreter PyEval_ReleaseThread(data->interpreter); return 0; } static int python_handle(size_t num, managed_fd* fds){ - //TODO implement some kind of intervaling functionality before people get it in their heads to start `import threading` + instance* inst = NULL; + python_instance_data* data = NULL; + PyObject* result = NULL; + size_t u, p; + + //handle intervals + if(timer_interval){ + uint64_t delta = mm_timestamp() - last_timestamp; + last_timestamp = mm_timestamp(); + + //add delta to all active timers + for(u = 0; u < intervals; u++){ + if(interval[u].interval){ + interval[u].delta += delta; + + //if timer expired, call handler + if(interval[u].delta >= interval[u].interval){ + interval[u].delta %= interval[u].interval; + + //swap to interpreter + PyEval_RestoreThread(interval[u].interpreter); + //call handler + result = PyObject_CallFunction(interval[u].reference, NULL); + Py_XDECREF(result); + //release interpreter + PyEval_ReleaseThread(interval[u].interpreter); + DBGPF("Calling interval handler %" PRIsize_t, u); + } + } + } + } + + for(u = 0; u < num; u++){ + inst = (instance*) fds[u].impl; + data = (python_instance_data*) inst->impl; + + //swap to interpreter + PyEval_RestoreThread(data->interpreter); + + //handle callbacks + for(p = 0; p < data->sockets; p++){ + if(data->socket[p].socket + && data->socket[p].fd == fds[u].fd){ + //FIXME maybe close/unregister the socket on handling errors + DBGPF("Calling descriptor handler on %s for fd %d", inst->name, data->socket[p].fd); + result = PyObject_CallFunction(data->socket[p].handler, "O", data->socket[p].socket); + Py_XDECREF(result); + } + } + + //release interpreter + PyEval_ReleaseThread(data->interpreter); + } + return 0; } @@ -396,6 +663,19 @@ static int python_shutdown(size_t n, instance** inst){ for(u = 0; u < n; u++){ data = (python_instance_data*) inst[u]->impl; + + //close sockets + for(p = 0; p < data->sockets; p++){ + close(data->socket[p].fd); //FIXME does python do this on its own? + Py_XDECREF(data->socket[p].socket); + Py_XDECREF(data->socket[p].handler); + } + + //release interval references + for(p = 0; p name); //swap to interpreter and end it, GIL is held after this but state is NULL PyThreadState_Swap(data->interpreter); diff --git a/backends/python.h b/backends/python.h index 10411ca..8ca12f9 100644 --- a/backends/python.h +++ b/backends/python.h @@ -1,6 +1,7 @@ #include "midimonster.h" MM_PLUGIN_API int init(); +static uint32_t python_interval(); static int python_configure(char* option, char* value); static int python_configure_instance(instance* inst, char* option, char* value); static int python_instance(instance* inst); @@ -17,8 +18,26 @@ typedef struct /*_python_channel_data*/ { double out; } mmpython_channel; +typedef struct /*_mmpy_registered_socket*/ { + int fd; + PyObject* handler; + PyObject* socket; +} mmpy_socket; + +typedef struct /*_mmpy_interval*/ { + uint64_t interval; + uint64_t delta; + PyObject* reference; + PyThreadState* interpreter; +} mmpy_timer; + typedef struct /*_python_instance_data*/ { PyThreadState* interpreter; + PyObject* config; //TODO + + size_t sockets; + mmpy_socket* socket; + size_t channels; mmpython_channel* channel; mmpython_channel* current_channel; diff --git a/backends/python.md b/backends/python.md index fae3139..b6a1162 100644 --- a/backends/python.md +++ b/backends/python.md @@ -7,6 +7,9 @@ Every instance has its own interpreter, which can be loaded with multiple Python These modules may contain member functions accepting a single `float` parameter, which can then be used as target channels. For each incoming event, the handler function is called. +Python modules may also register `socket` objects (and an associated callback function) with +the MIDIMonster core, which will then alert the module when there is data ready to be read. + To interact with the MIDIMonster core, import the `midimonster` module from within your module. The `midimonster` module provides the following functions: @@ -17,17 +20,47 @@ The `midimonster` module provides the following functions: | `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel | | `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel | | `current()` | `print(midimonster.current())` | Returns the name of the input channel whose handler function is currently running or `None` if the interpreter was called from another context | +| `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 | Example Python module: ``` +import socket import midimonster +# Simple channel ahndler def in1(value): midimonster.output("out1", 1 - value) + +# Socket data handler +def socket_handler(sock): + # This should get some more error handling + data = sock.recv(1024) + print("Received %d bytes from socket: %s" % (len(data), data)) + if(len(data) == 0): + # Unmanage the socket if it has been closed + midimonster.manage(None, sock) + sock.close() + +# Interval handler +def ping(): + print(midimonster.interval()) + +# Register an interval +midimonster.interval(ping, 1000) +# Create and register a client socket (add error handling as you like) +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.connect(("localhost", 8990)) +midimonster.manage(reader, s) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. +Note that registered sockets that have been closed (`socket.recv()` returned 0 bytes) +need to be unregistered from the MIDIMonster core, otherwise the core socket multiplexing +mechanism will report an error and shut down the MIDIMonster. + #### Global configuration The `python` backend does not take any global configuration. @@ -49,7 +82,7 @@ specify the channel as the functions qualified path (by prefixing it with the mo Example mappings: ``` py1.my_handlers.in1 < py1.foo -py1.out1 > py2.module.handler +py1.out1 > py2.module.handler ``` #### Known bugs / problems @@ -57,7 +90,10 @@ py1.out1 > py2.module.handler Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. This is intentional. -Importing a Python module named `midimonster` may cause problems and is unsupported. +Importing a Python module named `midimonster` is probably a bad idea and thus unsupported. + +The MIDIMonster is, at its core, single-threaded. Do not try to use Python's `threading` +module with the MIDIMonster. -There is currently no functionality for cyclic execution. This may be implemented in a future -release. +Note that executing Python code blocks the MIDIMonster core. It is not a good idea to call functions that +take a long time to complete (such as `time.sleep()`) within your Python modules. -- cgit v1.2.3 From 87d7b5a81b8e8f05b4cb8cc79e38d33395b2189f Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Mar 2020 13:29:07 +0100 Subject: Enable syntax highlighting, fix some errors --- backends/python.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'backends') diff --git a/backends/python.md b/backends/python.md index b6a1162..8589c18 100644 --- a/backends/python.md +++ b/backends/python.md @@ -25,7 +25,7 @@ The `midimonster` module provides the following functions: | `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 | Example Python module: -``` +```python import socket import midimonster @@ -45,14 +45,14 @@ def socket_handler(sock): # Interval handler def ping(): - print(midimonster.interval()) + print(midimonster.timestamp()) # Register an interval midimonster.interval(ping, 1000) # Create and register a client socket (add error handling as you like) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("localhost", 8990)) -midimonster.manage(reader, s) +midimonster.manage(socket_handler, s) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. -- cgit v1.2.3 From 339c7eaf6f87509d500857d33a82587f5554b2b5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Mar 2020 13:44:45 +0100 Subject: Fix typo --- backends/python.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/python.md b/backends/python.md index 8589c18..f06e504 100644 --- a/backends/python.md +++ b/backends/python.md @@ -29,7 +29,7 @@ Example Python module: import socket import midimonster -# Simple channel ahndler +# Simple channel handler def in1(value): midimonster.output("out1", 1 - value) -- cgit v1.2.3 From 5f4b349aff49be0a5f6895631a93c47fcafcff93 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Mar 2020 14:26:50 +0100 Subject: Add lua debug logging --- backends/lua.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 9a8091e..e7ba9f9 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -64,6 +64,7 @@ static uint32_t lua_interval(){ next_timer = timer[n].interval - timer[n].delta; } } + DBGPF("Next timer fires in %" PRIu32, next_timer); return next_timer; } return 1000; @@ -85,6 +86,7 @@ static int lua_update_timerfd(){ interval = timer[n].interval; } } + DBGPF("Recalculating timers, minimum is %" PRIu64, interval); //calculate gcd of all timers if any are active if(interval){ @@ -111,11 +113,13 @@ static int lua_update_timerfd(){ } if(interval == timer_interval){ + DBGPF("Keeping interval at %" PRIu64, interval); return 0; } #ifdef MMBACKEND_LUA_TIMERFD - //configure the new interval + //configure the new interval, 0.0 disarms the timer + DBGPF("Reconfiguring timerfd to %" PRIu64 ".%" PRIu64, timer_config.it_interval.tv_sec, timer_config.it_interval.tv_nsec); timerfd_settime(timer_fd, 0, &timer_config, NULL); #endif timer_interval = interval; -- cgit v1.2.3