From 9718e10c7f4151cea895f515c785c14e0021d967 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 18 Mar 2020 22:36:35 +0100 Subject: Implement default channel handlers for Lua/Python --- backends/python.c | 65 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 20 deletions(-) (limited to 'backends/python.c') diff --git a/backends/python.c b/backends/python.c index 70c2548..735e838 100644 --- a/backends/python.c +++ b/backends/python.c @@ -378,6 +378,11 @@ static int python_configure_instance(instance* inst, char* option, char* value){ PyEval_ReleaseThread(data->interpreter); return 0; } + else if(!strcmp(option, "default-handler")){ + free(data->default_handler); + data->default_handler = strdup(value); + return 0; + } LOGPF("Unknown instance parameter %s for instance %s", option, inst->name); return 1; @@ -600,38 +605,56 @@ static int python_handle(size_t num, managed_fd* fds){ return 0; } +static PyObject* python_resolve_symbol(char* spec_raw){ + char* module_name = NULL, *object_name = NULL, *spec = strdup(spec_raw); + PyObject* module = NULL, *result = NULL; + + module = PyImport_AddModule("__main__"); + object_name = spec; + module_name = strchr(object_name, '.'); + if(module_name){ + *module_name = 0; + //returns borrowed reference + module = PyImport_AddModule(object_name); + + if(!module){ + LOGPF("Module %s for symbol %s.%s is not loaded", object_name, object_name, module_name + 1); + return NULL; + } + + object_name = module_name + 1; + + //returns new reference + result = PyObject_GetAttrString(module, object_name); + } + + free(spec); + return result; +} + 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; + DBGPF("Starting up instance %s", inst[u]->name); //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; - } + if(data->default_handler){ + data->handler = python_resolve_symbol(data->default_handler); + } - //returns new reference - data->channel[p].handler = PyObject_GetAttrString(module, channel_name); + for(p = 0; p < data->channels; p++){ + if(!strchr(data->channel[p].name, '.') && data->handler){ + data->channel[p].handler = data->handler; + } + else{ + data->channel[p].handler = python_resolve_symbol(data->channel[p].name); + } } //release interpreter @@ -654,6 +677,7 @@ static int python_shutdown(size_t n, instance** inst){ Py_XDECREF(data->channel[p].handler); } free(data->channel); + free(data->default_handler); //do not free data here, needed for shutting down interpreters } @@ -675,6 +699,7 @@ static int python_shutdown(size_t n, instance** inst){ for(p = 0; p handler); 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 -- cgit v1.2.3 From 03a75ea3b4e72f09843bc8b12f8bac84a2f27709 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 20 Mar 2020 23:35:53 +0100 Subject: Restructure lua channel resolution --- backends/lua.c | 105 +++++++++++++++++++++++++++++------------------------- backends/lua.h | 14 +++++--- backends/python.c | 3 +- 3 files changed, 68 insertions(+), 54 deletions(-) (limited to 'backends/python.c') diff --git a/backends/lua.c b/backends/lua.c index b66a27a..b71ca97 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -245,13 +245,13 @@ static int lua_callback_output(lua_State* interpreter){ //find correct channel & output value for(n = 0; n < data->channels; n++){ - if(!strcmp(channel_name, data->channel_name[n])){ + if(!strcmp(channel_name, data->channel[n].name)){ channel = mm_channel(inst, n, 0); if(!channel){ return 0; } mm_channel_event(channel, val); - data->output[n] = val.normalised; + data->channel[n].out = val.normalised; return 0; } } @@ -357,8 +357,8 @@ 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(interpreter, (input) ? data->input[n] : data->output[n]); + if(!strcmp(channel_name, data->channel[n].name)){ + lua_pushnumber(interpreter, (input) ? data->channel[n].in : data->channel[n].out); return 1; } } @@ -453,26 +453,23 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ //find matching channel for(u = 0; u < data->channels; u++){ - if(!strcmp(spec, data->channel_name[u])){ + if(!strcmp(spec, data->channel[u].name)){ break; } } //allocate new channel if(u == data->channels){ - data->channel_name = realloc(data->channel_name, (u + 1) * sizeof(char*)); - data->reference = realloc(data->reference, (u + 1) * sizeof(int)); - data->input = realloc(data->input, (u + 1) * sizeof(double)); - data->output = realloc(data->output, (u + 1) * sizeof(double)); - if(!data->channel_name || !data->reference || !data->input || !data->output){ + data->channel = realloc(data->channel, (data->channels + 1) * sizeof(lua_channel_data)); + if(!data->channel){ LOG("Failed to allocate memory"); + data->channels = 0; return NULL; } - data->reference[u] = LUA_NOREF; - data->input[u] = data->output[u] = 0.0; - data->channel_name[u] = strdup(spec); - if(!data->channel_name[u]){ + data->channel[u].in = data->channel[u].out = 0.0; + data->channel[u].name = strdup(spec); + if(!data->channel[u].name){ LOG("Failed to allocate memory"); return NULL; } @@ -483,23 +480,24 @@ 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){ - size_t n = 0; + size_t n = 0, ident; lua_instance_data* data = (lua_instance_data*) inst->impl; //handle all incoming events for(n = 0; n < num; n++){ - data->input[c[n]->ident] = v[n].normalised; + ident = c[n]->ident; + data->channel[ident].in = v[n].normalised; //call lua channel handlers if present - if(data->reference[c[n]->ident] != LUA_NOREF){ + if(data->channel[ident].reference != 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_pushstring(data->interpreter, data->channel[ident].name); lua_settable(data->interpreter, LUA_REGISTRYINDEX); - lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]); + lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->channel[ident].reference); lua_pushnumber(data->interpreter, v[n].normalised); if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ - LOGPF("Failed to call handler for %s.%s: %s", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1)); + LOGPF("Failed to call handler for %s.%s: %s", inst->name, data->channel[ident].name, lua_tostring(data->interpreter, -1)); lua_pop(data->interpreter, 1); } } @@ -564,37 +562,51 @@ static int lua_handle(size_t num, managed_fd* fds){ return 0; } +static int lua_resolve_symbol(lua_State* interpreter, char* symbol){ + int reference = LUA_REFNIL; + + //exclude reserved names + if(!strcmp(symbol, "output") + || !strcmp(symbol, "thread") + || !strcmp(symbol, "sleep") + || !strcmp(symbol, "input_value") + || !strcmp(symbol, "output_value") + || !strcmp(symbol, "input_channel") + || !strcmp(symbol, "timestamp") + || !strcmp(symbol, "interval")){ + return LUA_NOREF; + } + + lua_getglobal(interpreter, symbol); + reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + if(reference == LUA_REFNIL){ + return LUA_NOREF; + } + return reference; +} + static int lua_start(size_t n, instance** inst){ size_t u, p; lua_instance_data* data = NULL; + int default_handler; //resolve channels to their handler functions for(u = 0; u < n; u++){ data = (lua_instance_data*) inst[u]->impl; - for(p = 0; p < data->channels; p++){ - //exclude reserved names - if(!data->default_handler - && strcmp(data->channel_name[p], "output") - && strcmp(data->channel_name[p], "thread") - && strcmp(data->channel_name[p], "sleep") - && 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); - if(data->reference[p] == LUA_REFNIL){ - data->reference[p] = LUA_NOREF; - } + default_handler = LUA_NOREF; + + //try to resolve default handler if given + if(data->default_handler){ + default_handler = lua_resolve_symbol(data->interpreter, data->default_handler); + if(default_handler == LUA_NOREF){ + LOGPF("Failed to resolve default handler %s on %s", data->default_handler, inst[u]->name); } - else if(data->default_handler){ - lua_getglobal(data->interpreter, data->default_handler); - data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); - if(data->reference[p] == LUA_REFNIL){ - data->reference[p] = LUA_NOREF; - LOGPF("Failed to resolve default handler function %s on instance %s", data->default_handler, inst[u]->name); - } + } + + for(p = 0; p < data->channels; p++){ + data->channel[p].reference = default_handler; + if(!data->default_handler){ + data->channel[p].reference = lua_resolve_symbol(data->interpreter, data->channel[p].name); } } } @@ -621,12 +633,9 @@ static int lua_shutdown(size_t n, instance** inst){ lua_close(data->interpreter); //cleanup channel data for(p = 0; p < data->channels; p++){ - free(data->channel_name[p]); + free(data->channel[p].name); } - free(data->channel_name); - free(data->reference); - free(data->input); - free(data->output); + free(data->channel); free(data->default_handler); free(inst[u]->impl); } diff --git a/backends/lua.h b/backends/lua.h index d8e720a..3c7568b 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -22,14 +22,18 @@ static int lua_shutdown(size_t n, instance** inst); static uint32_t lua_interval(); #endif +typedef struct /*_lua_channel*/ { + char* name; + int reference; + double in; + double out; +} lua_channel_data; + typedef struct /*_lua_instance_data*/ { size_t channels; - char** channel_name; - int* reference; - double* input; - double* output; - lua_State* interpreter; + lua_channel_data* channel; + lua_State* interpreter; char* default_handler; } lua_instance_data; diff --git a/backends/python.c b/backends/python.c index 735e838..c658f30 100644 --- a/backends/python.c +++ b/backends/python.c @@ -78,7 +78,8 @@ static void python_timer_recalculate(){ } //10msec is absolute lower limit and minimum gcd due to rounding - if(next_interval == 10){ + if(next_interval <= 10){ + next_interval = 10; break; } } -- cgit v1.2.3 From aa02ccf3abf183207b24fcbb9460cbd904a698e2 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 22 Mar 2020 00:30:13 +0100 Subject: Enable load-time channel setting for python --- backends/python.c | 26 +++++++++++++++++--------- backends/python.h | 1 + 2 files changed, 18 insertions(+), 9 deletions(-) (limited to 'backends/python.c') diff --git a/backends/python.c b/backends/python.c index c658f30..9f1d642 100644 --- a/backends/python.c +++ b/backends/python.c @@ -116,7 +116,6 @@ 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} }; @@ -127,19 +126,22 @@ static PyObject* mmpy_output(PyObject* self, PyObject* args){ } val.normalised = clamp(val.normalised, 1.0, 0.0); + //if not started yet, create any requested channels so we can set them at load time + if(!last_timestamp){ + python_channel(inst, (char*) channel_name, mmchannel_output); + } 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(!last_timestamp){ + data->channel[u].mark = 1; + } + else{ + mm_channel_event(mm_channel(inst, u, 0), val); + } + return 0; } } @@ -636,6 +638,7 @@ static PyObject* python_resolve_symbol(char* spec_raw){ static int python_start(size_t n, instance** inst){ python_instance_data* data = NULL; size_t u, p; + channel_value v; //resolve channel references to handler functions for(u = 0; u < n; u++){ @@ -656,6 +659,11 @@ static int python_start(size_t n, instance** inst){ else{ data->channel[p].handler = python_resolve_symbol(data->channel[p].name); } + //push initial values + if(data->channel[p].mark){ + v.normalised = data->channel[p].out; + mm_channel_event(mm_channel(inst[u], p, 0), v); + } } //release interpreter diff --git a/backends/python.h b/backends/python.h index a40098b..020aeac 100644 --- a/backends/python.h +++ b/backends/python.h @@ -16,6 +16,7 @@ typedef struct /*_python_channel_data*/ { PyObject* handler; double in; double out; + uint8_t mark; } mmpython_channel; typedef struct /*_mmpy_registered_socket*/ { -- cgit v1.2.3 From 253125ea28925e5207c375987ac36468327bed66 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 27 Mar 2020 21:40:45 +0100 Subject: Implement python cleanup handlers --- backends/lua.c | 9 +++-- backends/lua.md | 3 +- backends/python.c | 99 ++++++++++++++++++++++++++++++++++++------------------ backends/python.h | 1 + backends/python.md | 16 ++++++--- 5 files changed, 87 insertions(+), 41 deletions(-) (limited to 'backends/python.c') diff --git a/backends/lua.c b/backends/lua.c index 7424f65..127933a 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -274,10 +274,13 @@ static int lua_callback_cleanup_handler(lua_State* interpreter){ return 0; } - luaL_checktype(interpreter, 1, LUA_TFUNCTION); + if(lua_type(interpreter, 1) != LUA_TFUNCTION && lua_type(interpreter, 1) != LUA_TNIL){ + LOG("Cleanup handler function parameter was neither nil nor a function"); + return 0; + } data->cleanup_handler = luaL_ref(interpreter, LUA_REGISTRYINDEX); - if(current_handler == LUA_NOREF){ + if(current_handler == LUA_NOREF || current_handler == LUA_REFNIL){ lua_pushnil(interpreter); return 1; } @@ -656,7 +659,7 @@ static int lua_shutdown(size_t n, instance** inst){ data = (lua_instance_data*) inst[u]->impl; //call cleanup function if one is registered - if(data->cleanup_handler != LUA_NOREF){ + if(data->cleanup_handler != LUA_NOREF && data->cleanup_handler != LUA_REFNIL){ lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->cleanup_handler); lua_pcall(data->interpreter, 0, 0, 0); } diff --git a/backends/lua.md b/backends/lua.md index 30d7580..e59e513 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -15,13 +15,13 @@ The following functions are provided within the Lua interpreter for interaction |-------------------------------|-------------------------------|---------------------------------------| | `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel on this instance | | `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 | +| `cleanup_handler(function)` | `cleanup_handler(shutdown)` | Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | | `input_value(string)` | `input_value("foo")` | Get the last input value on a channel on this instance | | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel on this instance | | `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 | | `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) | | `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds | -| `cleanup_handler(function)` | | Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | Example script: ```lua @@ -45,6 +45,7 @@ function run_show() end function save_values() + -- Store state to a file, for example end interval(toggle, 1000) diff --git a/backends/python.c b/backends/python.c index 9f1d642..4c9248d 100644 --- a/backends/python.c +++ b/backends/python.c @@ -257,6 +257,33 @@ static PyObject* mmpy_interval(PyObject* self, PyObject* args){ return Py_None; } +static PyObject* mmpy_cleanup_handler(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + PyObject* current_handler = data->cleanup_handler; + + if(!PyArg_ParseTuple(args, "O", &(data->cleanup_handler)) + || (data->cleanup_handler != Py_None && !PyCallable_Check(data->cleanup_handler))){ + data->cleanup_handler = current_handler; + return NULL; + } + + if(data->cleanup_handler == Py_None){ + data->cleanup_handler = NULL; + } + else{ + Py_INCREF(data->cleanup_handler); + } + + if(!current_handler){ + Py_INCREF(Py_None); + return Py_None; + } + + Py_DECREF(current_handler); + return current_handler; +} + static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){ instance* inst = *((instance**) PyModule_GetState(self)); python_instance_data* data = (python_instance_data*) inst->impl; @@ -264,12 +291,10 @@ static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){ 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"); + if(!PyArg_ParseTuple(args, "OO", &handler, &sock) + || sock == Py_None + || (handler != Py_None && !PyCallable_Check(handler))){ + PyErr_SetString(PyExc_TypeError, "manage() requires either None or a callable and a socket-like object"); return NULL; } @@ -398,13 +423,14 @@ static PyObject* mmpy_init(){ }; 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"}, + {"output", mmpy_output, METH_VARARGS, "Output a channel event on the instance"}, + {"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel on the instance"}, + {"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel on the instance"}, {"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"}, + {"cleanup_handler", mmpy_cleanup_handler, METH_VARARGS, "Register or update the instances cleanup handler"}, {0} }; @@ -674,29 +700,44 @@ static int python_start(size_t n, instance** inst){ static int python_shutdown(size_t n, instance** inst){ size_t u, p; + PyObject* result = NULL; 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); + //if there are no instances, the python interpreter is not started, so cleanup can be skipped + if(python_main){ + //release interval references + for(p = 0; p < intervals; p++){ + //swap to interpreter + PyEval_RestoreThread(interval[p].interpreter); + Py_XDECREF(interval[p].reference); + PyEval_ReleaseThread(interval[p].interpreter); } - free(data->channel); - free(data->default_handler); - //do not free data here, needed for shutting down interpreters - } - if(python_main){ - //just used to lock the GIL + //lock the GIL for later interpreter release PyEval_RestoreThread(python_main); for(u = 0; u < n; u++){ data = (python_instance_data*) inst[u]->impl; + //swap to interpreter to be safe for releasing the references + PyThreadState_Swap(data->interpreter); + + //run cleanup handler before cleaning up channel data to allow reading channel data + if(data->cleanup_handler){ + result = PyObject_CallFunction(data->cleanup_handler, NULL); + Py_XDECREF(result); + Py_XDECREF(data->cleanup_handler); + } + + //clean up channels + for(p = 0; p < data->channels; p++){ + free(data->channel[p].name); + Py_XDECREF(data->channel[p].handler); + } + free(data->channel); + free(data->default_handler); + Py_XDECREF(data->handler); + //close sockets for(p = 0; p < data->sockets; p++){ close(data->socket[p].fd); //FIXME does python do this on its own? @@ -704,26 +745,18 @@ static int python_shutdown(size_t n, instance** inst){ Py_XDECREF(data->socket[p].handler); } - //release interval references - for(p = 0; p handler); - + //shut down interpreter, GIL is held after this but state is NULL 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"); + LOG("Failed to shut down python library"); } PyMem_RawFree(program_name); } diff --git a/backends/python.h b/backends/python.h index 020aeac..539389b 100644 --- a/backends/python.h +++ b/backends/python.h @@ -45,4 +45,5 @@ typedef struct /*_python_instance_data*/ { char* default_handler; PyObject* handler; + PyObject* cleanup_handler; } python_instance_data; diff --git a/backends/python.md b/backends/python.md index 6852a79..ab0fb38 100644 --- a/backends/python.md +++ b/backends/python.md @@ -17,13 +17,14 @@ 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 | +| `output(string, float)` | `midimonster.output("foo", 0.75)` | Output a value event to a channel on this instance | +| `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel of this instance | +| `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel of this instance | | `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 | +| `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 | +| `cleanup_handler(function)` | `midimonster.cleanup_handler(save_all)`| Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | Example Python module: ```python @@ -48,12 +49,16 @@ def socket_handler(sock): def ping(): print(midimonster.timestamp()) +def save_positions(): + # Store some data to disk + # 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(socket_handler, s) +midimonster.cleanup_handler(save_positions) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. @@ -92,6 +97,9 @@ 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. +Output events generated from cleanup handlers called during shutdown will not be routed, as the core +routing facility has already shut down at this point. There are no plans to change this behaviour. + 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` -- cgit v1.2.3 From 5fdb638454ce90eb565555dfece5030a2ec4a576 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 28 Mar 2020 01:09:58 +0100 Subject: Fix reference counting bug and lua timing --- backends/lua.c | 6 ++---- backends/python.c | 7 +++++-- 2 files changed, 7 insertions(+), 6 deletions(-) (limited to 'backends/python.c') diff --git a/backends/lua.c b/backends/lua.c index 127933a..e2f3b0e 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -533,7 +533,8 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ } static int lua_handle(size_t num, managed_fd* fds){ - uint64_t delta = timer_interval; + uint64_t delta = mm_timestamp() - last_timestamp; + last_timestamp = mm_timestamp(); size_t n; #ifdef MMBACKEND_LUA_TIMERFD @@ -547,9 +548,6 @@ static int lua_handle(size_t num, managed_fd* fds){ LOGPF("Failed to read timer: %s", strerror(errno)); return 1; } - #else - delta = mm_timestamp() - last_timestamp; - last_timestamp = mm_timestamp(); #endif //no timers active diff --git a/backends/python.c b/backends/python.c index 4c9248d..94f8e24 100644 --- a/backends/python.c +++ b/backends/python.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "python" +#define DEBUG #define PY_SSIZE_T_CLEAN #include @@ -269,9 +270,11 @@ static PyObject* mmpy_cleanup_handler(PyObject* self, PyObject* args){ } if(data->cleanup_handler == Py_None){ + DBGPF("Cleanup handler removed on %s (previously %s)", inst->name, current_handler ? "active" : "inactive"); data->cleanup_handler = NULL; } else{ + DBGPF("Cleanup handler installed on %s (previously %s)", inst->name, current_handler ? "active" : "inactive"); Py_INCREF(data->cleanup_handler); } @@ -280,7 +283,7 @@ static PyObject* mmpy_cleanup_handler(PyObject* self, PyObject* args){ return Py_None; } - Py_DECREF(current_handler); + //do not decrease refcount on current_handler here as the reference may be used by python code again return current_handler; } @@ -595,6 +598,7 @@ static int python_handle(size_t num, managed_fd* fds){ //if timer expired, call handler if(interval[u].delta >= interval[u].interval){ interval[u].delta %= interval[u].interval; + DBGPF("Calling interval handler %" PRIsize_t ", last delta %" PRIu64, u, delta); //swap to interpreter PyEval_RestoreThread(interval[u].interpreter); @@ -603,7 +607,6 @@ static int python_handle(size_t num, managed_fd* fds){ Py_XDECREF(result); //release interpreter PyEval_ReleaseThread(interval[u].interpreter); - DBGPF("Calling interval handler %" PRIsize_t, u); } } } -- cgit v1.2.3 From cf6a8e9a0f125633b1ef7d84e10935695598e654 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 29 Mar 2020 11:14:58 +0200 Subject: Reschedule artnet frame output on EAGAIN --- backends/artnet.c | 38 ++++++++++++++++++++++++++------------ backends/python.c | 1 - 2 files changed, 26 insertions(+), 13 deletions(-) (limited to 'backends/python.c') diff --git a/backends/artnet.c b/backends/artnet.c index c59a79d..585895b 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -206,10 +206,10 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ return data->data.channel + chan_a; } -static int artnet_transmit(instance* inst){ - size_t u; +static int artnet_transmit(instance* inst, artnet_output_universe* output){ artnet_instance_data* data = (artnet_instance_data*) inst->impl; - //output frame + + //build output frame artnet_pkt frame = { .magic = {'A', 'r', 't', '-', 'N', 'e', 't', 0x00}, .opcode = htobe16(OpDmx), @@ -224,16 +224,30 @@ static int artnet_transmit(instance* inst){ memcpy(frame.data, data->data.out, 512); if(sendto(artnet_fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ - LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); + #ifndef _WIN32 + if(errno != EAGAIN){ + LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); + #else + if(WSAGetLastError() != WSAEWOULDBLOCK){ + char* error = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + LOGPF("Failed to output frame for instance %s: %s", inst->name, error); + LocalFree(error); + #endif + return 1; + } + //reschedule frame output + output->mark = 1; + if(!next_frame || next_frame > ARTNET_SYNTHESIZE_MARGIN){ + next_frame = ARTNET_SYNTHESIZE_MARGIN; + } + return 0; } //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].output_instance[u].last_frame = mm_timestamp(); - artnet_fd[data->fd_index].output_instance[u].mark = 0; - } - } + output->last_frame = mm_timestamp(); + output->mark = 0; return 0; } @@ -285,7 +299,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) } return 0; } - return artnet_transmit(inst); + return artnet_transmit(inst, artnet_fd[data->fd_index].output_instance + u); } return 0; @@ -366,7 +380,7 @@ static int artnet_handle(size_t num, managed_fd* fds){ || 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); + artnet_transmit(inst, artnet_fd[u].output_instance + c); } } diff --git a/backends/python.c b/backends/python.c index 94f8e24..28b95a9 100644 --- a/backends/python.c +++ b/backends/python.c @@ -1,5 +1,4 @@ #define BACKEND_NAME "python" -#define DEBUG #define PY_SSIZE_T_CLEAN #include -- cgit v1.2.3 From f692715444c6ddeb47bf87b53acf46798785290a Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 27 Apr 2020 20:54:02 +0200 Subject: Allow access to previous value in python handlers --- backends/lua.md | 4 ++-- backends/maweb.c | 2 +- backends/python.c | 9 +++++---- backends/python.md | 3 +++ 4 files changed, 11 insertions(+), 7 deletions(-) (limited to 'backends/python.c') diff --git a/backends/lua.md b/backends/lua.md index 4e58ded..b2f40e0 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -26,8 +26,8 @@ The following functions are provided within the Lua interpreter for interaction | `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) | | `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds | -While a channel handler executes, calling `input_value` for that channel returns the previous value. Once -the handler returns, the internal buffer is updated. +While a channel handler executes, calling `input_value` for that channel returns the previous value. +The stored value is updated once the handler returns. Example script: ```lua diff --git a/backends/maweb.c b/backends/maweb.c index 5242f36..97d4cea 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -570,7 +570,7 @@ static int maweb_request_playbacks(instance* inst){ item_types, view, data->session); - rv |= maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); DBGPF("Poll request: %s", xmit_buffer); updates_inflight++; } diff --git a/backends/python.c b/backends/python.c index 28b95a9..bd73a20 100644 --- a/backends/python.c +++ b/backends/python.c @@ -559,18 +559,19 @@ static int python_set(instance* inst, size_t num, channel** c, channel_value* v) 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); + result = PyObject_CallFunction(chan->handler, "d", v[u].normalised); Py_XDECREF(result); data->current_channel = NULL; DBGPF("Done with handler for %s.%s", inst->name, chan->name); } + + //update input value buffer after finishing the handler + chan->in = v[u].normalised; + } //release interpreter diff --git a/backends/python.md b/backends/python.md index ab0fb38..a78d972 100644 --- a/backends/python.md +++ b/backends/python.md @@ -26,6 +26,9 @@ 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 | | `cleanup_handler(function)` | `midimonster.cleanup_handler(save_all)`| Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | +When a channel handler executes, calling `midimonster.inputvalue()` for that exact channel returns the previous value, +while the argument to the handler is the current value. The stored value is updated after the handler finishes executing. + Example Python module: ```python import socket -- cgit v1.2.3