From 4307514383539e65a1706fc475b621cd43d7bde5 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Mon, 10 Nov 2025 01:59:22 +0000 Subject: [PATCH 01/16] Improve flex decoder by enabling lua code for validation and/or decoding. --- CMakeLists.txt | 24 ++++++ README.md | 2 +- docs/LUA.md | 90 ++++++++++++++++++++ man/man1/rtl_433.1 | 4 +- src/devices/flex.c | 208 ++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 docs/LUA.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 6dc1f7910..f39c63f4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,6 +186,30 @@ else() message(STATUS "Threads support disabled.") endif() +######################################################################## +# Find LUA dependencies +######################################################################## +# 🚀 Find Lua 5.4 +# The FindLua module will set variables like Lua_FOUND, LUA_INCLUDE_DIR, and LUA_LIBRARIES. +find_package(Lua 5.4 QUIET) + +# Check if the library was found +if(Lua_FOUND) + message(STATUS "Found Lua ${Lua_VERSION}: ${LUA_LIBRARIES}") + + # Set the preprocessor definition -DHAS_LUA=1 for C/C++ source code + # PUBLIC visibility means targets linked to 'your_project_target' will also see this definition. + ADD_DEFINITIONS(-DHAS_LUA) + + # Link the target with the found Lua libraries + list(APPEND SDR_LIBRARIES ${LUA_LIBRARIES}) + + # Add the necessary include directories + include_directories(${LUA_INCLUDE_DIR}) +else() + message(STATUS "Lua 5.4 not found. Build will proceed without Lua support.") +endif() + ######################################################################## # Find OpenSSL build dependencies ######################################################################## diff --git a/README.md b/README.md index 6c07a2ad9..ba2e24d33 100644 --- a/README.md +++ b/README.md @@ -454,8 +454,8 @@ Available options are: preamble= : match and align at the preamble is a row spec of {} unique : suppress duplicate row output - countonly : suppress detailed row output + lua : a filename containing a lua script defining validate and/or encode functions E.g. -X "n=doorbell,m=OOK_PWM,s=400,l=800,r=7000,g=1000,match={24}0xa9878c,repeats>=3" diff --git a/docs/LUA.md b/docs/LUA.md new file mode 100644 index 000000000..a91a42cec --- /dev/null +++ b/docs/LUA.md @@ -0,0 +1,90 @@ +# Enhance flex decoders with LUA + +## TL;DR + +You can provide two LUA functions to be used by a flex decoder. These are `validate` which takes a single argument +corresponding to the `bitbuffer` and returns whether it is valid. It might check a CRC for example. + +The `encode` function takes the `bitbuffer` argument and adds extra items to the decoded packet. This can be +used when the `get` argument is insufficient. + +## Building + +This extension is only built if the LUA 5.4 development environment is installed. + +## Configuration + +The flex decoder definition now takes an extra `lua` argument whose value is a filename containing the LUA code. If there +are multiple flex decoders, each of them can have their own individual files. If the LUA code file contains `require` +statements, then care should be taken as to the working directory of the `rtl_433` daemon. It may make sense to make +this the `/etc/rtl_433` directory. + +## `bitbuffer` argument + +Both functions are passed a single table containing the main parts of the `bitbuffer`. In particular, the value passed +is an array of tables. There is one inner table for each row in the signal received. Each inner table contains two keys: + +* `len` -- the bit length of the message +* `data` -- the actual message. Note that this is binary characters and NOT hex. + +## `validate` details + +This function is passed the `bitbuffer` argument and returns one of two options: + +* `bool` indicating whether the data is valid (true) or not (false). +* `table` containing the valid components of the signal in the same format as the `bitbuffer` argument + +If there are no rows in the returned table, or the boolean is false, then the validation is treated as +failed. + +### Example + +Say we have a device which transmits 16 bits where the xor of the first and second bytes is 0xff. + +``` +function validate(data) + local result = {} + for _, value in ipairs(data) do + if value.len >= 16 and value.len <= 18 then + if string.byte(value.data, 1) ^ string.byte(value.data, 2) == 255 then + value.len = 16 + table.insert(result, value) + end + end + end + return result +end +``` + +## `encode` details + +This function is passed the `bitbuffer` argument and returns a table with key/values that are added +to the data object that represents the signal detection. This allows specific decoding that cannot +be performed by the `get` decoder argument. + +### Example + +Say we have a device that transmits the 16 bits above, where the first byte is mapped to a function on the remote control: + +``` +local keyMap = { + [0x32] = "off", + [0x33] = "+", + [0x34] = "up", + [0x35] = "on", + [0x36] = "-", + [0x37] = "down" +} + +function encode(data) + res = {} + res.key = keyMap[string.byte(data[1].data)] or string.byte(data[1].data) + return res +end +``` + +## Debugging + +When writing these scripts, you can use the `print` command to put diagnostic information out to the console. + +If the script fails to parse, then the error message will be printed, and the `rtl_433` program will not start. diff --git a/man/man1/rtl_433.1 b/man/man1/rtl_433.1 index 2a9316885..76198001b 100644 --- a/man/man1/rtl_433.1 +++ b/man/man1/rtl_433.1 @@ -321,10 +321,12 @@ preamble= : match and align at the preamble .RS unique : suppress duplicate row output .RE - .RS countonly : suppress detailed row output .RE +.RS +lua : a filename containing a lua script defining validate and/or encode functions +.RE E.g. \-X "n=doorbell,m=OOK_PWM,s=400,l=800,r=7000,g=1000,match={24}0xa9878c,repeats>=3" .SS "Output format option" diff --git a/src/devices/flex.c b/src/devices/flex.c index 9b66d9a4a..9287c9b82 100644 --- a/src/devices/flex.c +++ b/src/devices/flex.c @@ -13,6 +13,11 @@ #include "optparse.h" #include "fatal.h" #include +#ifdef HAS_LUA +#include +#include +#include +#endif static inline int bit(const uint8_t *bytes, unsigned b) { @@ -100,6 +105,9 @@ struct flex_params { unsigned decode_dm; unsigned decode_mc; char const *fields[7 + GETTER_SLOTS + 1]; // NOTE: needs to match output_fields +#ifdef HAS_LUA + lua_State *L; +#endif }; static void print_row_bytes(char *row_bytes, uint8_t *bits, int num_bits) @@ -136,6 +144,173 @@ static void render_getters(data_t *data, uint8_t *bits, struct flex_params *para } } +#ifdef HAS_LUA +static void flex_lua_create_bitbuffer_table(struct flex_params *params, bitbuffer_t *bitbuffer) +{ + int i; + + lua_createtable(params->L, bitbuffer->num_rows, 0); + + for (i = 0; i < bitbuffer->num_rows; i++) { + char row_bytes[BITBUF_COLS + 1]; + + print_row_bytes(row_bytes, bitbuffer->bb[i], bitbuffer->bits_per_row[i]); + lua_createtable(params->L, 0, 2); + lua_pushstring(params->L, "len"); + lua_pushinteger(params->L, bitbuffer->bits_per_row[i]); + lua_settable(params->L, -3); + lua_pushstring(params->L, "data"); + lua_pushlstring(params->L, (const char *)bitbuffer->bb[i], (bitbuffer->bits_per_row[i] + 7) / 8); + lua_settable(params->L, -3); + + lua_rawseti(params->L, -2, i + 1); + } +} + +static int flex_lua_validate(struct flex_params *params, bitbuffer_t *bitbuffer) +{ + int rc = 1; + + if (!params->L) + return rc; + + int top = lua_gettop(params->L); + + lua_getglobal(params->L, "validate"); + if (lua_isfunction(params->L, -1)) { + // We have a validate function. Pass it the data + // It should return either a boolean or a table like the input + + flex_lua_create_bitbuffer_table(params, bitbuffer); + + lua_pcall(params->L, 1, 1, 0); + if (lua_istable(params->L, -1)) { + // overwrite the bitbuffer + int row_num = 0; + lua_pushnil(params->L); + while (lua_next(params->L, -2) != 0) { + // The value at -1 should be a table + if (lua_istable(params->L, -1)) { + // Get the length + size_t bit_len; + lua_getfield(params->L, -1, "len"); + if (lua_isinteger(params->L, -1)) { + bit_len = bitbuffer->bits_per_row[row_num] = lua_tointeger(params->L, -1); + } + else { + rc = 0; + break; + } + lua_pop(params->L, 1); + lua_getfield(params->L, -1, "data"); + if (lua_isstring(params->L, -1)) { + size_t data_len; + const char *data_ptr = lua_tolstring(params->L, -1, &data_len); + if (data_len > (bit_len + 7) / 8) { + data_len = (bit_len + 7) / 8; + } + memcpy(bitbuffer->bb[row_num], data_ptr, data_len); + } + else { + rc = 0; + break; + } + lua_pop(params->L, 1); + row_num++; + } + lua_pop(params->L, 1); // pop the value + } + bitbuffer->num_rows = row_num; + rc = bitbuffer->num_rows > 0; + } + else { + rc = lua_toboolean(params->L, -1); + } + } + lua_settop(params->L, top); + return rc; +} + +static void flex_lua_encode(struct flex_params *params, bitbuffer_t *bitbuffer, data_t *data) +{ + if (!params->L) + return; + + int top = lua_gettop(params->L); + + lua_getglobal(params->L, "encode"); + if (lua_isfunction(params->L, -1)) { + // We have an encode function. Pass it the data + flex_lua_create_bitbuffer_table(params, bitbuffer); + + lua_pcall(params->L, 1, 1, 0); + // We expect to get back a table which should be added to the data object + if (lua_istable(params->L, -1)) { + // append to data + lua_pushnil(params->L); + while (lua_next(params->L, -2) != 0) { + // Add the key at -2 and the value at -1 + const char *key = lua_tostring(params->L, -2); + int valuetype = lua_type(params->L, -1); + if (valuetype == LUA_TNUMBER) { + data_dbl(data, key, "", NULL, lua_tonumber(params->L, -1)); // data_* creates a copy of the key + } + else if (valuetype == LUA_TBOOLEAN) { + data_int(data, key, "", NULL, lua_toboolean(params->L, -1)); // data_* creates a copy of the key + } + else if (lua_istable(params->L, -1)) { + // len and data keys + size_t bit_len; + lua_getfield(params->L, -1, "len"); + if (lua_isinteger(params->L, -1)) { + bit_len = lua_tointeger(params->L, -1); + } + else { + lua_pop(params->L, 2); // get rid of table and bad value + continue; + } + lua_pop(params->L, 1); + lua_getfield(params->L, -1, "data"); + if (lua_isstring(params->L, -1)) { + size_t data_len; + const char *data_ptr = lua_tolstring(params->L, -1, &data_len); + char buffer[128 * 2 + 30]; + char *bp = buffer; + size_t i; + + if (data_len > (bit_len + 7) / 8) { + data_len = (bit_len + 7) / 8; + } + if (data_len > 128) { + data_len = 128; + } + + bp += sprintf(bp, "{%ld}", bit_len); + for (i = 0; i < data_len; i++) { + bp += sprintf(bp, "%02x", data_ptr[i] & 0xff); + } + + *bp = '\0'; + data_str(data, key, "", NULL, buffer); + } + else { + lua_pop(params->L, 2); // get rid of table and bad value + continue; + } + lua_pop(params->L, 1); + } + else if (lua_isstring(params->L, -1)) { + const char *value = lua_tostring(params->L, -1); + data_str(data, key, "", NULL, value); // data_str creates copies of the string arguments + } + lua_pop(params->L, 1); // Get rid of the value + } + } + } + lua_settop(params->L, top); +} +#endif + /** Generic flex decoder. */ @@ -271,6 +446,11 @@ static int flex_callback(r_device *decoder, bitbuffer_t *bitbuffer) } } +#ifdef HAS_LUA + if (!flex_lua_validate(params, bitbuffer)) + return DECODE_FAIL_MIC; +#endif + decoder_log_bitbuffer(decoder, 1, params->name, bitbuffer, ""); // discard duplicates @@ -290,6 +470,10 @@ static int flex_callback(r_device *decoder, bitbuffer_t *bitbuffer) // add a data line for each getter render_getters(data, bitbuffer->bb[r], params); +#ifdef HAS_LUA + flex_lua_encode(params, bitbuffer, data); +#endif + decoder_output_data(decoder, data); return 1; } @@ -342,6 +526,10 @@ static int flex_callback(r_device *decoder, bitbuffer_t *bitbuffer) NULL); /* clang-format on */ +#ifdef HAS_LUA + flex_lua_encode(params, bitbuffer, data); +#endif + decoder_output_data(decoder, data); for (i = 0; i < bitbuffer->num_rows; i++) { free(row_codes[i]); @@ -424,8 +612,14 @@ static void help(void) "\tmatch= : only match if the are found\n" "\tpreamble= : match and align at the preamble\n" "\t\t is a row spec of {}\n" - "\tunique : suppress duplicate row output\n\n" - "\tcountonly : suppress detailed row output\n\n" + "\tunique : suppress duplicate row output\n" + "\tcountonly : suppress detailed row output\n" +#ifdef HAS_LUA + "\tlua : a filename containing a lua script defining validate and/or encode functions\n" +#else + "\tlua : not supported in this build\n" +#endif + "\n" "E.g. -X \"n=doorbell,m=OOK_PWM,s=400,l=800,r=7000,g=1000,match={24}0xa9878c,repeats>=3\"\n\n"); exit(0); } @@ -748,7 +942,15 @@ r_device *flex_create_device(char *spec) fprintf(stderr, "Maximum getter slots exceeded (%d)!\n", GETTER_SLOTS); usage(); } - +#ifdef HAS_LUA + } else if (!strcasecmp(key, "lua")) { + params->L = luaL_newstate(); + luaL_openlibs(params->L); + if (luaL_dofile(params->L, val) != LUA_OK) { + fprintf(stderr, "Bad lua code: %s\n", lua_tostring(params->L, -1)); + usage(); + } +#endif } else { fprintf(stderr, "Bad flex spec, unknown keyword (%s)!\n", key); usage(); From 698689662ee4ddd521fd0d374d5ba98c66ca2fbc Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Mon, 10 Nov 2025 13:48:14 +0000 Subject: [PATCH 02/16] Fix checkstyle issues. --- src/devices/flex.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/devices/flex.c b/src/devices/flex.c index 9287c9b82..fae83f115 100644 --- a/src/devices/flex.c +++ b/src/devices/flex.c @@ -947,8 +947,8 @@ r_device *flex_create_device(char *spec) params->L = luaL_newstate(); luaL_openlibs(params->L); if (luaL_dofile(params->L, val) != LUA_OK) { - fprintf(stderr, "Bad lua code: %s\n", lua_tostring(params->L, -1)); - usage(); + fprintf(stderr, "Bad lua code: %s\n", lua_tostring(params->L, -1)); + usage(); } #endif } else { From e123a95a7d4c6ee9f8ca819fe3b3d78895694851 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Wed, 12 Nov 2025 01:34:29 +0000 Subject: [PATCH 03/16] Improved lua handling to allow inline code in the config file. --- docs/LUA.md | 28 ++++++++++++++++++++++- include/optparse.h | 15 +++++++++++++ src/devices/flex.c | 15 ++++++++++--- src/optparse.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/docs/LUA.md b/docs/LUA.md index a91a42cec..b0028abf8 100644 --- a/docs/LUA.md +++ b/docs/LUA.md @@ -12,13 +12,39 @@ used when the `get` argument is insufficient. This extension is only built if the LUA 5.4 development environment is installed. -## Configuration +## Configuration -- LUA file The flex decoder definition now takes an extra `lua` argument whose value is a filename containing the LUA code. If there are multiple flex decoders, each of them can have their own individual files. If the LUA code file contains `require` statements, then care should be taken as to the working directory of the `rtl_433` daemon. It may make sense to make this the `/etc/rtl_433` directory. +It may prove easier to develop the LUA code by referencing a file, and then use the `luacode` option if the decoder +definition is to be distributed (i.e. it is all in a single file). + +## Configuration -- inline LUA code + +The flex decoder definition now takes an extra `luacode` argument whose value is the LUA code to run. Note that some +careful escaping is required. In particular, the `decoder` must be declared using the { } syntax: + +``` +decoder { + name=wierdthing, + , + luacode=[[ + ]] +} +``` + +Note that the closing `}` and `]]` must be at the end of lines. The `}` requirement poses some problems for LUA code +as the phrase `local result = {}` which would otherwise close the `{`. However, this line can be modified to read `local result = {} --` +and this solves the quoting issue (by adding an empty LUA comment). +If the LUA code file contains `require` +statements, then care should be taken as to the working directory of the `rtl_433` daemon. It may make sense to make +this the `/etc/rtl_433` directory. In any case, it means that distributing the decoder definition now requires the externally +required file to be distributed as well. + + ## `bitbuffer` argument Both functions are passed a single table containing the main parts of the `bitbuffer`. In particular, the value passed diff --git a/include/optparse.h b/include/optparse.h index ddd58f358..a361bac39 100644 --- a/include/optparse.h +++ b/include/optparse.h @@ -138,6 +138,21 @@ int kwargs_match(char const *s, char const *key, char const **val); /// @return the next key in s, end of string or NULL otherwise char const *kwargs_skip(char const *s); +/// Parse a comma-separated list of key/value pairs into kwargs. +/// +/// The input string will be modified and the pointer advanced. +/// The key and val pointers will be into the original string. +/// Note that the val can be surrounded by [[ and ]]. Inside the +/// [[ quoting, newlines, commas, etc are allowed. The terminating +/// ]] must be at the end of a line. This really only makes sense inside +/// a { } quoted statement. +/// +/// @param[in,out] s String of key=value pairs, separated by commas +/// @param[out] key keyword argument if found, NULL otherwise +/// @param[out] val value if found, NULL otherwise +/// @return the original value of *stringp (the keyword found) +char *getkwargswithvalescape(char **s, char **key, char **val); + /// Parse a comma-separated list of key/value pairs into kwargs. /// /// The input string will be modified and the pointer advanced. diff --git a/src/devices/flex.c b/src/devices/flex.c index fae83f115..0931dbda3 100644 --- a/src/devices/flex.c +++ b/src/devices/flex.c @@ -848,7 +848,7 @@ r_device *flex_create_device(char *spec) dev->fields = output_fields; char *key, *val; - while (getkwargs(&spec, &key, &val)) { + while (getkwargswithvalescape(&spec, &key, &val)) { key = remove_ws(key); val = trim_ws(val); @@ -943,10 +943,19 @@ r_device *flex_create_device(char *spec) usage(); } #ifdef HAS_LUA - } else if (!strcasecmp(key, "lua")) { + } + else if (!strcasecmp(key, "luacode")) { + params->L = luaL_newstate(); + luaL_openlibs(params->L); + if (!val || luaL_dostring(params->L, val) != LUA_OK) { + fprintf(stderr, "Bad lua code: %s\n", lua_tostring(params->L, -1)); + usage(); + } + } + else if (!strcasecmp(key, "lua")) { params->L = luaL_newstate(); luaL_openlibs(params->L); - if (luaL_dofile(params->L, val) != LUA_OK) { + if (!val || luaL_dofile(params->L, val) != LUA_OK) { fprintf(stderr, "Bad lua code: %s\n", lua_tostring(params->L, -1)); usage(); } diff --git a/src/optparse.c b/src/optparse.c index e4e0e1d37..ddb18ba40 100644 --- a/src/optparse.c +++ b/src/optparse.c @@ -366,6 +366,61 @@ char const *kwargs_skip(char const *s) return s; } +char *getkwargswithvalescape(char **s, char **key, char **val) +{ + // Find the = and see if next token is [[. + // if not, then chain to old code + if (s && *s) { + char *eq = strchr(*s, '='); + if (eq) { + char *v = eq + 1; + while (*v == ' ' || *v == '\t') + v++; + + if (v[0] == '[' && v[1] == '[') { + // We are in the escaping mode + char *k = *s; + *eq = '\0'; // null terminate the key + v += 2; + + char *vend = v; + while (vend[0]) { + while (vend[0] && (vend[0] != ']' || vend[1] != ']')) { + vend++; + } + // exit condition is end of input data or at ']]' + if (vend[0]) { + char *veol = vend + 2; + while (*veol == ' ' || *veol == '\t') { + veol++; + } + if (*veol == '\n' || *veol == '\r') + break; + vend = veol; + } + else { + break; + } + } + + if (*vend) { + // Must be pointing to ]] + *vend = '\0'; + vend += 2; + } + + if (key) + *key = k; + if (val) + *val = v; + *s = vend; + return k; + } + } + } + return getkwargs(s, key, val); +} + char *getkwargs(char **s, char **key, char **val) { char *v = asepc(s, ','); From 25acdbabb8eb39aab56a8c20034e699e6637aab7 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Mon, 24 Nov 2025 00:12:53 +0000 Subject: [PATCH 04/16] Updated the documentation and changed the approach to have nice accessor methods. See the docs for details. --- docs/LUA.md | 119 +++++++++++++++++++--- src/devices/flex.c | 244 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 330 insertions(+), 33 deletions(-) diff --git a/docs/LUA.md b/docs/LUA.md index b0028abf8..2a3173b6a 100644 --- a/docs/LUA.md +++ b/docs/LUA.md @@ -5,7 +5,7 @@ You can provide two LUA functions to be used by a flex decoder. These are `validate` which takes a single argument corresponding to the `bitbuffer` and returns whether it is valid. It might check a CRC for example. -The `encode` function takes the `bitbuffer` argument and adds extra items to the decoded packet. This can be +The `decode` function takes the `bitbuffer` argument and adds extra items to the decoded packet. This can be used when the `get` argument is insufficient. ## Building @@ -48,20 +48,35 @@ required file to be distributed as well. ## `bitbuffer` argument Both functions are passed a single table containing the main parts of the `bitbuffer`. In particular, the value passed -is an array of tables. There is one inner table for each row in the signal received. Each inner table contains two keys: +is an array of tables. There is one BitBuffer object for each row in the signal received. The BitBuffer object behaves like +a String -- all the String methods work on a BitBuffer. It has the following additional methods: -* `len` -- the bit length of the message -* `data` -- the actual message. Note that this is binary characters and NOT hex. +* `bitlen()` -- returns the bit length of the message +* `little_endian_buffer(le)` -- sets the byte ordering of the buffer (default big-endian) +* `little_endian_value(le)` -- sets the bit ordering of the returned values (default big-endian) +* `signed(sgn)` -- sets if the returned value should be a signed integer (default unsigned) + +It also supports bit index to extract values. + +```lua +local s = BitBuffer.new(string.char(0xc5, 0x6a), 16); +assert(s[{4, 8}] == 0x56, "basic check") +``` + +The syntax above is a field extraction operator -- the first value is the bit offset from the start of the message, and the second +value is the number of bits to extract. You can also include values of `little_endian_buffer`, `little_endian_value`, `signed` as other +entries in the index table if you want to override the current settings for a single call. ## `validate` details -This function is passed the `bitbuffer` argument and returns one of two options: +This function is passed the array of `BitBuffer` arguments and returns one of two options: * `bool` indicating whether the data is valid (true) or not (false). -* `table` containing the valid components of the signal in the same format as the `bitbuffer` argument +* `table` containing the valid components of the signal in the same format as the `BitBuffer` argument If there are no rows in the returned table, or the boolean is false, then the validation is treated as -failed. +failed. If you want to construct a new set of `BitBuffer` objects for the return table, then you can call the `BitBuffer.new` function +which takes a string and a bit length as arguments. ### Example @@ -71,9 +86,8 @@ Say we have a device which transmits 16 bits where the xor of the first and seco function validate(data) local result = {} for _, value in ipairs(data) do - if value.len >= 16 and value.len <= 18 then - if string.byte(value.data, 1) ^ string.byte(value.data, 2) == 255 then - value.len = 16 + if value:bitlen() >= 16 and value:bitlen() <= 18 then + if value[{0, 8}] ^ value[{8,8}] == 255 then table.insert(result, value) end end @@ -82,9 +96,9 @@ function validate(data) end ``` -## `encode` details +## `decode` details -This function is passed the `bitbuffer` argument and returns a table with key/values that are added +This function is passed the `BitBuffer` argument and returns a table with key/values that are added to the data object that represents the signal detection. This allows specific decoding that cannot be performed by the `get` decoder argument. @@ -102,15 +116,92 @@ local keyMap = { [0x37] = "down" } -function encode(data) +function decode(data) res = {} - res.key = keyMap[string.byte(data[1].data)] or string.byte(data[1].data) + res.key = keyMap[data[1][{0,8}] or data[1][{0, 8}] return res end ``` +This indexing is actually just syntactic sugar over the `getbits` call which is described below. + +## :getbits method + +The `getbits` method is available on a `BitBuffer`. This takes a table as an argument and +returns an integer which is the value of the selected, consecutive, bits. The table keys are: + +* `offset`: an integer which is the offset into the packet of the start of the bitfield +* `width`: the number of consecutive bits to return (no more than will fit into an integer) +* `little_endian_buffer`: a boolean set to true if the buffer is to be treated as little endian +* `little_endian_value`: a boolesn set to true if the result is to be treated as little endian. +* `signed`: a boolean to indicate if the returned value should be signed. + +The `getbits` method treats the supplied bitbuffer as a sequence of bits. By default, the most significant bit of the first byte is considered to be bit 0. If the `little_endian_buffer` is set to true, then the least significant bit of the first byte is considered to be bit 0. + +Once the required number of consecutive bits have been extracted, they are converted to an integer. By default, the first bit extracted is the bit with the highest value in the returned integer. If the `little_endian_value` is set, then the first bit extracted is the `1` bit in the returned integer. + +## BitBuffer.new + +This function creates a new `BitBuffer`. It is called with two arguments: + +* A string containing the bytes of the packet +* An integer indicating the number of bits in the packet. + +It is an error if the number of bits exceeds the size of the string. + +Two `BitBuffer` objects compare equal if they both have the same data string and bit length. + ## Debugging When writing these scripts, you can use the `print` command to put diagnostic information out to the console. If the script fails to parse, then the error message will be printed, and the `rtl_433` program will not start. + +If you want to test your `validate` or `decode` functions, then you can just call them after defining them with specially constructed `BitBuffer` objects. + +## Complete Example + + +``` +decoder { + n=remote_rc1,m=OOK_PWM,s=312,l=1072,r=6820,g=6000,t=304,y=0,unique,bits=16, + luacode=[[ +print("Loading validate && decode") +function validate(data) + local result = {} -- + for _, buffer in ipairs(data) do + if buffer:bitlen() >= 16 and buffer:bitlen() <= 18 then + if buffer[{0, 8}] + buffer[{8, 8}] == 255 then + table.insert(result, BitBuffer.new(buffer, 16)) + end + end + end + return result +end + +local keyMap = { + [0x32] = "off", + [0x33] = "+", + [0x34] = "up", + [0x35] = "on", + [0x36] = "-", + [0x37] = "down" +} -- + +function decode(data) + res = {} -- + res.key = keyMap[data[1][{8, 8}]] or data[1][{8, 8}] + return res +end +]] +} +``` + +There is the slight hassle that a `}` at the end of the line in the lua block causes parsing problems, so you can just put a `--` after it to start a comment. If +you wanted to test your functions, you could add the following code + +``` +local packet = BitBuffer.new(string.char(0xCB, 0x34), 16) +assert(validate({packet})[1] == packet, "Incorrect packet validation") +assert(decode({packet}).key == "up", "Key value not 'up'") +``` diff --git a/src/devices/flex.c b/src/devices/flex.c index 0931dbda3..3138600a8 100644 --- a/src/devices/flex.c +++ b/src/devices/flex.c @@ -17,6 +17,68 @@ #include #include #include + +static const char *lua_bitbuffer = "(function () \n" + "BitBuffer = {} \n" + "local BitBuffer_mt = { \n" + "__eq = function(bb1, bb2) return bb1._value == bb2._value and bb1._bitlen == bb2._bitlen end \n" + "} \n" + "function BitBuffer.new(str, bitlen) \n" + "local self = { \n" + "_value = tostring(str), \n" + "_bitlen = bitlen, \n" + "_signed = false, \n" + "_little_endian_buffer = false, \n" + "_little_endian_value = false \n" + "} \n" + "assert(str:len() * 8 >= bitlen, 'Bit length exceeds string length') \n" + "return setmetatable(self, BitBuffer_mt) \n" + "end \n" + "function BitBuffer_mt.little_endian_buffer(self, le) \n" + "if type(le) == \"boolean\" then \n" + "self._little_endian_buffer = le \n" + "end \n" + "return self._little_endian_buffer \n" + "end \n" + "function BitBuffer_mt.little_endian_value(self, le) \n" + "if type(le) == \"boolean\" then \n" + "self._little_endian_value = le \n" + "end \n" + "return self._little_endian_value \n" + "end \n" + "function BitBuffer_mt.signed(self, sgn) \n" + "if type(sgn) == \"boolean\" then \n" + "self._signed = sgn \n" + "end \n" + "return self._signed \n" + "end \n" + "function BitBuffer_mt.bitlen(self) \n" + "return self._bitlen \n" + "end \n" + "function BitBuffer_mt.__tostring(self) \n" + "return self._value \n" + "end \n" + "function ifnil(a, b) if a == nil then return b end return a end \n" + "function BitBuffer_mt.__index(self, key) \n" + "if type(key) == \"table\" then \n" + "local args={offset=key[1],width=key[2],signed=ifnil(key.signed, self._signed),little_endian_buffer=ifnil(key.little_endian_buffer,self._little_endian_buffer),little_endian_value=ifnil(key.little_endian_value,self._little_endian_value)} \n" + "return BitBuffer.getbits(self._value, args) \n" + "end \n" + "local method = BitBuffer_mt[key] \n" + "if method == nil then method = BitBuffer[key] end \n" + "if method ~= nil then \n" + "return method \n" + "end \n" + "local string_method = string[key] \n" + "if type(string_method) == \"function\" then \n" + "return function(passed_self, ...) \n" + "return string_method(self._value, ...) \n" + "end \n" + "end \n" + "return nil \n" + "end \n" + "end) () \n" + ; #endif static inline int bit(const uint8_t *bytes, unsigned b) @@ -150,21 +212,19 @@ static void flex_lua_create_bitbuffer_table(struct flex_params *params, bitbuffe int i; lua_createtable(params->L, bitbuffer->num_rows, 0); + lua_getglobal(params->L, "BitBuffer"); for (i = 0; i < bitbuffer->num_rows; i++) { - char row_bytes[BITBUF_COLS + 1]; + lua_getfield(params->L, -1, "new"); - print_row_bytes(row_bytes, bitbuffer->bb[i], bitbuffer->bits_per_row[i]); - lua_createtable(params->L, 0, 2); - lua_pushstring(params->L, "len"); - lua_pushinteger(params->L, bitbuffer->bits_per_row[i]); - lua_settable(params->L, -3); - lua_pushstring(params->L, "data"); lua_pushlstring(params->L, (const char *)bitbuffer->bb[i], (bitbuffer->bits_per_row[i] + 7) / 8); - lua_settable(params->L, -3); + lua_pushinteger(params->L, bitbuffer->bits_per_row[i]); + + lua_pcall(params->L, 2, 1, 0); - lua_rawseti(params->L, -2, i + 1); + lua_rawseti(params->L, -3, i + 1); } + lua_pop(params->L, 1); } static int flex_lua_validate(struct flex_params *params, bitbuffer_t *bitbuffer) @@ -193,7 +253,7 @@ static int flex_lua_validate(struct flex_params *params, bitbuffer_t *bitbuffer) if (lua_istable(params->L, -1)) { // Get the length size_t bit_len; - lua_getfield(params->L, -1, "len"); + lua_getfield(params->L, -1, "_bitlen"); if (lua_isinteger(params->L, -1)) { bit_len = bitbuffer->bits_per_row[row_num] = lua_tointeger(params->L, -1); } @@ -202,7 +262,7 @@ static int flex_lua_validate(struct flex_params *params, bitbuffer_t *bitbuffer) break; } lua_pop(params->L, 1); - lua_getfield(params->L, -1, "data"); + lua_getfield(params->L, -1, "_value"); if (lua_isstring(params->L, -1)) { size_t data_len; const char *data_ptr = lua_tolstring(params->L, -1, &data_len); @@ -231,16 +291,16 @@ static int flex_lua_validate(struct flex_params *params, bitbuffer_t *bitbuffer) return rc; } -static void flex_lua_encode(struct flex_params *params, bitbuffer_t *bitbuffer, data_t *data) +static void flex_lua_decode(struct flex_params *params, bitbuffer_t *bitbuffer, data_t *data) { if (!params->L) return; int top = lua_gettop(params->L); - lua_getglobal(params->L, "encode"); + lua_getglobal(params->L, "decode"); if (lua_isfunction(params->L, -1)) { - // We have an encode function. Pass it the data + // We have an decode function. Pass it the data flex_lua_create_bitbuffer_table(params, bitbuffer); lua_pcall(params->L, 1, 1, 0); @@ -261,7 +321,7 @@ static void flex_lua_encode(struct flex_params *params, bitbuffer_t *bitbuffer, else if (lua_istable(params->L, -1)) { // len and data keys size_t bit_len; - lua_getfield(params->L, -1, "len"); + lua_getfield(params->L, -1, "_bitlen"); if (lua_isinteger(params->L, -1)) { bit_len = lua_tointeger(params->L, -1); } @@ -270,7 +330,7 @@ static void flex_lua_encode(struct flex_params *params, bitbuffer_t *bitbuffer, continue; } lua_pop(params->L, 1); - lua_getfield(params->L, -1, "data"); + lua_getfield(params->L, -1, "_value"); if (lua_isstring(params->L, -1)) { size_t data_len; const char *data_ptr = lua_tolstring(params->L, -1, &data_len); @@ -309,6 +369,148 @@ static void flex_lua_encode(struct flex_params *params, bitbuffer_t *bitbuffer, } lua_settop(params->L, top); } + + +static int flex_lua_get_value(lua_State *L, int table, const char *key, int default_value) { + lua_pushstring(L, key); + lua_rawget(L, table); + int isnum; + int val = lua_tointegerx(L, -1, &isnum); + if (!isnum) { + if (lua_type(L, -1) == LUA_TBOOLEAN) { + val = lua_toboolean(L, -1); + } else { + val = default_value; + } + } + lua_pop(L, 1); + return val; +} + +static int flex_lua_getbits(lua_State *L) { + // This takes the string, a table with the following keys: + // offset (the bit offset) + // width (the bit width) + // signed if the result is signed (default unsigned) + // little_endian sets default for next two values + // little_endian_buffer if buffer starts at lsb of first byte + // little_endian_value if least significant bit of result is first + + // Endianness + // + // 7 .... 0 7 .... 0 + // 11000101 01101010 + // buffer / value + // BIG/BIG offset 4 width 8 => 01010110 => 0x56 + // LITTLE/LITTLE offset 4 with 8 => 00110101 => 10101100 => 0xAC + // BIG/LITTLE offset 4 width 8 => 01101010 => 0x6A + // LITTLE/BIG offset 4 width 8 => 0x35 + + size_t data_len; + size_t bit_len = 0; + if (lua_type(L, 1) == LUA_TTABLE) { + // We can get the bit length + lua_getfield(L, 1, "_bitlen"); + if (lua_isinteger(L, -1)) { + bit_len = lua_tointeger(L, -1); + } + lua_pop(L, 1); + } + const char *data = luaL_tolstring(L, 1, &data_len); + + if (!bit_len || bit_len > data_len * 8) { + bit_len = data_len * 8; + } + + if (!lua_istable(L, 2)) { + luaL_error(L, "Argument 2 is not a table"); + } + + int offset = flex_lua_get_value(L, 2, "offset", -1); + if (offset < 0) { + luaL_error(L, "Invalid offset"); + } + + int width = flex_lua_get_value(L, 2, "width", -1); + if (width < 0) { + luaL_error(L, "Invalid width"); + } + + int issigned = flex_lua_get_value(L, 2, "signed", 0); + + int little_endian = flex_lua_get_value(L, 2, "little_endian", 0); + int little_endian_buffer = flex_lua_get_value(L, 2, "little_endian_buffer", little_endian); + int little_endian_value = flex_lua_get_value(L, 2, "little_endian_value", little_endian); + + long result = 0; + + int result_bits = 0; + + if ((size_t) (offset + width) > bit_len) { + luaL_error(L, "Offset + width is too large for this string"); + } + + while (width > 0) { + const unsigned int byte_off = (unsigned) offset >> 3; + // We know that this is OK due to the offset+width check above. + const unsigned char b = data[byte_off]; + int bits = 8 - (offset & 7); + if (!little_endian_buffer) { + // We want the bottom bits, but no more than + unsigned int val = b & ((1 << bits) - 1); + if (bits > width) { + val >>= (bits - width); + bits = width; + } + result = (result << bits) | val; + } else { + // We want the top bits shuffled to the bottom + unsigned int val = b >> (8 - bits); + if (width < 8) { + bits = width; + val &= (1 << width) - 1; + } + result |= val << result_bits; + } + result_bits += bits; + offset += bits; + width -= bits; + } + + if (!little_endian_value != !little_endian_buffer && result_bits > 1) { + // Flip the bits end to end + long flipped_result = 0; + long bitmask = 1 << (result_bits - 1); + const long mask = 1 << (result_bits - 1); + + for (; bitmask; bitmask >>= 1) { + flipped_result >>= 1; + if (result & bitmask) { + flipped_result |= mask; + } + } + result = flipped_result; + } + + if (issigned && result_bits > 0) { + result = (result << (64 - result_bits)) >> (64 - result_bits); + } + + lua_pushinteger(L, result); + + return 1; +} + +static void flex_lua_register(lua_State *L) { + if (luaL_dostring(L, lua_bitbuffer) != LUA_OK) { + fprintf(stderr, "Bad lua code: %s\n", lua_tostring(L, -1)); + } + lua_getglobal(L, "BitBuffer"); + lua_pushcfunction(L, flex_lua_getbits); + lua_setfield(L, -2, "getbits"); + lua_pop(L, 1); +} + #endif /** @@ -471,7 +673,7 @@ static int flex_callback(r_device *decoder, bitbuffer_t *bitbuffer) render_getters(data, bitbuffer->bb[r], params); #ifdef HAS_LUA - flex_lua_encode(params, bitbuffer, data); + flex_lua_decode(params, bitbuffer, data); #endif decoder_output_data(decoder, data); @@ -527,7 +729,7 @@ static int flex_callback(r_device *decoder, bitbuffer_t *bitbuffer) /* clang-format on */ #ifdef HAS_LUA - flex_lua_encode(params, bitbuffer, data); + flex_lua_decode(params, bitbuffer, data); #endif decoder_output_data(decoder, data); @@ -615,9 +817,11 @@ static void help(void) "\tunique : suppress duplicate row output\n" "\tcountonly : suppress detailed row output\n" #ifdef HAS_LUA - "\tlua : a filename containing a lua script defining validate and/or encode functions\n" + "\tlua : a filename containing a lua script defining validate and/or decode functions\n" + "\tluacode : a lua script defining validate and/or decode functions\n" #else "\tlua : not supported in this build\n" + "\tluacode : not supported in this build\n" #endif "\n" "E.g. -X \"n=doorbell,m=OOK_PWM,s=400,l=800,r=7000,g=1000,match={24}0xa9878c,repeats>=3\"\n\n"); @@ -947,6 +1151,7 @@ r_device *flex_create_device(char *spec) else if (!strcasecmp(key, "luacode")) { params->L = luaL_newstate(); luaL_openlibs(params->L); + flex_lua_register(params->L); if (!val || luaL_dostring(params->L, val) != LUA_OK) { fprintf(stderr, "Bad lua code: %s\n", lua_tostring(params->L, -1)); usage(); @@ -955,6 +1160,7 @@ r_device *flex_create_device(char *spec) else if (!strcasecmp(key, "lua")) { params->L = luaL_newstate(); luaL_openlibs(params->L); + flex_lua_register(params->L); if (!val || luaL_dofile(params->L, val) != LUA_OK) { fprintf(stderr, "Bad lua code: %s\n", lua_tostring(params->L, -1)); usage(); From d4c78d13565b12ecbd32d3a8306df120a62c51d7 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Mon, 24 Nov 2025 01:02:03 +0000 Subject: [PATCH 05/16] Add tests to ensure that the LUA stuff is working correctly --- tests/CMakeLists.txt | 5 +++ tests/lua_test.conf | 76 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 tests/lua_test.conf diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 95fe7a895..e107f25d3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,6 +33,11 @@ endforeach(testSrc) ######################################################################## add_test(rtl_433_help ../src/rtl_433 -h) +find_package(Lua 5.4 QUIET) +if (Lua_FOUND) + add_test(rtl_433_lua ../src/rtl_433 -r /dev/null -c ../../tests/lua_test.conf) +endif() + ######################################################################## # Define style checks ######################################################################## diff --git a/tests/lua_test.conf b/tests/lua_test.conf new file mode 100644 index 000000000..e43078e20 --- /dev/null +++ b/tests/lua_test.conf @@ -0,0 +1,76 @@ +decoder { + n=remote_1,m=OOK_PPM,s=312,l=1072,r=6820,g=6000,t=304,y=0, + luacode=[[ + print("Loading validate && decode") + function validate(data) + local result = {} -- + for _, buffer in ipairs(data) do + if buffer:bitlen() >= 16 and buffer:bitlen() <= 18 then + if buffer[{0, 8}] + buffer[{8, 8}] == 255 then + table.insert(result, BitBuffer.new(buffer, 16)) + end + end + end + return result + end + + local keyMap = { + [0x32] = "off", + [0x33] = "+", + [0x34] = "up", + [0x35] = "on", + [0x36] = "-", + [0x37] = "down" + } -- + + function decode(data) + res = {} -- + res.key = keyMap[data[1][{8, 8}]] or data[1][{8, 8}] + return res + end + local packet = BitBuffer.new(string.char(0xCB, 0x34), 16) + assert(validate({packet})[1] == packet, "Incorrect packet validation") + assert(decode({packet}).key == "up", "Key value not 'up'") +]] +} + +decoder { + n=remote_2,m=OOK_PPM,s=312,l=1072,r=6820,g=6000,t=304,y=0, + luacode=[[ + -- 11000101 01101010 + local s = BitBuffer.new(string.char(0xc5, 0x6a), 16); + + assert(s:bitlen() == 16, "Size check") + + function doit (p, s, o, w, b, v, r) + assert(s[{o,w, little_endian_value=v, little_endian_buffer=b}] == r, "invalid extraction") + s:little_endian_buffer(b) + s:little_endian_value(v) + print(p, string.format("0x%X 0x%X", s[{o, w}], r)) + assert(s[{o,w}] == r, "invalid extraction") + local rsigned = r + if r & (1 << (w - 1)) ~= 0 then + rsigned = r | (-1 << w) + end + + assert(s[{o,w,signed=1}] == rsigned, "invalid signed extraction") + assert(s:getbits({offset=o,width=w,little_endian_buffer=b,little_endian_value=v}) == r, "invalid getbits result") + end + + doit("big/big", s, 4, 8, false, false, 0x56) + doit("little/little", s, 4, 8, true, true, 0xAC) + doit("big/little", s, 4, 8, false, true, 0x6A) + doit("little/big", s, 4, 8, true, false, 0x35) + + -- 11000101 01101010 01111011 + -- BIG: 001 0101 1010 1001 + -- LTL: 000 1101 0101 1011 + s = BitBuffer.new(string.char(0xc5, 0x6a, 0x7b), 24) + + doit("big/big", s, 3, 15, false, false, 0x15a9) + doit("little/little", s, 3, 15, true, true, 0x6d58) + doit("big/little", s, 3, 15, false, true, 0x4ad4) + doit("little/big", s, 3, 15, true, false, 0xd5b) + +]] +} From eaef5af3ad438351a253ab0e4fff1bcb5a2dc29a Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Mon, 24 Nov 2025 02:21:41 +0000 Subject: [PATCH 06/16] Added support for negative widths (and added tests for it). Also the width now defaults to 1 bit. --- src/devices/flex.c | 15 ++++++++++----- tests/lua_test.conf | 1 + 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/devices/flex.c b/src/devices/flex.c index 3138600a8..6659f18a7 100644 --- a/src/devices/flex.c +++ b/src/devices/flex.c @@ -431,10 +431,8 @@ static int flex_lua_getbits(lua_State *L) { luaL_error(L, "Invalid offset"); } - int width = flex_lua_get_value(L, 2, "width", -1); - if (width < 0) { - luaL_error(L, "Invalid width"); - } + // Width defaults to 1 + int width = flex_lua_get_value(L, 2, "width", 1); int issigned = flex_lua_get_value(L, 2, "signed", 0); @@ -446,8 +444,15 @@ static int flex_lua_getbits(lua_State *L) { int result_bits = 0; + if (width < 0) { + // Realign to the start and flip the value bit order + offset += width + 1; + width = -width; + little_endian_value = !little_endian_value; + } + if ((size_t) (offset + width) > bit_len) { - luaL_error(L, "Offset + width is too large for this string"); + luaL_error(L, "Bitfield outside this string"); } while (width > 0) { diff --git a/tests/lua_test.conf b/tests/lua_test.conf index e43078e20..551bc8329 100644 --- a/tests/lua_test.conf +++ b/tests/lua_test.conf @@ -55,6 +55,7 @@ decoder { assert(s[{o,w,signed=1}] == rsigned, "invalid signed extraction") assert(s:getbits({offset=o,width=w,little_endian_buffer=b,little_endian_value=v}) == r, "invalid getbits result") + assert(s[{o+w-1, -w, little_endian_value=not v}] == r, "failed negative bitwidth") end doit("big/big", s, 4, 8, false, false, 0x56) From 7e5032856f6f77312c61b3d490bc130b39582e9d Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Wed, 26 Nov 2025 01:49:47 +0000 Subject: [PATCH 07/16] Add support for single bit extraction --- docs/LUA.md | 29 ++++++++++++++++++++++++----- src/devices/flex.c | 4 ++++ tests/lua_test.conf | 3 +++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/LUA.md b/docs/LUA.md index 2a3173b6a..1b678a9c5 100644 --- a/docs/LUA.md +++ b/docs/LUA.md @@ -61,11 +61,29 @@ It also supports bit index to extract values. ```lua local s = BitBuffer.new(string.char(0xc5, 0x6a), 16); assert(s[{4, 8}] == 0x56, "basic check") +assert(s[8] == 0, "single bit check") ``` The syntax above is a field extraction operator -- the first value is the bit offset from the start of the message, and the second value is the number of bits to extract. You can also include values of `little_endian_buffer`, `little_endian_value`, `signed` as other -entries in the index table if you want to override the current settings for a single call. +entries in the index table if you want to override the current settings for a single call. To access a single bit, you can +just provide the offset. This will be interpreted according to the current setting of `:little_endian_buffer()`. + +## Bit ordering + +There are two ways to think about numbering bits in a sequence of bytes: + +* big-endian-buffer: the first bit (#0) is the *most* significant bit in the first byte. Bit #8 is the *most* significant bit in the second byte. +* little-endian-buffer: the first bit (#0) is the *least* significant bit in the first byte. Bit #8 is the *least* significant bit in the second byte. + +This has implications when cross byte boundaries. The field extraction operator always returns a consecutive sequence of bits according to the endianness above. + +Once the sequence of bits has been extracted, there are two ways to turn it into an integer: + +* big-endian-value: the first extracted bit is the *most* significant bit in the returned integer. +* little-endian-value: the first extracted bit is the *least* significant bit in the returned integer. + + ## `validate` details @@ -136,21 +154,22 @@ returns an integer which is the value of the selected, consecutive, bits. The ta * `little_endian_value`: a boolesn set to true if the result is to be treated as little endian. * `signed`: a boolean to indicate if the returned value should be signed. -The `getbits` method treats the supplied bitbuffer as a sequence of bits. By default, the most significant bit of the first byte is considered to be bit 0. If the `little_endian_buffer` is set to true, then the least significant bit of the first byte is considered to be bit 0. - -Once the required number of consecutive bits have been extracted, they are converted to an integer. By default, the first bit extracted is the bit with the highest value in the returned integer. If the `little_endian_value` is set, then the first bit extracted is the `1` bit in the returned integer. +The `getbits` method treats the supplied bitbuffer as a sequence of bits according to the `little_endian_buffer` setting. +Once the required number of consecutive bits have been extracted, they are converted to an integer according to the `little_endian_value` setting. ## BitBuffer.new This function creates a new `BitBuffer`. It is called with two arguments: -* A string containing the bytes of the packet +* A string containing the bytes of the packet or another BitBuffer object. * An integer indicating the number of bits in the packet. It is an error if the number of bits exceeds the size of the string. Two `BitBuffer` objects compare equal if they both have the same data string and bit length. +The `tostring` function converts a `BitBuffer` to the string that it was created with. + ## Debugging When writing these scripts, you can use the `print` command to put diagnostic information out to the console. diff --git a/src/devices/flex.c b/src/devices/flex.c index 6659f18a7..ca9273554 100644 --- a/src/devices/flex.c +++ b/src/devices/flex.c @@ -64,6 +64,10 @@ static const char *lua_bitbuffer = "(function () \n" "local args={offset=key[1],width=key[2],signed=ifnil(key.signed, self._signed),little_endian_buffer=ifnil(key.little_endian_buffer,self._little_endian_buffer),little_endian_value=ifnil(key.little_endian_value,self._little_endian_value)} \n" "return BitBuffer.getbits(self._value, args) \n" "end \n" + "if type(key) == \"number\" then \n" + "local args={offset=key,width=1,little_endian_buffer=self._little_endian_buffer} \n" + "return BitBuffer.getbits(self._value, args) \n" + "end \n" "local method = BitBuffer_mt[key] \n" "if method == nil then method = BitBuffer[key] end \n" "if method ~= nil then \n" diff --git a/tests/lua_test.conf b/tests/lua_test.conf index 551bc8329..848b18901 100644 --- a/tests/lua_test.conf +++ b/tests/lua_test.conf @@ -42,6 +42,9 @@ decoder { assert(s:bitlen() == 16, "Size check") + assert(s[0] == 1, "single bit access") + assert(s[11] == 0, "single bit access") + function doit (p, s, o, w, b, v, r) assert(s[{o,w, little_endian_value=v, little_endian_buffer=b}] == r, "invalid extraction") s:little_endian_buffer(b) From 6b5b9a1682dc1d9542c162198cb9d603ea682540 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Wed, 26 Nov 2025 02:12:46 +0000 Subject: [PATCH 08/16] Added documentation on the negative width and added a test case --- docs/LUA.md | 7 +++++++ tests/lua_test.conf | 1 + 2 files changed, 8 insertions(+) diff --git a/docs/LUA.md b/docs/LUA.md index 1b678a9c5..89ec158b5 100644 --- a/docs/LUA.md +++ b/docs/LUA.md @@ -62,6 +62,7 @@ It also supports bit index to extract values. local s = BitBuffer.new(string.char(0xc5, 0x6a), 16); assert(s[{4, 8}] == 0x56, "basic check") assert(s[8] == 0, "single bit check") +assert(s[{11, -8}] == 0x6A, "reversed extract") ``` The syntax above is a field extraction operator -- the first value is the bit offset from the start of the message, and the second @@ -69,6 +70,12 @@ value is the number of bits to extract. You can also include values of `little_e entries in the index table if you want to override the current settings for a single call. To access a single bit, you can just provide the offset. This will be interpreted according to the current setting of `:little_endian_buffer()`. +If the width is negative, then the bit extraction proceeds in the opposite direction. E.g. in the case above, `{11, -8}` means to +start at bit 11 and work backwards to bit 4 (inclusive). This gives you the bit reversed value to `{4, 8}`. Effectively, `{11, -8}` is +exactly the same as `{4, 8}` except that the `little_endian_value` setting is inverted. + + + ## Bit ordering There are two ways to think about numbering bits in a sequence of bytes: diff --git a/tests/lua_test.conf b/tests/lua_test.conf index 848b18901..1faea5f58 100644 --- a/tests/lua_test.conf +++ b/tests/lua_test.conf @@ -44,6 +44,7 @@ decoder { assert(s[0] == 1, "single bit access") assert(s[11] == 0, "single bit access") + assert(s[{11, -8}] == 0x6a, "reversed access") function doit (p, s, o, w, b, v, r) assert(s[{o,w, little_endian_value=v, little_endian_buffer=b}] == r, "invalid extraction") From cd375e03a21990ed0322f7edddb06493762b3925 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Wed, 26 Nov 2025 02:14:35 +0000 Subject: [PATCH 09/16] Updated the man page through maintainer_update --- README.md | 3 ++- man/man1/rtl_433.1 | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba2e24d33..f43b872d8 100644 --- a/README.md +++ b/README.md @@ -455,7 +455,8 @@ Available options are: is a row spec of {} unique : suppress duplicate row output countonly : suppress detailed row output - lua : a filename containing a lua script defining validate and/or encode functions + lua : a filename containing a lua script defining validate and/or decode functions + luacode : a lua script defining validate and/or decode functions E.g. -X "n=doorbell,m=OOK_PWM,s=400,l=800,r=7000,g=1000,match={24}0xa9878c,repeats>=3" diff --git a/man/man1/rtl_433.1 b/man/man1/rtl_433.1 index 76198001b..a5d455efa 100644 --- a/man/man1/rtl_433.1 +++ b/man/man1/rtl_433.1 @@ -325,7 +325,10 @@ unique : suppress duplicate row output countonly : suppress detailed row output .RE .RS -lua : a filename containing a lua script defining validate and/or encode functions +lua : a filename containing a lua script defining validate and/or decode functions +.RE +.RS +luacode : a lua script defining validate and/or decode functions .RE E.g. \-X "n=doorbell,m=OOK_PWM,s=400,l=800,r=7000,g=1000,match={24}0xa9878c,repeats>=3" From d9c44f7230fd689c086292153e9e4bfd0d5027be Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Wed, 26 Nov 2025 02:20:04 +0000 Subject: [PATCH 10/16] Add note about multiple decoders --- docs/LUA.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/LUA.md b/docs/LUA.md index 89ec158b5..cc1c6ffd0 100644 --- a/docs/LUA.md +++ b/docs/LUA.md @@ -22,6 +22,9 @@ this the `/etc/rtl_433` directory. It may prove easier to develop the LUA code by referencing a file, and then use the `luacode` option if the decoder definition is to be distributed (i.e. it is all in a single file). +> [NOTE!] +> You can have multiple decoders using LUA code. Each decoder uses its own environment and there is no sharing of anything between different decoders. + ## Configuration -- inline LUA code The flex decoder definition now takes an extra `luacode` argument whose value is the LUA code to run. Note that some From 86828506f5fb99f64a6be230e1c3348bb8c11037 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Wed, 26 Nov 2025 02:22:00 +0000 Subject: [PATCH 11/16] Fix typo --- docs/LUA.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/LUA.md b/docs/LUA.md index cc1c6ffd0..763226e5c 100644 --- a/docs/LUA.md +++ b/docs/LUA.md @@ -22,7 +22,7 @@ this the `/etc/rtl_433` directory. It may prove easier to develop the LUA code by referencing a file, and then use the `luacode` option if the decoder definition is to be distributed (i.e. it is all in a single file). -> [NOTE!] +> [NOTE] > You can have multiple decoders using LUA code. Each decoder uses its own environment and there is no sharing of anything between different decoders. ## Configuration -- inline LUA code From 9a33bb6bcba97433e740c5f67be2231742adee3d Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Wed, 26 Nov 2025 15:10:35 +0000 Subject: [PATCH 12/16] Updated docs with information about why the default bit ordering is the way that it is. --- docs/LUA.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/LUA.md b/docs/LUA.md index 763226e5c..521e4cf1b 100644 --- a/docs/LUA.md +++ b/docs/LUA.md @@ -83,7 +83,7 @@ exactly the same as `{4, 8}` except that the `little_endian_value` setting is in There are two ways to think about numbering bits in a sequence of bytes: -* big-endian-buffer: the first bit (#0) is the *most* significant bit in the first byte. Bit #8 is the *most* significant bit in the second byte. +* big-endian-buffer: the first bit (#0) is the *most* significant bit in the first byte (0x80). Bit #8 is the *most* significant bit in the second byte. * little-endian-buffer: the first bit (#0) is the *least* significant bit in the first byte. Bit #8 is the *least* significant bit in the second byte. This has implications when cross byte boundaries. The field extraction operator always returns a consecutive sequence of bits according to the endianness above. @@ -93,6 +93,8 @@ Once the sequence of bits has been extracted, there are two ways to turn it into * big-endian-value: the first extracted bit is the *most* significant bit in the returned integer. * little-endian-value: the first extracted bit is the *least* significant bit in the returned integer. +The default is that the big-endian-buffer and big-endian-value are set. Note that `rtl_433` maps decoded symbols from RF to bytes by filling the 0x80 bit of the first byte with the first decoded symbol. + ## `validate` details From f0fec5b0c56b1d33cb9b9b7f8fbcb93b78642f47 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Wed, 26 Nov 2025 19:45:33 +0000 Subject: [PATCH 13/16] Added -DENABLE_LUA=ON to the recommended build line. The option behaves like the other ENABLE_* options. --- CMakeLists.txt | 13 +++++++++++-- docs/BUILDING.md | 2 +- docs/LUA.md | 3 ++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f39c63f4e..6510578a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,8 +191,11 @@ endif() ######################################################################## # 🚀 Find Lua 5.4 # The FindLua module will set variables like Lua_FOUND, LUA_INCLUDE_DIR, and LUA_LIBRARIES. -find_package(Lua 5.4 QUIET) +set(ENABLE_LUA AUTO CACHE STRING "Enable Flex LUA support") +set_property(CACHE ENABLE_LUA PROPERTY STRINGS AUTO ON OFF) +if(ENABLE_LUA) # AUTO / ON +find_package(Lua 5.4 QUIET) # Check if the library was found if(Lua_FOUND) message(STATUS "Found Lua ${Lua_VERSION}: ${LUA_LIBRARIES}") @@ -206,8 +209,14 @@ if(Lua_FOUND) # Add the necessary include directories include_directories(${LUA_INCLUDE_DIR}) +elseif(ENABLE_LUA STREQUAL "AUTO") + message(STATUS "LUA development files not found, Flex LUA won't be possible.") +else() + message(FATAL_ERROR "LUA development files not found.") +endif() + else() - message(STATUS "Lua 5.4 not found. Build will proceed without Lua support.") + message(STATUS "Flex LUA disabled.") endif() ######################################################################## diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 68c743e51..f6a98751d 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -111,7 +111,7 @@ Then install only from packages (version 0.7) or only from source (version 0.8). To properly configure builds without relying on automatic feature detection you should set all options explicitly, e.g. - cmake -DENABLE_RTLSDR=ON -DENABLE_SOAPYSDR=ON -DENABLE_OPENSSL=ON -DBUILD_DOCUMENTATION=OFF -DCMAKE_BUILD_TYPE=Release -GNinja -B build + cmake -DENABLE_LUA=ON -DENABLE_RTLSDR=ON -DENABLE_SOAPYSDR=ON -DENABLE_OPENSSL=ON -DBUILD_DOCUMENTATION=OFF -DCMAKE_BUILD_TYPE=Release -GNinja -B build cmake --build build -j 10 DESTDIR=/tmp/destdir cmake --build build --target install diff --git a/docs/LUA.md b/docs/LUA.md index 521e4cf1b..1900762d2 100644 --- a/docs/LUA.md +++ b/docs/LUA.md @@ -75,7 +75,8 @@ just provide the offset. This will be interpreted according to the current setti If the width is negative, then the bit extraction proceeds in the opposite direction. E.g. in the case above, `{11, -8}` means to start at bit 11 and work backwards to bit 4 (inclusive). This gives you the bit reversed value to `{4, 8}`. Effectively, `{11, -8}` is -exactly the same as `{4, 8}` except that the `little_endian_value` setting is inverted. +exactly the same as `{4, 8}` except that the `little_endian_value` setting is inverted. Note that the first number is the starting bit number and +reversing {o, w} maps to {o + w - 1, -w}. From c6151af9a72a14935bf67037b539805d9a25de71 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Tue, 2 Dec 2025 21:20:08 -0500 Subject: [PATCH 14/16] Now can run the tests if LUA is on the system, but configured off. --- tests/CMakeLists.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e107f25d3..bdcf0eadb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,9 +33,16 @@ endforeach(testSrc) ######################################################################## add_test(rtl_433_help ../src/rtl_433 -h) +if(ENABLE_LUA) # AUTO / ON + find_package(Lua 5.4 QUIET) -if (Lua_FOUND) +# Check if the library was found +if(Lua_FOUND) add_test(rtl_433_lua ../src/rtl_433 -r /dev/null -c ../../tests/lua_test.conf) +elseif(ENABLE_LUA STREQUAL "AUTO") +else() + message(FATAL_ERROR "LUA development files not found.") +endif() endif() ######################################################################## From 1305b7697b34c2234eb0e64a629ee085234a273a Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Tue, 2 Dec 2025 21:33:27 -0500 Subject: [PATCH 15/16] Add more building instructions. Also relax to lua 5.3 as it should be good enough. --- CMakeLists.txt | 4 ++-- docs/BUILDING.md | 20 ++++++++++++++++++++ tests/CMakeLists.txt | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6510578a5..2a39806d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,13 +189,13 @@ endif() ######################################################################## # Find LUA dependencies ######################################################################## -# 🚀 Find Lua 5.4 +# 🚀 Find Lua 5.3/5.4 # The FindLua module will set variables like Lua_FOUND, LUA_INCLUDE_DIR, and LUA_LIBRARIES. set(ENABLE_LUA AUTO CACHE STRING "Enable Flex LUA support") set_property(CACHE ENABLE_LUA PROPERTY STRINGS AUTO ON OFF) if(ENABLE_LUA) # AUTO / ON -find_package(Lua 5.4 QUIET) +find_package(Lua 5.3 QUIET) # Check if the library was found if(Lua_FOUND) message(STATUS "Found Lua ${Lua_VERSION}: ${LUA_LIBRARIES}") diff --git a/docs/BUILDING.md b/docs/BUILDING.md index f6a98751d..d8df3d5ec 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -107,6 +107,26 @@ Purge all SoapySDR packages and source installation from /usr/local. Then install only from packages (version 0.7) or only from source (version 0.8). ::: +## LUA Support + +If you want LUA support, then you may need to install the LUA 5.4 development libraries for your platform. + +### Debian + +`sudo apt install liblua5.4-dev` + +### MacOS + +`brew install lua` + +or + +`sudo port install lua5.4` + +### Windows + +There are problems with installing a recent version of lua as some of the package managers don't have a current version. + ## Package maintainers To properly configure builds without relying on automatic feature detection you should set all options explicitly, e.g. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bdcf0eadb..ad601b079 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -35,7 +35,7 @@ add_test(rtl_433_help ../src/rtl_433 -h) if(ENABLE_LUA) # AUTO / ON -find_package(Lua 5.4 QUIET) +find_package(Lua 5.3 QUIET) # Check if the library was found if(Lua_FOUND) add_test(rtl_433_lua ../src/rtl_433 -r /dev/null -c ../../tests/lua_test.conf) From 62805b943d44d5505406dd594175a2dc46d98869 Mon Sep 17 00:00:00 2001 From: Philip Gladstone Date: Wed, 3 Dec 2025 19:54:12 -0500 Subject: [PATCH 16/16] Updated the builod notes and removed emoji --- CMakeLists.txt | 2 +- docs/BUILDING.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a39806d8..5e51bbe71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,7 +189,7 @@ endif() ######################################################################## # Find LUA dependencies ######################################################################## -# 🚀 Find Lua 5.3/5.4 +# Find Lua 5.3/5.4 # The FindLua module will set variables like Lua_FOUND, LUA_INCLUDE_DIR, and LUA_LIBRARIES. set(ENABLE_LUA AUTO CACHE STRING "Enable Flex LUA support") set_property(CACHE ENABLE_LUA PROPERTY STRINGS AUTO ON OFF) diff --git a/docs/BUILDING.md b/docs/BUILDING.md index d8df3d5ec..f6baefc1b 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -109,7 +109,7 @@ Then install only from packages (version 0.7) or only from source (version 0.8). ## LUA Support -If you want LUA support, then you may need to install the LUA 5.4 development libraries for your platform. +If you want LUA support, then you may need to install the LUA 5.4 or the LUA 5.3 development libraries for your platform. ### Debian @@ -125,7 +125,7 @@ or ### Windows -There are problems with installing a recent version of lua as some of the package managers don't have a current version. +Untested at the moment. ## Package maintainers @@ -135,6 +135,8 @@ To properly configure builds without relying on automatic feature detection you cmake --build build -j 10 DESTDIR=/tmp/destdir cmake --build build --target install +If you don't want to include the LUA feature, then set `-DENABLE_LUA=OFF` to ensure that the LUA feature is not built even if the libraries are present on the build system. + ## Windows ### Visual Studio 2017