From 7b5b44ed4a3d8ffbdaae2f0e2eefef202f7c5f70 Mon Sep 17 00:00:00 2001 From: h2zero Date: Fri, 27 Aug 2021 10:53:11 -0600 Subject: [PATCH] [Initial work] Add decoder lib / tests, create docs. --- .clang-format | 14 ++ .github/workflows/build.yml | 160 ++++++++++++++++++++ .gitignore | 3 + .gitmodules | 3 + CMakeLists.txt | 26 ++++ README.md | 118 ++++++++++++++- src/arduino_json | 1 + src/decoder.cpp | 228 ++++++++++++++++++++++++++++ src/decoder.h | 6 + src/device_json.h | 250 +++++++++++++++++++++++++++++++ tests/BLE/CMakeLists.txt | 16 ++ tests/BLE/test_ble.cpp | 102 +++++++++++++ tests/BLE_fail/CMakeLists.txt | 16 ++ tests/BLE_fail/test_ble_fail.cpp | 111 ++++++++++++++ tests/CMakeLists.txt | 4 + tests/CompileOptions.cmake | 90 +++++++++++ 16 files changed, 1147 insertions(+), 1 deletion(-) create mode 100644 .clang-format create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 160000 src/arduino_json create mode 100644 src/decoder.cpp create mode 100644 src/decoder.h create mode 100644 src/device_json.h create mode 100644 tests/BLE/CMakeLists.txt create mode 100644 tests/BLE/test_ble.cpp create mode 100644 tests/BLE_fail/CMakeLists.txt create mode 100644 tests/BLE_fail/test_ble_fail.cpp create mode 100644 tests/CMakeLists.txt create mode 100644 tests/CompileOptions.cmake diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..55199aa2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,14 @@ +BasedOnStyle: Google +Language: Cpp +ColumnLimit: 0 +IndentWidth: 2 +TabWidth: 2 +UseTab: Never +IndentPPDirectives: AfterHash +ReflowComments: false +SpacesBeforeTrailingComments: 1 +AlignConsecutiveMacros: true +AlignTrailingComments: false +AccessModifierOffset: -2 +DerivePointerAlignment: false +PointerAlignment: Left diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..8ba6d21a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,160 @@ +name: Build + +on: [push, pull_request] + +jobs: + gcc: + name: GCC + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - gcc: "4.6" + - gcc: "4.7" + - gcc: "4.8" + - gcc: "4.9" + - gcc: "5" + - gcc: "6" + - gcc: "7" + cxxflags: -fsanitize=leak -fno-sanitize-recover=all + - gcc: "8" + cxxflags: -fsanitize=undefined -fno-sanitize-recover=all + - gcc: "9" + cxxflags: -fsanitize=address -fno-sanitize-recover=all + - gcc: "10" + - gcc: "11" + steps: + - name: Install + run: | + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ trusty main' + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ trusty universe' + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ xenial main' + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ xenial universe' + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ bionic main' + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ bionic universe' + sudo add-apt-repository -yn 'deb http://mirrors.kernel.org/ubuntu hirsute main universe' + sudo apt-get update + sudo apt-get install -y gcc-${{ matrix.gcc }} g++-${{ matrix.gcc }} + - name: Checkout + uses: actions/checkout@v2 + - name: Checkout submodules + run: git submodule update --init --recursive + - name: Configure + run: cmake -DCMAKE_BUILD_TYPE=Debug . + env: + CC: gcc-${{ matrix.gcc }} + CXX: g++-${{ matrix.gcc }} + CXXFLAGS: ${{ matrix.cxxflags }} + - name: Build + run: cmake --build . + - name: Test + run: ctest --output-on-failure -V -C Debug . + env: + UBSAN_OPTIONS: print_stacktrace=1 + + clang: + name: Clang + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - clang: "3.5" + cxxflags: "-stdlib=libc++" + - clang: "3.6" + cxxflags: "-stdlib=libc++" + - clang: "3.7" + cxxflags: "-stdlib=libc++" + - clang: "3.8" + cxxflags: "-stdlib=libc++" + - clang: "3.9" + cxxflags: "-stdlib=libc++" + - clang: "4.0" + cxxflags: "-stdlib=libc++" + - clang: "5.0" + - clang: "6.0" + - clang: "7" + - clang: "8" + cxxflags: -fsanitize=leak -fno-sanitize-recover=all + - clang: "9" + cxxflags: -fsanitize=undefined -fno-sanitize-recover=all + - clang: "10" + cxxflags: -fsanitize=address -fno-sanitize-recover=all + steps: + - name: Install + run: | + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ trusty main' + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ trusty universe' + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ xenial main' + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ xenial universe' + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ bionic main' + sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ bionic universe' + sudo apt-get update + sudo apt-get install -y clang-${{ matrix.clang }} + - name: Checkout + uses: actions/checkout@v2 + - name: Checkout submodules + run: git submodule update --init --recursive + - name: Configure + run: cmake -DCMAKE_BUILD_TYPE=Debug . + env: + CC: clang-${{ matrix.clang }} + CXX: clang++-${{ matrix.clang }} + CXXFLAGS: >- + ${{ matrix.cxxflags }} + ${{ contains(matrix.cxxflags, 'libc++') && '-I/usr/lib/llvm-10/include/c++/v1/' || '' }} + - name: Build + run: cmake --build . + - name: Test + run: ctest --output-on-failure -V -C Debug . + env: + UBSAN_OPTIONS: print_stacktrace=1 + + xcode: + name: XCode + needs: clang + runs-on: macos-10.15 + strategy: + fail-fast: false + matrix: + include: + - xcode: "10.3" + - xcode: "11.7" + - xcode: "12.4" + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Checkout submodules + run: git submodule update --init --recursive + - name: Select XCode version + run: sudo xcode-select --switch /Applications/Xcode_${{ matrix.xcode }}.app + - name: Configure + run: cmake -DCMAKE_BUILD_TYPE=Debug . + - name: Build + run: cmake --build . + - name: Test + run: ctest --output-on-failure -V -C Debug . + + msvc: + name: Visual Studio + strategy: + fail-fast: false + matrix: + include: + - os: windows-2016 + - os: windows-2019 + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Checkout submodules + run: git submodule update --init --recursive + - name: Configure + run: cmake -DCMAKE_BUILD_TYPE=Debug . + - name: Build + run: cmake --build . + - name: Test + run: ctest --output-on-failure -V -C Debug . + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3aaf9ee1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/build +.vscode + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..df9e7e28 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/arduino_json"] + path = src/arduino_json + url = https://github.com/bblanchon/ArduinoJson.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..d7ce0bba --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.3) + +project(1decoder VERSION 0.1.0) + +add_library(1decoder + src/decoder.cpp + ) + +target_include_directories(1decoder + PUBLIC + $ + $ + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +target_compile_features(1decoder PRIVATE cxx_std_11) + +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + include(CTest) +endif() + +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTING) + include(tests/CompileOptions.cmake) + add_subdirectory(tests) +endif() + diff --git a/README.md b/README.md index ef5ece21..8af59381 100644 --- a/README.md +++ b/README.md @@ -1 +1,117 @@ -# 1decoder \ No newline at end of file +# 1decoder + +# Using + +Call `decodeBLEJson(JsonObject)` with the input being of the Arduino JSON JsonObject type. If the device is known the JsonObject will have the decoded device data added to it. + +### Example +Input JsonObject: +``` +{ + "servicedata": "712098000163b6658d7cc40d0410024001" +} +``` + +JsonObject after decoding: +``` +{ + "servicedata": "712098000163b6658d7cc40d0410024001" + "brand":"Xiaomi", + "model":"miflora", + "model_id":"HHCCJCY01HHCC", + "tempc":32, + "tempf":89.6 +} +``` + +# Adding device decoding + +Device decode specifications are located in the [device_json.h](src/device_json.h) file. The format is: +``` +R""""( +{ + "brand":"Xiaomi", + "model":"miflora", + "model_id":"HHCCJCY01HHCC", + "condition":["servicedata", "contain", "209800"], + "properties":{ + "tempc":{ + "condition":["servicedata", 25, "4"], + "decoder":["value_from_hex_data", "servicedata", 30, 4, true], + "post_proc":['/', 10] + }, + "moi":{ + "condition":["servicedata", 25, "8"], + "decoder":["value_from_hex_data", "servicedata", 30, 2, false] + }, + "lux":{ + "condition":["servicedata", 25, "7"], + "decoder":["value_from_hex_data", "servicedata", 30, 6, true] + }, + "fer":{ + "condition":["servicedata", 25, "9"], + "decoder":["value_from_hex_data", "servicedata", 30, 4, true] + } + } +})"""", +``` + +Each device must provide a `brand`, `model`, `model_id`, `condition`, and `properties`. +- `brand` = brand name of the device. +- `model` = model name of the device. +- `model_id` = model id number of the device. + +### Condition +`condition` is a JSON array, which must contain as the first parameter, the data source to test for the condtion. Valid inputs are: +- "servicedata" +- "manufacturerdata" +- "name" +- "uuid" + +The second parameter is how the data should be tested. Valid inputs are: +- "contain" tests if the specified value (see below) exists the data source +- "index" tests if the specified value exists at the index location (see below) in the data source + +The third parameter can be either the index value or the data value to find. If the second parameter is `contain`, the third parameter should be the value to look for in the data source. If the second parameter is `index`, the third parameter should be the location in the data source to look for the value provided as the fourth parameter. + +`condition` can have multiple conditions chanined together using '|' and '&' between them. +For example: `"condition":["servicedata", "index", 0, "0804", '|', "servicedata", "index", 0, "8804"]` +This will match if the service data at index 0 is "0804" `OR` "8804". + +### Properties +Properties is a nested JSON object containing one or more JSON objects. In the example above it looks like: +``` + "properties":{ + "tempc":{ + "condition":["servicedata", 25, "4"], + "decoder":["value_from_hex_data", "servicedata", 30, 4, true], + "post_proc":['/', 10] + }, +``` + +Here we have a single property that defines a value that we want to decode. The key "tempc" will be used as the key in the JsonObject provided when `decodeBLEJson(JsonObject)` is called. "tempc" in this example is another JSON object that has an (optional, explained below) `condition`, `decoder`, and `post_proc`. + +`condition` is a JSON array. The first parameter defines the data source of the condition to test and must be one of: +- "servicedata" +- "manufacturerdata" + +The second parameter is the index of the data source to look for the value. The third parameter is the value to test for. +If the condition is met the data will be decoded and added to the JsonObject. + +`decoder` is a JSON array that specifies the decoder function and parameters to decode the value. The first parameter is the name of the function to call, currently only "value_from_hex_data" is valid. The other parameters are: +- "servicedata", Extract the value from the service data. Could also be "manufacturerdata" +- 30, The index of the data source where the value exists. +- 4, The length of the data in bytes (characters in the string). +- true/false, If the value in the data source should have it's endianness reversed before converting. +- (optional)true/false, Sets if the resulting value can be a negative number. + +`post_proc` This specifies any post processing of the resulting decoded value. This is a JSON array that should be written in the order that the operation order is desired. In the simple example the first parameter is the '/' divide operation and the second parameter (10) is the value to divide the result by. Multiple operations can be chained together in this array to perform more complex calculations. Valid operations are: +- '/' divide +- '*' multiply +- '+' add +- '-' subtract +- '<' shift left +- '>' shift right +- '!' Not (invert), useful for bool types + +`val_bits` (Not shown in the example) is an additional parameter that can be added to define the value. It will convert the post processed result into a value with the number of bits specified by `val_bits`. Valid values are: 1 (for bool), 8, 16, 32. Double is the default type if this is omitted. diff --git a/src/arduino_json b/src/arduino_json new file mode 160000 index 00000000..b8108331 --- /dev/null +++ b/src/arduino_json @@ -0,0 +1 @@ +Subproject commit b810833145e3cdab738fb4a45d7d4010a0e13f6d diff --git a/src/decoder.cpp b/src/decoder.cpp new file mode 100644 index 00000000..5c3f52f5 --- /dev/null +++ b/src/decoder.cpp @@ -0,0 +1,228 @@ +#include "decoder.h" + +#include + +#include "device_json.h" + +#ifdef DEBUG_DECODER +# include +# define DEBUG_PRINT printf +#else +# define DEBUG_PRINT +#endif + +/* + * @breif revert the string data 2 by 2 to get the correct endianness + */ +void reverse_hex_data(const char* in, char* out, int l) { + int i = l, j = 0; + while (i) { + out[j] = in[i - 2]; + out[j + 1] = in[i - 1]; + i -= 2; + j += 2; + } + out[l] = '\0'; +} + +/* + * @breif Extracts the data value from the data string + */ +long value_from_hex_string(const char* data_str, int offset, int data_length, bool reverse, bool canBeNegative = true) { + DEBUG_PRINT("offset: %d, len %d, rev %u, neg, %u\n", offset, data_length, reverse, canBeNegative); + std::string data(&data_str[offset], data_length); + + if (reverse) { + reverse_hex_data(&data_str[offset], &data[0], data_length); + } + + DEBUG_PRINT("extracting value from %s\n", data.c_str()); + long value = strtol(data.c_str(), NULL, 16); + + if (value > 65000 && data_length <= 4 && canBeNegative) + value = value - 65535; + return value; +} + +/* + * @breif Compares the input json values to the known devices and decodes the data if a match is found. + */ +bool decodeBLEJson(JsonObject& jsondata) { + DynamicJsonDocument doc(2048); + const char* svc_data = jsondata["servicedata"].as(); + const char* mfg_data = jsondata["manufacturerdata"].as(); + const char* dev_name = jsondata["name"].as(); + const char* svc_uuid = jsondata["servicedatauuid"].as(); + bool success = false; + + /* loop through the devices and attempt to match the input data to a device parameter set */ + for (auto i = 0; i < sizeof(_devices) / sizeof(_devices[0]); ++i) { + DeserializationError error = deserializeJson(doc, _devices[i]); + if (error) { + DEBUG_PRINT("deserializeJson() failed: %s\n", error.c_str()); + return success; + } + + JsonArray condition = doc["condition"]; + bool match = false; + for (unsigned int i = 0; i < condition.size();) { + const char* data_str; + if (strstr(condition[i].as(), "servicedata") != nullptr && svc_data != nullptr) { + data_str = svc_data; + } else if (strstr(condition[i].as(), "manufacturerdata") != nullptr && mfg_data != nullptr) { + data_str = mfg_data; + } else if (strstr(condition[i].as(), "name") != nullptr && dev_name != nullptr) { + data_str = dev_name; + } else if (strstr(condition[i].as(), "uuid") != nullptr && svc_uuid != nullptr) { + data_str = svc_uuid; + } else { + break; + } + + if (strstr(condition[i + 1].as(), "contain") != nullptr) { + if (strstr(data_str, condition[i + 2].as()) != nullptr) { + match = true; + } + i += 3; + } else if (strstr(condition[i + 1].as(), "index") != nullptr) { + DEBUG_PRINT("comparing index: %s to %s at index %u\n", &data_str[condition[i + 2].as()], + condition[i + 3].as(), condition[i + 2].as()); + if (strncmp(&data_str[condition[i + 2].as()], condition[i + 3].as(), + strlen(condition[i + 3].as())) == 0) { + match = true; + } + i += 4; + } + + if (i < condition.size()) { + if (!match && *condition[i].as() == '|') { + i++; + continue; + } else if (match && *condition[i].as() == '&') { + i++; + match = false; + continue; + } + } + break; + } + + /* found a match, extract the data */ + if (match) { + jsondata["brand"] = doc["brand"]; + jsondata["model"] = doc["model"]; + jsondata["model_id"] = doc["model_id"]; + + JsonObject properties = doc["properties"]; + + /* Loop through all the devices properties and extract the values */ + for (JsonPair kv : properties) { + JsonObject prop = kv.value().as(); + JsonArray prop_condition = prop["condition"]; + + if (prop_condition.isNull() || strstr((const char*)prop_condition[0], "servicedata") != nullptr || + strstr((const char*)prop_condition[0], "manufacturerdata") != nullptr) { + if (prop_condition.isNull() || svc_data[prop_condition[1].as()] == *prop_condition[2].as()) { + JsonArray decoder = prop["decoder"]; + if (strstr((const char*)decoder[0], "value_from_hex_data") != nullptr) { + const char* src = svc_data; + if (strstr((const char*)decoder[1], "manufacturerdata")) { + src = mfg_data; + } + + /* use a double for all values and cast later if required */ + double temp_val; + if (decoder.size() == 5) { + temp_val = (double)value_from_hex_string(src, decoder[2].as(), decoder[3].as(), + decoder[4].as()); + } else if (decoder.size() == 6) { + temp_val = (double)value_from_hex_string(src, decoder[2].as(), decoder[3].as(), + decoder[4].as(), decoder[6].as()); + } + + /* Do any required post processing of the value */ + if (prop.containsKey("post_proc")) { + JsonArray post_proc = prop["post_proc"]; + for (unsigned int i = 0; i < post_proc.size(); i += 2) { + switch (*post_proc[i].as()) { + case '/': + temp_val /= post_proc[i + 1].as(); + break; + case '*': + temp_val *= post_proc[i + 1].as(); + break; + case '-': + temp_val -= post_proc[i + 1].as(); + break; + case '+': + temp_val += post_proc[i + 1].as(); + break; + case '<': { + long val = (long)temp_val; + temp_val = val << post_proc[i + 1].as(); + break; + } + case '>': { + long val = (long)temp_val; + temp_val = val >> post_proc[i + 1].as(); + break; + } + case '!': { + bool val = (bool)temp_val; + temp_val = !val; + break; + } + } + } + } + + /* If there is any underscores at the beginning of the property name, there is multiple + * properties of this type, we need remove the underscores for creating the key. + */ + unsigned int key_index = 0; + while (kv.key().c_str()[key_index] == '_') { + key_index++; + } + + std::string _key(&kv.key().c_str()[key_index]); + + /* Cast to a differnt value type if specified */ + if (prop.containsKey("val_bits")) { + switch (prop["val_bits"].as()) { + case 1: + jsondata[_key] = (bool)temp_val; + break; + case 8: + jsondata[_key] = (int8_t)temp_val; + break; + case 16: + jsondata[_key] = (int16_t)temp_val; + break; + case 32: + jsondata[_key] = (int32_t)temp_val; + break; + default: + jsondata[_key] = temp_val; + break; + } + } else { + jsondata[_key] = temp_val; + } + + /* If the property is temp in C, make sure to convert and add temp in F */ + if (_key == "tempc") { + double tc = jsondata[_key]; + jsondata["tempf"] = tc * 1.8 + 32; + } + + success = true; + DEBUG_PRINT("found value = %s : %.2f\n", _key.c_str(), jsondata[_key].as()); + } + } + } + } + return success; + } + } + return success; +} diff --git a/src/decoder.h b/src/decoder.h new file mode 100644 index 00000000..edeae08f --- /dev/null +++ b/src/decoder.h @@ -0,0 +1,6 @@ +#ifndef _DECODER_H_ +#define _DECODER_H_ +#include "ArduinoJson.h" +//#define DEBUG_DECODER +extern "C" bool decodeBLEJson(JsonObject& jsondata); // extern "C" needed for platformio linker? +#endif diff --git a/src/device_json.h b/src/device_json.h new file mode 100644 index 00000000..396f0e8c --- /dev/null +++ b/src/device_json.h @@ -0,0 +1,250 @@ +const char* _devices[] = { +R""""( +{ + "brand":"Xiaomi", + "model":"miflora", + "model_id":"HHCCJCY01HHCC", + "condition":["servicedata", "contain", "209800"], + "properties":{ + "tempc":{ + "condition":["servicedata", 25, "4"], + "decoder":["value_from_hex_data", "servicedata", 30, 4, true], + "post_proc":['/', 10] + }, + "moi":{ + "condition":["servicedata", 25, "8"], + "decoder":["value_from_hex_data", "servicedata", 30, 2, false] + }, + "lux":{ + "condition":["servicedata", 25, "7"], + "decoder":["value_from_hex_data", "servicedata", 30, 6, true] + }, + "fer":{ + "condition":["servicedata", 25, "9"], + "decoder":["value_from_hex_data", "servicedata", 30, 4, true] + } + } +})"""", +R""""( +{ + "brand":"xiaomi", + "model":"Cleargrass clock", + "model_id":"LYWSD02", + "condition":["servicedata", "contain", "205b04"], + "properties":{ + "tempc":{ + "condition":["servicedata", 25, "4"], + "decoder":["value_from_hex_data", "servicedata", 30, 4, true], + "post_proc":['/', 10] + }, + "hum":{ + "condition":["servicedata", 25, "6"], + "decoder":["value_from_hex_data", "servicedata", 30, 4, true, false], + "post_proc":['/', 10] + } + } +})"""", +R""""( +{ + "brand":"xiaomi", + "model":"Mi Jia round", + "model_id":"LYWSDCGQ", + "condition":["servicedata", "contain", "20aa01"], + "properties":{ + "tempc":{ + "condition":["servicedata", 23, "d"], + "decoder":["value_from_hex_data", "servicedata", 28, 4, true], + "post_proc":['/', 10] + }, + "hum":{ + "condition":["servicedata", 23, "d"], + "decoder":["value_from_hex_data", "servicedata", 32, 4, true, false], + "post_proc":['/', 10] + }, + "_hum":{ + "condition":["servicedata", 23, "6"], + "decoder":["value_from_hex_data", "servicedata", 28, 4, true, false], + "post_proc":['/', 10] + } + } +})"""", +R""""( +{ + "brand":"xiaomi", + "model":"CG_THP", + "model_id":"CGP1W", + "condition":["servicedata", "index", 0, "0809"], + "properties":{ + "tempc":{ + "decoder":["value_from_hex_data", "servicedata", 20, 4, true], + "post_proc":['/', 10] + }, + "hum":{ + "decoder":["value_from_hex_data", "servicedata", 24, 4, true, false], + "post_proc":['/', 10] + }, + "pres":{ + "decoder":["value_from_hex_data", "servicedata", 32, 4, true, false], + "post_proc":['/', 100] + } + } +})"""", +R""""( +{ + "brand":"xiaomi", + "model":"CG_round_v1", + "model_id":"CGG1", + "condition":["servicedata", "index", 0, "0807", '|', "servicedata", "index", 0, "8816"], + "properties":{ + "tempc":{ + "decoder":["value_from_hex_data", "servicedata", 20, 4, true], + "post_proc":['/', 10] + }, + "hum":{ + "decoder":["value_from_hex_data", "servicedata", 24, 4, true, false], + "post_proc":['/', 10] + } + } +})"""", +R""""( +{ + "brand":"xiaomi", + "model":"CG_round_v2", + "model_id":"CGG1", + "condition":["servicedata", "index", 4, "4703"], + "properties":{ + "tempc":{ + "condition":["servicedata", 23, "d"], + "decoder":["value_from_hex_data", "servicedata", 28, 4, true], + "post_proc":['/', 10] + }, + "hum":{ + "condition":["servicedata", 23, "d"], + "decoder":["value_from_hex_data", "servicedata", 32, 4, true, false], + "post_proc":['/', 10] + }, + "_tempc":{ + "condition":["servicedata", 23, "4"], + "decoder":["value_from_hex_data", "servicedata", 28, 4, true], + "post_proc":['/', 10] + } + } +})"""", +R""""( +{ + "brand":"xiaomi", + "model":"CG_alarm_clock", + "model_id":"CGD1", + "condition":["servicedata", "index", 0, "080caf", '|', "servicedata", "index", 0, "080c09", '|', "servicedata", "index", 0, "080cd0"], + "properties":{ + "tempc":{ + "decoder":["value_from_hex_data", "servicedata", 20, 4, true], + "post_proc":['/', 10] + }, + "hum":{ + "decoder":["value_from_hex_data", "servicedata", 24, 4, true, false], + "post_proc":['/', 10] + } + } +})"""", +R""""( +{ + "brand":"Qingping", + "model":"TH lite", + "model_id":"CGDK2", + "condition":["servicedata", "index", 0, "8810"], + "properties":{ + "tempc":{ + "decoder":["value_from_hex_data", "servicedata", 20, 4, true], + "post_proc":['/', 10] + }, + "hum":{ + "decoder":["value_from_hex_data", "servicedata", 24, 4, true, false], + "post_proc":['/', 10] + } + } +})"""", +R""""( +{ + "brand":"Qingping", + "model":"Door sensor", + "model_id":"CGH1", + "condition":["servicedata", "index", 0, "c804", '|', "servicedata", "index", 0, "4804"], + "properties":{ + "open":{ + "decoder":["value_from_hex_data", "servicedata", 21, 1, false], + "val_bits":1, + "post_proc":['!'] + } + } +})"""", +R""""( +{ + "brand":"Qingping", + "model":"Door sensor", + "model_id":"CGH1", + "condition":["servicedata", "index", 0, "0804", '|', "servicedata", "index", 0, "8804"], + "properties":{ + "open":{ + "decoder":["value_from_hex_data", "servicedata", 33, 1, false], + "val_bits":1, + "post_proc":['!'] + } + } +})"""", +R""""( +{ + "brand":"xiaomi", + "model":"Formaldehyde detector", + "model_id":"JQJCY01YM", + "condition":["servicedata", "contain", "20df02"], + "properties":{ + "for":{ + "condition":["servicedata", 23, "0"], + "decoder":["value_from_hex_data", "servicedata", 28, 4, true], + "post_proc":['/', 100] + }, + "hum":{ + "condition":["servicedata", 23, "6"], + "decoder":["value_from_hex_data", "servicedata", 28, 4, true, false], + "post_proc":['/', 10] + }, + "batt":{ + "condition":["servicedata", 23, "a"], + "decoder":["value_from_hex_data", "servicedata", 28, 2, false, false] + } + } +})"""", +R""""( +{ + "brand":"inkbird", + "model":"TH Sensor", + "model_id":"IBS-TH1", + "condition":["name", "index", 0, "sps"], + "properties":{ + "tempc":{ + "decoder":["value_from_hex_data", "manufacturerdata", 0, 4, true], + "post_proc":['/', 100] + }, + "hum":{ + "decoder":["value_from_hex_data", "manufacturerdata", 4, 4, true, false], + "post_proc":['/', 100] + }, + "batt":{ + "decoder":["value_from_hex_data", "manufacturerdata", 14, 2, false, false] + } + } +})"""", +R""""( +{ + "brand":"xiaomi", + "model":"Miband", + "model_id":"MiBand", + "condition":["uuid", "contain", "fee0"], + "properties":{ + "steps":{ + "decoder":["value_from_hex_data", "servicedata", 0, 4, true, false] + } + } +})"""", +}; \ No newline at end of file diff --git a/tests/BLE/CMakeLists.txt b/tests/BLE/CMakeLists.txt new file mode 100644 index 00000000..172cef5b --- /dev/null +++ b/tests/BLE/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.3) + +project(test_ble) + +add_executable(test_ble test_ble.cpp) + +target_compile_features(test_ble PRIVATE cxx_std_11) + +target_link_libraries(test_ble PUBLIC 1decoder) + +target_include_directories(test_ble PUBLIC + "${PROJECT_BINARY_DIR}" + ) + +add_test(NAME run_test_ble COMMAND test_ble) + diff --git a/tests/BLE/test_ble.cpp b/tests/BLE/test_ble.cpp new file mode 100644 index 00000000..d4737c89 --- /dev/null +++ b/tests/BLE/test_ble.cpp @@ -0,0 +1,102 @@ +#include + +#include "decoder.h" + +// Service data test input [test name] [data] +const char* test_servicedata[][2] = { + {"Mi flora", "712098004a63b6658d7cc40d071003f32600"}, + {"Mi flora", "712098005763b6658d7cc40d0810011e"}, + {"Mi flora", "712098000163b6658d7cc40d0410024001"}, + {"Mi flora", "712098000863b6658d7cc40d0910020000"}, + {"Cleargrass clock", "70205b04756ab883c8593f090410020001"}, + {"Cleargrass clock", "70205b04dc6ab883c8593f09061002b202"}, + {"Cleargrass clock", "70205b04756ab883c8593f090410020901"}, + {"Mi jia round sensor", "5020aa0137dfaa33342d580d100404016602"}, + {"Mi jia round sensor", "5020aa018ddfaa33342d580610026602"}, + {"Cleargrass THP sensor", "08094c0140342d5801040801870207024f2702015c"}, + {"Cleargrass THP sensor", "08094c0140342d5801040f01880207024f2702015c"}, + {"Cleargrass THP sensor", "08094c0140342d5801040c01810207024d2702015c"}, + {"Cleargrass round sensor", "5030470341743e10342d580410021201"}, + {"Cleargrass round sensor", "5030470383743e10342d580d100410017e02"}, + {"Cleargrass round sensor", "0807743e10342d5801041201720202010d"}, + {"Cleargrass round sensor", "8816YYYYYYYYYYYY0104eb001b01020164"}, + {"Cleargrass round sensor", "8816xxxxxxxxxxxx0104f4003b01020164"}, + {"Cleargrass alarm clock", "080caffd50342d5801040a017f0202012a"}, + {"Cleargrass alarm clock", "080caffd50342d5801040d019e0202012a"}, + {"Cleargrass alarm clock", "080caffd50342d5801040e01910202012a"}, + {"Qingping TH lite", "8810799111342d580104e8008f0302010b"}, + {"Qingping TH lite", "8810799111342d580104e9001d0202010b"}, + {"Qingping Door Open", "0804751060342d580201600f012b0f0100"}, + {"Qingping Door Close", "0804751060342d580201600f01420f0101"}, + {"Qingping Door Open Action", "4804751060342d580401000f01cb"}, + {"Qingping Door Close Action", "4804751060342d580401010f01d5"}, + {"Formaldehyde detector", "5020df02383a5c014357480a10015e"}, + {"Formaldehyde detector", "5020df02283a5c014357480610025302"}, + {"Formaldehyde detector", "5020df025b3a5c014357481010020800"}}; + +// manufacturer data test input [test name] [device name] [data] +const char* test_mfgdata[][3] = { + {"Inkbird TH1", "sps", "660a03150110805908"}}; + +// uuid test input [test name] [uuid] [data source] [data] +const char* test_uuid[][4] = { + {"MiBand", "fee0", "servicedata", "a21e0000"}}; + +int main() { + StaticJsonDocument<1024> doc; + JsonObject bleObject; + + for (unsigned int i = 0; i < sizeof(test_servicedata) / sizeof(test_servicedata[0]); ++i) { + doc.clear(); + std::cout << "trying " << test_servicedata[i][0] << " : " << test_servicedata[i][1] << std::endl; + doc["servicedata"] = test_servicedata[i][1]; + bleObject = doc.as(); + if (decodeBLEJson(bleObject)) { + std::cout << "Found : "; + bleObject.remove("servicedata"); + serializeJson(doc, std::cout); + std::cout << std::endl; + } else { + std::cout << "FAILED! Error parsing: " << test_servicedata[i][0] << " : " << test_servicedata[i][1] << std::endl; + return 1; + } + } + + for (unsigned int i = 0; i < sizeof(test_mfgdata) / sizeof(test_mfgdata[0]); ++i) { + doc.clear(); + std::cout << "trying " << test_mfgdata[i][0] << " : " << test_mfgdata[i][1] << std::endl; + doc["name"] = test_mfgdata[i][1]; + doc["manufacturerdata"] = test_mfgdata[i][2]; + bleObject = doc.as(); + if (decodeBLEJson(bleObject)) { + std::cout << "Found : "; + bleObject.remove("name"); + bleObject.remove("manufacturerdata"); + serializeJson(doc, std::cout); + std::cout << std::endl; + } else { + std::cout << "FAILED! Error parsing: " << test_mfgdata[i][0] << " : " << test_mfgdata[i][1] << " : " << test_mfgdata[i][2] << std::endl; + return 1; + } + } + + for (unsigned int i = 0; i < sizeof(test_uuid) / sizeof(test_uuid[0]); ++i) { + doc.clear(); + std::cout << "trying " << test_uuid[i][0] << " : " << test_uuid[i][1] << std::endl; + doc[test_uuid[i][2]] = test_uuid[i][3]; + doc["servicedatauuid"] = test_uuid[i][1]; + bleObject = doc.as(); + if (decodeBLEJson(bleObject)) { + std::cout << "Found : "; + bleObject.remove("servicedatauuid"); + bleObject.remove(test_uuid[i][2]); + serializeJson(doc, std::cout); + std::cout << std::endl; + } else { + std::cout << "FAILED! Error parsing: " << test_uuid[i][0] << " : " << test_uuid[i][1] << " : " << test_uuid[i][2] << " : " << test_uuid[i][3] << std::endl; + serializeJson(doc, std::cout); + std::cout << std::endl; + return 1; + } + } +} \ No newline at end of file diff --git a/tests/BLE_fail/CMakeLists.txt b/tests/BLE_fail/CMakeLists.txt new file mode 100644 index 00000000..818b8938 --- /dev/null +++ b/tests/BLE_fail/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.3) + +project(test_ble_fail) + +add_executable(test_ble_fail test_ble_fail.cpp) + +target_compile_features(test_ble_fail PRIVATE cxx_std_11) + +target_link_libraries(test_ble_fail PUBLIC 1decoder) + +target_include_directories(test_ble_fail PUBLIC + "${PROJECT_BINARY_DIR}" + ) + +add_test(NAME run_test_ble_fail COMMAND test_ble_fail) + diff --git a/tests/BLE_fail/test_ble_fail.cpp b/tests/BLE_fail/test_ble_fail.cpp new file mode 100644 index 00000000..44bcdcfb --- /dev/null +++ b/tests/BLE_fail/test_ble_fail.cpp @@ -0,0 +1,111 @@ +#include + +#include "decoder.h" + +// Service data test input [test name] [data] +const char* test_servicedata[][2] = { + {"Mi flora", "712098004a63b6658d7cc40d071003f32600"}, + {"Mi flora", "712098005763b6658d7cc40d0810011e"}, + {"Mi flora", "712098000163b6658d7cc40d0410024001"}, + {"Mi flora", "712098000863b6658d7cc40d0910020000"}, + {"Cleargrass clock", "70205b04756ab883c8593f090410020001"}, + {"Cleargrass clock", "70205b04dc6ab883c8593f09061002b202"}, + {"Cleargrass clock", "70205b04756ab883c8593f090410020901"}, + {"Mi jia round sensor", "5020aa0137dfaa33342d580d100404016602"}, + {"Mi jia round sensor", "5020aa018ddfaa33342d580610026602"}, + {"Cleargrass THP sensor", "08094c0140342d5801040801870207024f2702015c"}, + {"Cleargrass THP sensor", "08094c0140342d5801040f01880207024f2702015c"}, + {"Cleargrass THP sensor", "08094c0140342d5801040c01810207024d2702015c"}, + {"Cleargrass round sensor", "5030470341743e10342d580410021201"}, + {"Cleargrass round sensor", "5030470383743e10342d580d100410017e02"}, + {"Cleargrass round sensor", "0807743e10342d5801041201720202010d"}, + {"Cleargrass round sensor", "8816YYYYYYYYYYYY0104eb001b01020164"}, + {"Cleargrass round sensor", "8816xxxxxxxxxxxx0104f4003b01020164"}, + {"Cleargrass alarm clock", "080caffd50342d5801040a017f0202012a"}, + {"Cleargrass alarm clock", "080caffd50342d5801040d019e0202012a"}, + {"Cleargrass alarm clock", "080caffd50342d5801040e01910202012a"}, + {"Qingping TH lite", "8810799111342d580104e8008f0302010b"}, + {"Qingping TH lite", "8810799111342d580104e9001d0202010b"}, + {"Qingping Door Open", "0804751060342d580201600f012b0f0100"}, + {"Qingping Door Close", "0804751060342d580201600f01420f0101"}, + {"Qingping Door Open Action", "4804751060342d580401000f01cb"}, + {"Qingping Door Close Action", "4804751060342d580401010f01d5"}, + {"Formaldehyde detector", "5020df02383a5c014357480a10015e"}, + {"Formaldehyde detector", "5020df02283a5c014357480610025302"}, + {"Formaldehyde detector", "5020df025b3a5c014357481010020800"}, + {"SHOULD FAIL", "0c01810207024d270201508094c0140342d5801040"}}; + +// manufacturer data test input [test name] [device name] [data] +const char* test_mfgdata[][3] = { + {"Inkbird TH1", "sps", "660a03150110805908"}, + {"SHOULD FAIL", "fail", "270201508094c014"}}; + +// uuid test input [test name] [uuid] [data source] [data] +const char* test_uuid[][4] = { + {"MiBand", "fee0", "servicedata", "a21e0000"}, + {"SHOULD FAIL", "fa11", "servicedata", "123456789ABCDEF"}}; + +int main() { + StaticJsonDocument<1024> doc; + JsonObject bleObject; + + for (unsigned int i = 0; i < sizeof(test_servicedata) / sizeof(test_servicedata[0]); ++i) { + doc.clear(); + std::cout << "trying " << test_servicedata[i][0] << " : " << test_servicedata[i][1] << std::endl; + doc["servicedata"] = test_servicedata[i][1]; + bleObject = doc.as(); + if (decodeBLEJson(bleObject)) { + std::cout << "Found : "; + bleObject.remove("servicedata"); + serializeJson(doc, std::cout); + std::cout << std::endl; + } else if (strcmp(test_servicedata[i][0], "SHOULD FAIL") == 0) { + continue; + } else { + std::cout << "FAILED! Error parsing: " << test_servicedata[i][0] << " : " << test_servicedata[i][1] << std::endl; + return 1; + } + } + + for (unsigned int i = 0; i < sizeof(test_mfgdata) / sizeof(test_mfgdata[0]); ++i) { + doc.clear(); + std::cout << "trying " << test_mfgdata[i][0] << " : " << test_mfgdata[i][1] << std::endl; + doc["name"] = test_mfgdata[i][1]; + doc["manufacturerdata"] = test_mfgdata[i][2]; + bleObject = doc.as(); + if (decodeBLEJson(bleObject)) { + std::cout << "Found : "; + bleObject.remove("name"); + bleObject.remove("manufacturerdata"); + serializeJson(doc, std::cout); + std::cout << std::endl; + } else if (strcmp(test_mfgdata[i][0], "SHOULD FAIL") == 0) { + continue; + } else { + std::cout << "FAILED! Error parsing: " << test_mfgdata[i][0] << " : " << test_mfgdata[i][1] << " : " << test_mfgdata[i][2] << std::endl; + return 1; + } + } + + for (unsigned int i = 0; i < sizeof(test_uuid) / sizeof(test_uuid[0]); ++i) { + doc.clear(); + std::cout << "trying " << test_uuid[i][0] << " : " << test_uuid[i][1] << std::endl; + doc[test_uuid[i][2]] = test_uuid[i][3]; + doc["servicedatauuid"] = test_uuid[i][1]; + bleObject = doc.as(); + if (decodeBLEJson(bleObject)) { + std::cout << "Found : "; + bleObject.remove("servicedatauuid"); + bleObject.remove(test_uuid[i][2]); + serializeJson(doc, std::cout); + std::cout << std::endl; + } else if (strcmp(test_uuid[i][0], "SHOULD FAIL") == 0) { + continue; + } else { + std::cout << "FAILED! Error parsing: " << test_uuid[i][0] << " : " << test_uuid[i][1] << " : " << test_uuid[i][2] << " : " << test_uuid[i][3] << std::endl; + serializeJson(doc, std::cout); + std::cout << std::endl; + return 1; + } + } +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..d078eae6 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,4 @@ +link_libraries(1decoder) + +add_subdirectory(BLE) +add_subdirectory(BLE_fail) diff --git a/tests/CompileOptions.cmake b/tests/CompileOptions.cmake new file mode 100644 index 00000000..968d3ad7 --- /dev/null +++ b/tests/CompileOptions.cmake @@ -0,0 +1,90 @@ +if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") + add_compile_options( + -pedantic + -Wall + -Wcast-align + -Wcast-qual + -Wconversion + -Wctor-dtor-privacy + -Wdisabled-optimization + -Werror + -Wextra + -Wformat=2 + -Winit-self + -Wmissing-include-dirs + -Wnon-virtual-dtor + -Wold-style-cast + -Woverloaded-virtual + -Wparentheses + -Wredundant-decls + -Wshadow + -Wsign-promo + -Wstrict-aliasing + -Wundef + ) + +endif() + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.8) + add_compile_options(-g -Og) + else() + add_compile_options(-g -O0) + endif() + + add_compile_options( + -Wstrict-null-sentinel + -Wno-vla # Allow VLA in tests + ) + add_definitions(-DHAS_VARIABLE_LENGTH_ARRAY) + + if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.5) + add_compile_options(-Wlogical-op) # the flag exists in 4.4 but is buggy + endif() + + if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.6) + add_compile_options(-Wnoexcept) + endif() +endif() + +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options( + -Wc++11-compat + -Wdeprecated-register + -Wno-vla-extension # Allow VLA in tests + ) + add_definitions( + -DHAS_VARIABLE_LENGTH_ARRAY + -DSUBSCRIPT_CONFLICTS_WITH_BUILTIN_OPERATOR + ) +endif() + +if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.0) + add_compile_options(-g -Og) + else() + add_compile_options(-g -O0) + endif() +endif() + +if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9.0) + add_compile_options(-g -Og) + else() + add_compile_options(-g -O0) + endif() +endif() + +if(MSVC) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) + add_compile_options( + /W4 # Set warning level + /WX # Treats all compiler warnings as errors. + ) + + if (NOT MSVC_VERSION LESS 1910) # >= Visual Studio 2017 + add_compile_options( + /Zc:__cplusplus # Enable updated __cplusplus macro + ) + endif() +endif()