From b618c4a6b74a52f830ca53029e1cc680d56a2501 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 6 Jul 2019 17:25:12 +0200 Subject: Implement Lua backend --- backends/lua.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 backends/lua.md (limited to 'backends/lua.md') diff --git a/backends/lua.md b/backends/lua.md new file mode 100644 index 0000000..91e8fe2 --- /dev/null +++ b/backends/lua.md @@ -0,0 +1,49 @@ +### The `lua` backend + +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. + +To process incoming channel events, the MIDIMonster calls corresponding Lua functions with +the value (as a Lua `number` type) as parameter. To send output on a channel, the Lua environment +provides the function `output(channel-name, value)`. + +Example script: +``` +function bar(value) + output("foo", value / 2) +end +``` + +Input values range between 0.0 and 1.0, output values are clamped to the same range. + +#### Global configuration + +The backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `source` | `script.lua` | none | Lua source file | + +A single instance may have multiple `source` options specified, which will all be read cumulatively. + +#### Channel specification + +Channel names may be any valid Lua function name. + +Example mapping: +``` +lua1.foo > lua2.bar +``` + +#### Known bugs / problems + +Using `output` as an input channel name to a Lua instance does not work, as the interpreter has +`output` globally assigned to the event output function. Using `output` as an output channel name +via `output("output", value)` works as intended. + +The path to the Lua source files is relative to the current working directory. This may lead +to problems when copying configuration between installations. -- cgit v1.2.3 From ee4a46105acecb6a7adc1e7189e8b0a66404b421 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 13 Jul 2019 17:51:11 +0200 Subject: Improved Lua backend with intervals --- TODO | 2 +- backends/lua.c | 302 +++++++++++++++++++++++++++++++++++++++++++++++++++---- backends/lua.h | 10 ++ backends/lua.md | 32 ++++-- configs/demo.lua | 45 ++++++--- configs/lua.cfg | 10 +- 6 files changed, 359 insertions(+), 42 deletions(-) (limited to 'backends/lua.md') diff --git a/TODO b/TODO index cfdd409..f39dae0 100644 --- a/TODO +++ b/TODO @@ -5,4 +5,4 @@ Printing backend document example configs lua timer -evdev relaxes size +evdev relative axis size diff --git a/backends/lua.c b/backends/lua.c index ae2d460..08d8d3c 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -1,10 +1,18 @@ #include +#include +#include +#include #include "lua.h" #define BACKEND_NAME "lua" #define LUA_REGISTRY_KEY "_midimonster_lua_instance" -//TODO instance identification for callvacks +static size_t timers = 0; +static lua_timer* timer = NULL; +static struct itimerspec timer_config = { + 0 +}; +static int timer_fd = -1; int init(){ backend lua = { @@ -24,31 +32,83 @@ int init(){ fprintf(stderr, "Failed to register lua backend\n"); return 1; } + + //create the timer to expire intervals + timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); + if(timer_fd < 0){ + fprintf(stderr, "Failed to create timer for Lua backend\n"); + return 1; + } + return 0; +} + +static int lua_update_timerfd(){ + uint64_t interval = 0, gcd, residual; + size_t n = 0; + + //find the minimum for the lower interval bounds + for(n = 0; n < timers; n++){ + if(timer[n].interval && (!interval || timer[n].interval < interval)){ + interval = timer[n].interval; + } + } + + //stop the timer + if(!interval){ + memset(&timer_config, 0, sizeof(struct itimerspec)); + } + //calculate gcd of all timers + else{ + for(n = 0; n < timers; n++){ + if(timer[n].interval){ + //calculate gcd of current interval and this timers interval + gcd = timer[n].interval; + while(gcd){ + residual = interval % gcd; + interval = gcd; + gcd = residual; + } + //since we round everything, 10 is the lowest interval we get + if(interval == 10){ + break; + } + } + } + + timer_config.it_interval.tv_sec = interval / 1000; + timer_config.it_interval.tv_nsec = (interval % 1000) * 1e6; + timer_config.it_value.tv_nsec = 1; + } + + //configure the new interval + timerfd_settime(timer_fd, 0, &timer_config, NULL); return 0; } static int lua_callback_output(lua_State* interpreter){ - int arguments = lua_gettop(interpreter); - size_t n; + size_t n = 0; channel_value val; const char* channel_name = NULL; channel* channel = NULL; instance* inst = NULL; lua_instance_data* data = NULL; - if(arguments != 2){ - fprintf(stderr, "Lua output function called with %d arguments, expected 2\n", arguments); + if(lua_gettop(interpreter) != 2){ + fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", lua_gettop(interpreter)); return 0; } - channel_name = lua_tostring(interpreter, 1); - val.normalised = clamp(lua_tonumber(interpreter, 2), 1.0, 0.0); - + //get instance pointer from registry lua_pushstring(interpreter, LUA_REGISTRY_KEY); lua_gettable(interpreter, LUA_REGISTRYINDEX); - inst = (instance *) lua_touserdata(interpreter, -1); + inst = (instance*) lua_touserdata(interpreter, -1); data = (lua_instance_data*) inst->impl; + //fetch function parameters + channel_name = lua_tostring(interpreter, 1); + val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); + + //find correct channel & output value for(n = 0; n < data->channels; n++){ if(!strcmp(channel_name, data->channel_name[n])){ channel = mm_channel(inst, n, 0); @@ -56,6 +116,7 @@ static int lua_callback_output(lua_State* interpreter){ return 0; } mm_channel_event(channel, val); + data->output[n] = val.normalised; return 0; } } @@ -64,6 +125,122 @@ static int lua_callback_output(lua_State* interpreter){ return 0; } +static int lua_callback_interval(lua_State* interpreter){ + size_t n = 0; + instance* inst = NULL; + lua_instance_data* data = NULL; + uint64_t interval = 0; + int reference = LUA_NOREF; + + if(lua_gettop(interpreter) != 2){ + fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", lua_gettop(interpreter)); + return 0; + } + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + data = (lua_instance_data*) inst->impl; + + //fetch and round the interval + interval = luaL_checkinteger(interpreter, 2); + if(interval % 10 < 5){ + interval -= interval % 10; + } + else{ + interval += (10 - (interval % 10)); + } + + //push the function again + lua_pushvalue(interpreter, 1); + if(lua_gettable(interpreter, LUA_REGISTRYINDEX) == LUA_TNUMBER){ + //already interval'd + reference = luaL_checkinteger(interpreter, 4); + } + else if(interval){ + //get a reference to the function + lua_pushvalue(interpreter, 1); + reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + + //the function indexes the reference + lua_pushvalue(interpreter, 1); + lua_pushinteger(interpreter, reference); + lua_settable(interpreter, LUA_REGISTRYINDEX); + } + + //find matching timer + for(n = 0; n < timers; n++){ + if(timer[n].reference == reference && timer[n].interpreter == interpreter){ + break; + } + } + + if(n < timers){ + //set new interval + timer[n].interval = interval; + timer[n].delta = 0; + } + else if(interval){ + //append new timer + timer = realloc(timer, (timers + 1) * sizeof(lua_timer)); + if(!timer){ + fprintf(stderr, "Failed to allocate memory\n"); + timers = 0; + return 0; + } + timer[timers].interval = interval; + timer[timers].delta = 0; + timer[timers].interpreter = interpreter; + timer[timers].reference = reference; + timers++; + } + + //recalculate timerspec + lua_update_timerfd(); + return 0; +} + +static int lua_callback_value(lua_State* interpreter, uint8_t input){ + size_t n = 0; + instance* inst = NULL; + lua_instance_data* data = NULL; + const char* channel_name = NULL; + + if(lua_gettop(interpreter) != 1){ + fprintf(stderr, "Lua get_value function called with %d arguments, expected 1 (string)\n", lua_gettop(interpreter)); + return 0; + } + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + data = (lua_instance_data*) inst->impl; + + //fetch argument + channel_name = lua_tostring(interpreter, 1); + + //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]); + return 1; + } + } + + fprintf(stderr, "Tried to get unknown channel %s.%s\n", inst->name, channel_name); + return 0; +} + +static int lua_callback_input_value(lua_State* interpreter){ + return lua_callback_value(interpreter, 1); +} + +static int lua_callback_output_value(lua_State* interpreter){ + return lua_callback_value(interpreter, 0); +} + static int lua_configure(char* option, char* value){ fprintf(stderr, "The lua backend does not take any global configuration\n"); return 1; @@ -72,6 +249,7 @@ static int lua_configure(char* option, char* value){ static int lua_configure_instance(instance* inst, char* option, char* value){ lua_instance_data* data = (lua_instance_data*) inst->impl; + //load a lua file into the interpreter if(!strcmp(option, "script")){ if(luaL_dofile(data->interpreter, value)){ fprintf(stderr, "Failed to load lua source file %s for instance %s: %s\n", value, inst->name, lua_tostring(data->interpreter, -1)); @@ -105,13 +283,16 @@ static instance* lua_instance(){ } luaL_openlibs(data->interpreter); - //register lua api functions + //register lua interface functions lua_register(data->interpreter, "output", lua_callback_output); + 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); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); lua_pushlightuserdata(data->interpreter, (void *) inst); - lua_settable(data->interpreter, LUA_REGISTRYINDEX); + lua_settable(data->interpreter, LUA_REGISTRYINDEX); inst->impl = data; return inst; @@ -131,11 +312,16 @@ static channel* lua_channel(instance* inst, char* spec){ //allocate new channel if(u == data->channels){ data->channel_name = realloc(data->channel_name, (u + 1) * sizeof(char*)); - if(!data->channel_name){ + 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){ fprintf(stderr, "Failed to allocate memory\n"); 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]){ fprintf(stderr, "Failed to allocate memory\n"); @@ -151,25 +337,91 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t n = 0; lua_instance_data* data = (lua_instance_data*) inst->impl; + //handle all incoming events for(n = 0; n < num; n++){ - //call lua channel handlers - lua_getglobal(data->interpreter, data->channel_name[c[n]->ident]); - lua_pushnumber(data->interpreter, v[n].normalised); - if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ - fprintf(stderr, "Failed to call handler for %s.%s: %s\n", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1)); - lua_pop(data->interpreter, 1); + data->input[c[n]->ident] = v[n].normalised; + //call lua channel handlers if present + if(data->reference[n] != LUA_NOREF){ + 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){ + fprintf(stderr, "Failed to call handler for %s.%s: %s\n", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1)); + lua_pop(data->interpreter, 1); + } } } return 0; } static int lua_handle(size_t num, managed_fd* fds){ - //TODO call timer callbacks + uint8_t read_buffer[100]; + uint64_t delta = timer_config.it_interval.tv_sec * 1000 + timer_config.it_interval.tv_nsec / 1e6; + size_t n; + + if(!num){ + return 0; + } + + //read the timer iteration to acknowledge the fd + if(read(timer_fd, read_buffer, sizeof(read_buffer)) < 0){ + fprintf(stderr, "Failed to read from Lua timer: %s\n", strerror(errno)); + return 1; + } + + //add delta to all active timers + for(n = 0; n < timers; n++){ + if(timer[n].interval){ + timer[n].delta += delta; + //call lua function if timer expired + if(timer[n].delta >= timer[n].interval){ + timer[n].delta %= timer[n].interval; + lua_rawgeti(timer[n].interpreter, LUA_REGISTRYINDEX, timer[n].reference); + lua_pcall(timer[n].interpreter, 0, 0, 0); + } + } + } return 0; } static int lua_start(){ - //TODO start timers / register fds + size_t n, u, p; + instance** inst = NULL; + lua_instance_data* data = NULL; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + //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(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], "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; + } + } + } + } + + free(inst); + if(!n){ + return 0; + } + + //register the timer with the core + fprintf(stderr, "Lua backend registering 1 descriptor to core\n"); + if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ + return 1; + } return 0; } @@ -178,6 +430,7 @@ static int lua_shutdown(){ instance** inst = NULL; lua_instance_data* data = NULL; + //fetch all instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ fprintf(stderr, "Failed to fetch instance list\n"); return 1; @@ -192,10 +445,19 @@ static int lua_shutdown(){ free(data->channel_name[p]); } free(data->channel_name); + free(data->reference); + free(data->input); + free(data->output); free(inst[u]->impl); } free(inst); + //free module-global data + free(timer); + timer = NULL; + timers = 0; + close(timer_fd); + timer_fd = -1; fprintf(stderr, "Lua backend shut down\n"); return 0; diff --git a/backends/lua.h b/backends/lua.h index 27e1afd..ccffef7 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -17,5 +17,15 @@ static int lua_shutdown(); typedef struct /*_lua_instance_data*/ { size_t channels; char** channel_name; + int* reference; + double* input; + double* output; lua_State* interpreter; } lua_instance_data; + +typedef struct /*_lua_interval_callback*/ { + uint64_t interval; + uint64_t delta; + lua_State* interpreter; + int reference; +} lua_timer; diff --git a/backends/lua.md b/backends/lua.md index 91e8fe2..970e8e2 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -5,15 +5,32 @@ events using the Lua programming language. Every instance has it's own interpreter state which can be loaded with custom handler scripts. -To process incoming channel events, the MIDIMonster calls corresponding Lua functions with -the value (as a Lua `number` type) as parameter. To send output on a channel, the Lua environment -provides the function `output(channel-name, value)`. +To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist) +with the value (as a Lua `number` type) as parameter. + +The following functions are provided within the Lua interpreter for interaction with the MIDIMonster + +| 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) | +| `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 | + Example script: ``` function bar(value) output("foo", value / 2) end + +step = 0 +function toggle() + output("bar", step * 1.0) + step = (step + 1) % 2; +end + +interval(toggle, 1000) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. @@ -41,9 +58,12 @@ lua1.foo > lua2.bar #### Known bugs / problems -Using `output` as an input channel name to a Lua instance does not work, as the interpreter has -`output` globally assigned to the event output function. Using `output` as an output channel name -via `output("output", value)` works as intended. +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 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. The path to the Lua source files is relative to the current working directory. This may lead to problems when copying configuration between installations. diff --git a/configs/demo.lua b/configs/demo.lua index e816ac4..24a8396 100644 --- a/configs/demo.lua +++ b/configs/demo.lua @@ -1,21 +1,42 @@ --- This example MIDIMonstaer Lua script spreads one input channel onto multiple output +-- This example MIDIMonster Lua script spreads one input channel onto multiple output -- channels using a polynomial function evaluated at multiple points. This effect can -- be visualized e.g. with martrix (https://github.com/cbdevnet/martrix). --- This is just a demonstration of global variables -foo = 0 - -- The polynomial to evaluate -function polynomial(offset, x) - return math.exp(-20 * (x - offset) ^ 2) +function polynomial(x) + return math.exp(-40 * input_value("width") * (x - input_value("offset")) ^ 2) end --- Handler function for the input channel -function input(value) - foo = foo + 1 - print("input at ", value, foo) - +-- Evaluate and set output channels +function evaluate() for chan=0,10 do - output("out" .. chan, polynomial(value, (1 / 10) * chan)) + output("out" .. chan, polynomial((1 / 10) * chan)) + end +end + +-- Handler functions for the input channels +function offset(value) + evaluate() +end + +function width(value) + evaluate() +end + +-- This is an example showing a simple chase running on its own without the need +-- (but the possibility) for external input + +-- Global value for the current step +current_step = 0 + +function step() + if(current_step > 0) then + output("dim", 0.0) + else + output("dim", 1.0) end + + current_step = (current_step + 1) % 2 end + +interval(step, 1000) diff --git a/configs/lua.cfg b/configs/lua.cfg index 9182122..42fd27d 100644 --- a/configs/lua.cfg +++ b/configs/lua.cfg @@ -8,8 +8,9 @@ bind = 0.0.0.0 device = /dev/input/by-path/platform-i8042-serio-2-event-mouse [evdev xbox] -input = Xbox Wireless +device = /dev/input/event17 axis.ABS_X = 34300 0 65535 255 4095 +axis.ABS_Y = 34300 0 65535 255 4095 [lua lua] script = configs/demo.lua @@ -19,8 +20,9 @@ universe = 0 destination = 255.255.255.255 [map] -mouse.EV_KEY.BTN_LEFT > lua.input -xbox.EV_ABS.ABS_X > lua.input +mouse.EV_KEY.BTN_LEFT > lua.click +xbox.EV_ABS.ABS_X > lua.offset +xbox.EV_ABS.ABS_Y > lua.width art.1 < lua.out0 art.2 < lua.out1 @@ -33,3 +35,5 @@ art.8 < lua.out7 art.9 < lua.out8 art.10 < lua.out9 art.11 < lua.out10 + +art.12 < lua.dim -- cgit v1.2.3 From e776e02531aca45f424f7139e5d7304ba3096b45 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 14 Jul 2019 16:49:31 +0200 Subject: Work around missing timerfd on OSX/Windows --- backend.c | 2 +- backends/lua.c | 40 ++++++++++++++++++++++++++++++++++++++++ backends/lua.h | 6 ++++++ backends/lua.md | 4 ++-- configs/lua.cfg | 2 +- 5 files changed, 50 insertions(+), 4 deletions(-) (limited to 'backends/lua.md') diff --git a/backend.c b/backend.c index ed5e6db..5df5d73 100644 --- a/backend.c +++ b/backend.c @@ -227,7 +227,7 @@ struct timeval backend_timeout(){ struct timeval tv = { secs, - msecs + msecs * 1000 }; return tv; } diff --git a/backends/lua.c b/backends/lua.c index 8069f8f..7fcd0ab 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -10,10 +10,17 @@ static size_t timers = 0; static lua_timer* timer = NULL; uint64_t timer_interval = 0; +#ifdef MMBACKEND_LUA_TIMERFD static int timer_fd = -1; +#else +static uint64_t last_timestamp; +#endif int init(){ backend lua = { + #ifndef MMBACKEND_LUA_TIMERFD + .interval = lua_interval, + #endif .name = BACKEND_NAME, .conf = lua_configure, .create = lua_instance, @@ -31,21 +38,33 @@ int init(){ return 1; } + #ifdef MMBACKEND_LUA_TIMERFD //create the timer to expire intervals timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); if(timer_fd < 0){ fprintf(stderr, "Failed to create timer for Lua backend\n"); return 1; } + #endif return 0; } +static uint32_t lua_interval(){ + //FIXME Return delta for next timer here + if(timer_interval){ + return timer_interval; + } + return 1000; +} + static int lua_update_timerfd(){ uint64_t interval, gcd, residual; size_t n = 0; + #ifdef MMBACKEND_LUA_TIMERFD struct itimerspec timer_config = { 0 }; + #endif //find the minimum for the lower interval bounds for(n = 0; n < timers; n++){ @@ -72,16 +91,20 @@ static int lua_update_timerfd(){ } } + #ifdef MMBACKEND_LUA_TIMERFD timer_config.it_interval.tv_sec = timer_config.it_value.tv_sec = interval / 1000; timer_config.it_interval.tv_nsec = timer_config.it_value.tv_nsec = (interval % 1000) * 1e6; + #endif } if(interval == timer_interval){ return 0; } + #ifdef MMBACKEND_LUA_TIMERFD //configure the new interval timerfd_settime(timer_fd, 0, &timer_config, NULL); + #endif timer_interval = interval; return 0; } @@ -359,6 +382,7 @@ static int lua_handle(size_t num, managed_fd* fds){ uint64_t delta = timer_interval; size_t n; + #ifdef MMBACKEND_LUA_TIMERFD if(!num){ return 0; } @@ -368,6 +392,18 @@ static int lua_handle(size_t num, managed_fd* fds){ fprintf(stderr, "Failed to read from Lua timer: %s\n", strerror(errno)); return 1; } + #else + if(!last_timestamp){ + last_timestamp = mm_timestamp(); + } + delta = mm_timestamp() - last_timestamp; + last_timestamp = mm_timestamp(); + #endif + + //no timers active + if(!timer_interval){ + return 0; + } //add delta to all active timers for(n = 0; n < timers; n++){ @@ -418,11 +454,13 @@ static int lua_start(){ return 0; } + #ifdef MMBACKEND_LUA_TIMERFD //register the timer with the core fprintf(stderr, "Lua backend registering 1 descriptor to core\n"); if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ return 1; } + #endif return 0; } @@ -457,8 +495,10 @@ static int lua_shutdown(){ free(timer); timer = NULL; timers = 0; + #ifdef MMBACKEND_LUA_TIMERFD close(timer_fd); timer_fd = -1; + #endif fprintf(stderr, "Lua backend shut down\n"); return 0; diff --git a/backends/lua.h b/backends/lua.h index ccffef7..7aad891 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -4,6 +4,11 @@ #include #include +//OSX and Windows don't have the cool new toys... +#ifdef __linux__ + #define MMBACKEND_LUA_TIMERFD +#endif + int init(); static int lua_configure(char* option, char* value); static int lua_configure_instance(instance* inst, char* option, char* value); @@ -13,6 +18,7 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v); static int lua_handle(size_t num, managed_fd* fds); static int lua_start(); static int lua_shutdown(); +static uint32_t lua_interval(); typedef struct /*_lua_instance_data*/ { size_t channels; diff --git a/backends/lua.md b/backends/lua.md index 970e8e2..e273b28 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -15,7 +15,7 @@ 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 | | `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 | +| `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | Example script: @@ -58,7 +58,7 @@ lua1.foo > lua2.bar #### Known bugs / problems -Using any of the interface functions (`output`, `interval`, `input\_value`, `output\_value`) as an +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 these names as arguments to the output and value interface functions works as intended. diff --git a/configs/lua.cfg b/configs/lua.cfg index 42fd27d..b892e91 100644 --- a/configs/lua.cfg +++ b/configs/lua.cfg @@ -8,7 +8,7 @@ bind = 0.0.0.0 device = /dev/input/by-path/platform-i8042-serio-2-event-mouse [evdev xbox] -device = /dev/input/event17 +;device = /dev/input/event17 axis.ABS_X = 34300 0 65535 255 4095 axis.ABS_Y = 34300 0 65535 255 4095 -- cgit v1.2.3 From c75721e77ecada3c88f4b493c1e3036c151bfe88 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 24 Jul 2019 21:33:23 +0200 Subject: Clarify backend documentation --- backends/evdev.md | 4 +++- backends/lua.c | 4 ++-- backends/lua.md | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) (limited to 'backends/lua.md') diff --git a/backends/evdev.md b/backends/evdev.md index dfe5ec9..d750f1e 100644 --- a/backends/evdev.md +++ b/backends/evdev.md @@ -30,6 +30,8 @@ instances. The configuration value contains, space-separated, the following valu * `flat`: An offset, below which all deviations will be ignored * `resolution`: Axis resolution in units per millimeter (or units per radian for rotational axes) +If an axis is not used for output, this configuration can be omitted. + For real devices, all of these parameters for every axis can be found by running `evtest` on the device. #### Channel specification @@ -69,4 +71,4 @@ than `0`, respectively. As for output, only the values `-1`, `0` and `1` are gen `EV_KEY` key-down events are sent for normalized channel values over `0.9`. Extended event type values such as `EV_LED`, `EV_SND`, etc are recognized in the MIDIMonster configuration file -but may or may not work with the internal channel mapping and normalization code. \ No newline at end of file +but may or may not work with the internal channel mapping and normalization code. diff --git a/backends/lua.c b/backends/lua.c index 4d946cd..61e4e08 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -277,7 +277,7 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ lua_instance_data* data = (lua_instance_data*) inst->impl; //load a lua file into the interpreter - if(!strcmp(option, "script")){ + if(!strcmp(option, "script") || !strcmp(option, "source")){ if(luaL_dofile(data->interpreter, value)){ fprintf(stderr, "Failed to load lua source file %s for instance %s: %s\n", value, inst->name, lua_tostring(data->interpreter, -1)); return 1; @@ -285,7 +285,7 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ return 0; } - fprintf(stderr, "Unknown configuration parameter %s for lua backend\n", option); + fprintf(stderr, "Unknown configuration parameter %s for lua instance %s\n", option, inst->name); return 1; } diff --git a/backends/lua.md b/backends/lua.md index e273b28..1c67477 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -43,7 +43,7 @@ The backend does not take any global configuration. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `source` | `script.lua` | none | Lua source file | +| `script` | `script.lua` | none | Lua source file | A single instance may have multiple `source` options specified, which will all be read cumulatively. -- cgit v1.2.3 From cf93d280af47aea1bf8bdafa30eabb2c2de005b8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 10 Aug 2019 15:30:48 +0200 Subject: Implement stream client connections in libmmbackend (Fixes #19) --- backends/artnet.c | 2 +- backends/libmmbackend.c | 62 +++++++++++++++++++++++++++++++++++-------------- backends/libmmbackend.h | 22 ++++++++++++++---- backends/lua.md | 2 +- backends/osc.c | 2 +- backends/sacn.c | 2 +- 6 files changed, 67 insertions(+), 25 deletions(-) (limited to 'backends/lua.md') diff --git a/backends/artnet.c b/backends/artnet.c index a6df4ab..7f3f08c 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -19,7 +19,7 @@ static int artnet_listener(char* host, char* port){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(fd < 0){ return -1; } diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index b27ebc5..2fd3b8b 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -52,7 +52,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad return 0; } -int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast){ int fd = -1, status, yes = 1; struct addrinfo hints = { .ai_family = AF_UNSPEC, @@ -80,20 +80,31 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ fprintf(stderr, "Failed to enable SO_REUSEADDR on socket\n"); } - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to enable SO_BROADCAST on socket\n"); - } + if(mcast){ + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to enable SO_BROADCAST on socket\n"); + } - yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to disable IP_MULTICAST_LOOP on socket: %s\n", strerror(errno)); + yes = 0; + if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to disable IP_MULTICAST_LOOP on socket: %s\n", strerror(errno)); + } } - status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); - if(status < 0){ - close(fd); - continue; + if(listener){ + status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } + } + else{ + status = connect(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } } break; @@ -107,11 +118,11 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ //set nonblocking #ifdef _WIN32 - u_long mode = 1; - if(ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR){ - closesocket(fd); - return 1; - } + u_long mode = 1; + if(ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR){ + closesocket(fd); + return 1; + } #else int flags = fcntl(fd, F_GETFL, 0); if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ @@ -123,3 +134,20 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ return fd; } + +int mmbackend_send(int fd, uint8_t* data, size_t length){ + ssize_t total = 0, sent; + while(total < length){ + sent = send(fd, data + total, length - total, 0); + if(sent < 0){ + fprintf(stderr, "Failed to send: %s\n", strerror(errno)); + return 1; + } + total += sent; + } + return 0; +} + +int mmbackend_send_str(int fd, char* data){ + return mmbackend_send(fd, (uint8_t*) data, strlen(data)); +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 77cad6a..31c4b96 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -16,7 +16,8 @@ #include #include "../portability.h" -/* Parse spec as host specification in the form +/* + * Parse spec as host specification in the form * host port * into its constituent parts. * Returns offsets into the original string and modifies it. @@ -25,13 +26,26 @@ */ void mmbackend_parse_hostspec(char* spec, char** host, char** port); -/* Parse a given host / port combination into a sockaddr_storage +/* + * Parse a given host / port combination into a sockaddr_storage * suitable for usage with connect / sendto * Returns 0 on success */ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len); -/* Create a socket of given type and mode for a bind / connect host. +/* + * Create a socket of given type and mode for a bind / connect host. * Returns -1 on failure, a valid file descriptor for the socket on success. */ -int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener); +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast); + +/* + * Send arbitrary data over multiple writes if necessary + * Returns 1 on failure, 0 on success. + */ +int mmbackend_send(int fd, uint8_t* data, size_t length); + +/* + * Wraps mmbackend_send for cstrings + */ +int mmbackend_send_str(int fd, char* data); diff --git a/backends/lua.md b/backends/lua.md index 1c67477..6ad5c2a 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -37,7 +37,7 @@ Input values range between 0.0 and 1.0, output values are clamped to the same ra #### Global configuration -The backend does not take any global configuration. +The `lua` backend does not take any global configuration. #### Instance configuration diff --git a/backends/osc.c b/backends/osc.c index 03e431f..77bbde4 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -525,7 +525,7 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ return 1; } - data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(data->fd < 0){ fprintf(stderr, "Failed to bind for instance %s\n", inst->name); return 1; diff --git a/backends/sacn.c b/backends/sacn.c index 6f7d1a5..2f418e5 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -60,7 +60,7 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(fd < 0){ return -1; } -- cgit v1.2.3 From 350f0d2d2eaff5f0d57b09857102e2df1e96d733 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 7 Nov 2019 18:44:19 +0100 Subject: Makefile install target and packaging instructions (Fixes #28) --- Makefile | 22 +++++++++++++++++----- README.md | 23 +++++++++++++++++++++++ backends/lua.md | 5 +---- backends/maweb.c | 2 +- backends/winmidi.c | 2 +- config.c | 38 +++++++++++++++++++++++++++++++++++--- configs/flying-faders.cfg | 2 +- configs/lua.cfg | 2 +- midimonster.h | 13 ++++++++++++- plugin.c | 12 ++++++++++-- 10 files changed, 102 insertions(+), 19 deletions(-) (limited to 'backends/lua.md') diff --git a/Makefile b/Makefile index 2d88c49..5a83a2d 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,7 @@ -.PHONY: all clean run sanitize backends windows full backends-full +.PHONY: all clean run sanitize backends windows full backends-full install OBJS = config.o backend.o plugin.o -PLUGINDIR = "\"./backends/\"" -PLUGINDIR_W32 = "\"backends\\\\\"" +PREFIX ?= /usr SYSTEM := $(shell uname -s) CFLAGS ?= -g -Wall -Wpedantic @@ -11,7 +10,6 @@ CFLAGS += -fvisibility=hidden #CFLAGS += -DDEBUG midimonster: LDLIBS = -ldl -midimonster: CFLAGS += -DPLUGINS=$(PLUGINDIR) # Work around strange linker passing convention differences in Linux and OSX ifeq ($(SYSTEM),Linux) @@ -21,6 +19,14 @@ ifeq ($(SYSTEM),Darwin) midimonster: LDFLAGS += -Wl,-export_dynamic endif +# Allow overriding the locations for backend plugins and default configuration +ifdef DEFAULT_CFG +midimonster: CFLAGS += -DDEFAULT_CFG=\"$(DEFAULT_CFG)\" +endif +ifdef PLUGINS +midimonster: CFLAGS += -DPLUGINS=\"$(PLUGINS)\" +endif + all: midimonster backends full: midimonster backends-full @@ -39,7 +45,7 @@ midimonster: midimonster.c portability.h $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ midimonster.exe: export CC = x86_64-w64-mingw32-gcc -midimonster.exe: CFLAGS += -DPLUGINS=$(PLUGINDIR_W32) -Wno-format +midimonster.exe: CFLAGS += -Wno-format midimonster.exe: LDLIBS = -lws2_32 midimonster.exe: LDFLAGS += -Wl,--out-implib,libmmapi.a midimonster.exe: midimonster.c portability.h $(OBJS) @@ -55,6 +61,12 @@ clean: run: valgrind --leak-check=full --show-leak-kinds=all ./midimonster +install: + install -d "$(DESTDIR)$(PREFIX)/bin" + install -d "$(DESTDIR)$(PREFIX)/lib/midimonster" + install -m 0755 midimonster "$(DESTDIR)$(PREFIX)/bin" + install -m 0755 backends/*.so "$(DESTDIR)$(PREFIX)/lib/midimonster" + sanitize: export CC = clang sanitize: export CFLAGS += -g -Wall -Wpedantic -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer sanitize: midimonster backends diff --git a/README.md b/README.md index 130945a..c31f16c 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,16 @@ be used to build a subset of the backends as well as the core. For Linux and OSX, just running `make` in the source directory should do the trick. +The build process accepts the following parameters, either from the environment or +as arguments to the `make` invocation: + +| Target | Parameter | Default value | Description | +|---------------|-----------------------|-------------------------------|-------------------------------| +| build targets | `DEFAULT_CFG` | `monster.cfg` | Default configuration file | +| build targets | `PLUGINS` | Linux/OSX: `./backends/`, Windows: `backends\` | Backend plugin library path | +| `install` | `DESTDIR` | empty | Destination directory for packaging builds | +| `install` | `PREFIX` | `/usr` | Install prefix for binaries | + Some backends have been marked as optional as they require rather large additional software to be installed, for example the `ola` backend. To create a build including these, run `make full`. @@ -163,6 +173,19 @@ To build for Windows, you still need to compile on a Linux machine. Install the crosscompiler package listed above and run `make windows`. This will build `midimonster.exe` as well as a set of backends as DLL files. +For system-wide install or packaging builds, the following steps are recommended: + +``` +export PREFIX=/usr +export PLUGINS=$PREFIX/lib/midimonster +export DEFAULT_CFG=/etc/midimonster.cfg +make +make install +``` + +Depending on your configuration of `DESTDIR`, the `make install` step may require root privileges to +install the binaries to the appropriate destinations. + ## Development The architecture is split into the `midimonster` core, handling mapping diff --git a/backends/lua.md b/backends/lua.md index 6ad5c2a..f38e189 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -43,7 +43,7 @@ The `lua` backend does not take any global configuration. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `script` | `script.lua` | none | Lua source file | +| `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. @@ -64,6 +64,3 @@ 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. - -The path to the Lua source files is relative to the current working directory. This may lead -to problems when copying configuration between installations. diff --git a/backends/maweb.c b/backends/maweb.c index c98d04b..57d04ae 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -470,7 +470,7 @@ static int maweb_request_playbacks(instance* inst){ offsets[0] = offsets[1] = offsets[2] = 1; page_index = data->channel[channel].page; //poll logic differs between the consoles because reasons - //dont quote me on this section + //don't quote me on this section if(data->peer_type == peer_dot2){ //blocks 0, 100 & 200 have 21 execs and need to be queried from fader view view = (data->channel[channel].index >= 300) ? 3 : 2; diff --git a/backends/winmidi.c b/backends/winmidi.c index de7d867..d6bc0bc 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -84,7 +84,7 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value) } if(!strcmp(option, "write")){ if(data->write){ - fprintf(stderr, "winmidi instance %s already connected to an otput device\n", inst->name); + fprintf(stderr, "winmidi instance %s already connected to an output device\n", inst->name); return 1; } data->write = strdup(value); diff --git a/config.c b/config.c index f4d928e..ddee720 100644 --- a/config.c +++ b/config.c @@ -1,5 +1,7 @@ #include #include +#include +#include #include "midimonster.h" #include "config.h" #include "backend.h" @@ -310,16 +312,45 @@ done: return rv; } -int config_read(char* cfg_file){ +int config_read(char* cfg_filepath){ int rv = 1; size_t line_alloc = 0; ssize_t status; map_type mapping_type = map_rtl; char* line_raw = NULL, *line, *separator; - FILE* source = fopen(cfg_file, "r"); + + //create heap copy of file name because original might be in readonly memory + char* source_dir = strdup(cfg_filepath), *source_file = NULL; + #ifdef _WIN32 + char path_separator = '\\'; + #else + char path_separator = '/'; + #endif + + if(!source_dir){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + //change working directory to the one containing the configuration file so relative paths work as expected + source_file = strrchr(source_dir, path_separator); + if(source_file){ + *source_file = 0; + source_file++; + if(chdir(source_dir)){ + fprintf(stderr, "Failed to change to configuration file directory %s: %s\n", source_dir, strerror(errno)); + goto bail; + } + } + else{ + source_file = source_dir; + } + + FILE* source = fopen(source_file, "r"); + if(!source){ fprintf(stderr, "Failed to open configuration file for reading\n"); - return 1; + goto bail; } for(status = getline(&line_raw, &line_alloc, source); status >= 0; status = getline(&line_raw, &line_alloc, source)){ @@ -459,6 +490,7 @@ int config_read(char* cfg_file){ rv = 0; bail: + free(source_dir); fclose(source); free(line_raw); return rv; diff --git a/configs/flying-faders.cfg b/configs/flying-faders.cfg index 4197581..d331f38 100644 --- a/configs/flying-faders.cfg +++ b/configs/flying-faders.cfg @@ -13,7 +13,7 @@ dest = learn@9000 /1/xy = ff 0.0 1.0 0.0 1.0 [lua generator] -script = configs/flying-faders.lua +script = flying-faders.lua [map] diff --git a/configs/lua.cfg b/configs/lua.cfg index af17496..098c0f1 100644 --- a/configs/lua.cfg +++ b/configs/lua.cfg @@ -13,7 +13,7 @@ axis.ABS_X = 34300 0 65535 255 4095 axis.ABS_Y = 34300 0 65535 255 4095 [lua lua] -script = configs/demo.lua +script = demo.lua [artnet art] universe = 0 diff --git a/midimonster.h b/midimonster.h index 491cc11..b05326c 100644 --- a/midimonster.h +++ b/midimonster.h @@ -33,7 +33,18 @@ #include "portability.h" /* Default configuration file name to read when no other is specified */ -#define DEFAULT_CFG "monster.cfg" +#ifndef DEFAULT_CFG + #define DEFAULT_CFG "monster.cfg" +#endif + +/* Default backend plugin location */ +#ifndef PLUGINS + #ifndef _WIN32 + #define PLUGINS "./backends/" + #else + #define PLUGINS "backends\\" + #endif +#endif /* Forward declare some of the structs so we can use them in each other */ struct _channel_value; diff --git a/plugin.c b/plugin.c index dd99041..a14baff 100644 --- a/plugin.c +++ b/plugin.c @@ -24,13 +24,21 @@ static int plugin_attach(char* path, char* file){ plugin_init init = NULL; void* handle = NULL; char* lib = NULL; + #ifdef _WIN32 + char* path_separator = "\\"; + #else + char* path_separator = "/"; + #endif - lib = calloc(strlen(path) + strlen(file) + 1, sizeof(char)); + lib = calloc(strlen(path) + strlen(file) + 2, sizeof(char)); if(!lib){ fprintf(stderr, "Failed to allocate memory\n"); return 1; } - snprintf(lib, strlen(path) + strlen(file) + 1, "%s%s", path, file); + snprintf(lib, strlen(path) + strlen(file) + 2, "%s%s%s", + path, + (path[strlen(path)] == path_separator[0]) ? "" : path_separator, + file); handle = dlopen(lib, RTLD_NOW); if(!handle){ -- cgit v1.2.3