diff --git a/CMakeLists.txt b/CMakeLists.txt index 6dc1f7910..5e51bbe71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,6 +186,39 @@ else() message(STATUS "Threads support disabled.") endif() +######################################################################## +# Find LUA dependencies +######################################################################## +# 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.3 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}) +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 "Flex LUA disabled.") +endif() + ######################################################################## # Find OpenSSL build dependencies ######################################################################## diff --git a/README.md b/README.md index 6c07a2ad9..f43b872d8 100644 --- a/README.md +++ b/README.md @@ -454,8 +454,9 @@ 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 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/docs/BUILDING.md b/docs/BUILDING.md index 68c743e51..f6baefc1b 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -107,14 +107,36 @@ 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 or the LUA 5.3 development libraries for your platform. + +### Debian + +`sudo apt install liblua5.4-dev` + +### MacOS + +`brew install lua` + +or + +`sudo port install lua5.4` + +### Windows + +Untested at the moment. + ## Package maintainers 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 +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 diff --git a/docs/LUA.md b/docs/LUA.md new file mode 100644 index 000000000..1900762d2 --- /dev/null +++ b/docs/LUA.md @@ -0,0 +1,239 @@ +# 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 `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 + +This extension is only built if the LUA 5.4 development environment is installed. + +## 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). + +> [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 +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 +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: + +* `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") +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 +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. 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. Note that the first number is the starting bit number and +reversing {o, w} maps to {o + w - 1, -w}. + + + +## 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 (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. + +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. + +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 + +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 + +If there are no rows in the returned table, or the boolean is false, then the validation is treated as +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 + +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:bitlen() >= 16 and value:bitlen() <= 18 then + if value[{0, 8}] ^ value[{8,8}] == 255 then + table.insert(result, value) + end + end + end + return result +end +``` + +## `decode` 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 decode(data) + res = {} + 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 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 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. + +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/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/man/man1/rtl_433.1 b/man/man1/rtl_433.1 index 2a9316885..a5d455efa 100644 --- a/man/man1/rtl_433.1 +++ b/man/man1/rtl_433.1 @@ -321,10 +321,15 @@ 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 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" .SS "Output format option" diff --git a/src/devices/flex.c b/src/devices/flex.c index 9b66d9a4a..ca9273554 100644 --- a/src/devices/flex.c +++ b/src/devices/flex.c @@ -13,6 +13,77 @@ #include "optparse.h" #include "fatal.h" #include +#ifdef HAS_LUA +#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" + "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" + "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) { @@ -100,6 +171,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 +210,318 @@ 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); + lua_getglobal(params->L, "BitBuffer"); + + for (i = 0; i < bitbuffer->num_rows; i++) { + lua_getfield(params->L, -1, "new"); + + lua_pushlstring(params->L, (const char *)bitbuffer->bb[i], (bitbuffer->bits_per_row[i] + 7) / 8); + lua_pushinteger(params->L, bitbuffer->bits_per_row[i]); + + lua_pcall(params->L, 2, 1, 0); + + lua_rawseti(params->L, -3, i + 1); + } + lua_pop(params->L, 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, "_bitlen"); + 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, "_value"); + 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_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, "decode"); + if (lua_isfunction(params->L, -1)) { + // We have an decode 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, "_bitlen"); + 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, "_value"); + 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); +} + + +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"); + } + + // Width defaults to 1 + int width = flex_lua_get_value(L, 2, "width", 1); + + 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 (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, "Bitfield outside 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 + /** Generic flex decoder. */ @@ -271,6 +657,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 +681,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_decode(params, bitbuffer, data); +#endif + decoder_output_data(decoder, data); return 1; } @@ -342,6 +737,10 @@ static int flex_callback(r_device *decoder, bitbuffer_t *bitbuffer) NULL); /* clang-format on */ +#ifdef HAS_LUA + flex_lua_decode(params, bitbuffer, data); +#endif + decoder_output_data(decoder, data); for (i = 0; i < bitbuffer->num_rows; i++) { free(row_codes[i]); @@ -424,8 +823,16 @@ 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 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"); exit(0); } @@ -654,7 +1061,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); @@ -748,7 +1155,26 @@ 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, "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(); + } + } + 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(); + } +#endif } else { fprintf(stderr, "Bad flex spec, unknown keyword (%s)!\n", key); 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, ','); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 95fe7a895..ad601b079 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,6 +33,18 @@ endforeach(testSrc) ######################################################################## add_test(rtl_433_help ../src/rtl_433 -h) +if(ENABLE_LUA) # AUTO / ON + +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) +elseif(ENABLE_LUA STREQUAL "AUTO") +else() + message(FATAL_ERROR "LUA development files not found.") +endif() +endif() + ######################################################################## # Define style checks ######################################################################## diff --git a/tests/lua_test.conf b/tests/lua_test.conf new file mode 100644 index 000000000..1faea5f58 --- /dev/null +++ b/tests/lua_test.conf @@ -0,0 +1,81 @@ +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") + + 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") + 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") + 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) + 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) + +]] +}