Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add nesting and NOT condition support for device conditions. #103

Merged
merged 2 commits into from
Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/participate/adding-decoders.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ The third parameter (fifth if data length is specified) can be either the index
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".

`condition` can contain JSON arrays that can be processed separately. This allows for nesting of detection tests such as:
`"condition": [["servicedata", "index", 0, "1234", "&" "servicedata", "index", 5, "5678"], "|", "servicedata", "index", 30, "ABCD"]`
This will result in a positive detection if the service data at index `0` == `0x1234` and the service data at index `5` == `0x5678`, otherwise, if the service data at index `30` == `0xABCD`, the result will also be positive.

::: warning Note
Nesting is discouraged from use wherever possible as the recursive nature may cause stack overflowing in some circumstaces.
DigiH marked this conversation as resolved.
Show resolved Hide resolved
The above example could be re-written as:
`"condition": ["servicedata", "index", 30, "ABCD", "|", "servicedata", "index", 0, "1234", "&" "servicedata", "index", 5, "5678"]`
Which has the same result, without nesting.
:::

`condition` NOT(!) testing; Anytime a condition test value is preceded by a "!", the inverse of the result will be used to determine the result.
Example: `"condition": ["servicedata", "index", 30, "!", "ABCD", "&", "servicedata", "index", 0, "1234"]
If the value of the service data at index 30 is not 0xABCD and the data at index 0 is 0x1234, the result is a positive detection.

### Properties
Properties is a nested JSON object containing one or more JSON objects. In the example above it looks like:
```
Expand All @@ -82,6 +97,17 @@ Here we have a single property that defines a value that we want to decode. The
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.

`condition` can contain JSON arrays that can be processed separately. This allows for nesting of detection tests such as:
`"condition": [["servicedata", 25, "4", "&" "servicedata", 26, "5"], "|", "servicedata", 30, "ABCD"]`
This will result in a positive detection if the service data at index `25` == `4` and the service data at index `26` == `5`, otherwise, if the service data at index `30` == `0xABCD`, the result will also be positive.

::: warning Note
Nesting is discouraged from use wherever possible as the recursive nature may cause stack overflowing in some circumstaces.
DigiH marked this conversation as resolved.
Show resolved Hide resolved
The above example could be re-written as:
`"condition": ["servicedata", 30, "ABCD", "|", "servicedata", 25, "4", "&" "servicedata", 5, "5"]`
Which has the same result, without nesting.
:::

Property conditions also allow for a NOT comparison, as in
```
"properties":{
Expand Down
249 changes: 140 additions & 109 deletions src/decoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ bool TheengsDecoder::data_index_is_valid(const char* str, size_t index, size_t l
}

int TheengsDecoder::data_length_is_valid(size_t data_len, size_t default_min,
JsonArray& condition, int idx) {
const JsonArray& condition, int idx) {
std::string op = condition[idx + 1].as<std::string>();
if (!op.empty() && op.length() > 2) {
return (data_len >= default_min) ? 0 : -1;
Expand All @@ -149,6 +149,140 @@ int TheengsDecoder::data_length_is_valid(size_t data_len, size_t default_min,
return -1;
}

bool TheengsDecoder::checkDeviceMatch(const JsonArray& condition,
const char* svc_data,
const char* mfg_data,
const char* dev_name,
const char* svc_uuid) {
bool match = false;
size_t cond_size = condition.size();

for (size_t i = 0; i < cond_size;) {
if (condition[i].is<JsonArray>()) {
DEBUG_PRINT("found nested array\n");
match = checkDeviceMatch(condition[i], svc_data, mfg_data, dev_name, svc_uuid);

if (++i < cond_size) {
if (!match && *condition[i].as<const char*>() == '|') {
} else if (match && *condition[i].as<const char*>() == '&') {
match = false;
} else {
break;
}
i++;
} else {
break;
}
}

const char* cmp_str;
const char* cond_str = condition[i].as<const char*>();
int len_idx;
if (svc_data != nullptr && strstr(cond_str, SVC_DATA) != nullptr) {
len_idx = data_length_is_valid(strlen(svc_data), m_minSvcDataLen, condition, i);
if (len_idx >= 0) {
i += len_idx;
cmp_str = svc_data;
match = true;
} else {
match = false;
break;
}
} else if (mfg_data != nullptr && strstr(cond_str, MFG_DATA) != nullptr) {
len_idx = data_length_is_valid(strlen(mfg_data), m_minMfgDataLen, condition, i);
if (len_idx >= 0) {
i += len_idx;
cmp_str = mfg_data;
match = true;
} else {
match = false;
break;
}
} else if (dev_name != nullptr && strstr(cond_str, "name") != nullptr) {
cmp_str = dev_name;
} else if (svc_uuid != nullptr && strstr(cond_str, "uuid") != nullptr) {
cmp_str = svc_uuid;
} else {
break;
}

cond_str = condition[i + 1].as<const char*>();
if (cond_str) {
if (cmp_str == svc_uuid && !strncmp(cmp_str, "0x", 2)) {
cmp_str += 2;
}

if (strstr(cond_str, "contain") != nullptr) {
if (strstr(cmp_str, condition[i + 2].as<const char*>()) != nullptr) {
match = true;
} else {
match = false;
}
i += 3;
} else if (strstr(cond_str, "index") != nullptr) {
size_t cond_index = condition[i + 2].as<size_t>();
size_t cond_len = strlen(condition[i + 3].as<const char*>());

if (!data_index_is_valid(cmp_str, cond_index, cond_len)) {
DEBUG_PRINT("Invalid data %s; skipping\n", cmp_str);
match = false;
break;
}

bool inverse = false;
if (*condition[i + 3].as<const char*>() == '!') {
inverse = true;
}

DEBUG_PRINT("comparing value: %s to %s at index %u\n",
&cmp_str[cond_index],
condition[i + 3 + inverse].as<const char*>(),
condition[i + 2].as<size_t>());

if (strncmp(&cmp_str[cond_index],
condition[i + 3 + inverse].as<const char*>(),
cond_len) == 0) {
match = inverse ? false : true;
} else {
match = inverse ? true : false;
}

i += 4 + inverse;
}

cond_str = condition[i].as<const char*>();
}

size_t cond_size = condition.size();

if (i < cond_size && cond_str != nullptr) {
if (!match && *cond_str == '|') {
i++;
continue;
} else if (match && *cond_str == '&') {
i++;
match = false;
continue;
} else if (match) { // check for AND case before exit
while (i < cond_size && *cond_str != '&') {
if (!condition[++i].is<const char*>()) {
continue;
}
cond_str = condition[++i].as<const char*>();
}

if (i < cond_size && cond_str != nullptr) {
i++;
match = false;
continue;
}
}
}
break;
}
return match;
}

bool TheengsDecoder::checkPropCondition(const JsonArray& prop_condition,
const char* svc_data,
const char* mfg_data) {
Expand Down Expand Up @@ -189,17 +323,15 @@ bool TheengsDecoder::checkPropCondition(const JsonArray& prop_condition,
if (!strncmp(&data_src[prop_condition[i + 1].as<int>()],
prop_condition[i + 2 + inverse].as<const char*>(), cond_len)) {
cond_met = inverse ? false : true;
} else if (inverse) {
cond_met = true;
} else {
cond_met = inverse ? true : false;
}
} else {
DEBUG_PRINT("ERROR property condition data source invalid\n");
return false;
}

if (inverse) {
i++;
}
i += inverse;

if (cond_size > (i + 3)) {
if (!cond_met && *prop_condition[i + 3].as<const char*>() == '|') {
Expand Down Expand Up @@ -254,108 +386,8 @@ int TheengsDecoder::decodeBLEJson(JsonObject& jsondata) {
peakDocSize = doc.memoryUsage();
#endif

JsonArray condition = doc["condition"];
bool match = false;
size_t min_len = m_minMfgDataLen;

for (unsigned int i = 0; i < condition.size();) {
const char* cmp_str;
const char* cond_str = condition[i].as<const char*>();
int len_idx;
if (svc_data != nullptr && strstr(cond_str, SVC_DATA) != nullptr) {
len_idx = data_length_is_valid(strlen(svc_data), m_minSvcDataLen, condition, i);
if (len_idx >= 0) {
i += len_idx;
cmp_str = svc_data;
match = true;
} else {
match = false;
break;
}
} else if (mfg_data != nullptr && strstr(cond_str, MFG_DATA) != nullptr) {
len_idx = data_length_is_valid(strlen(mfg_data), m_minMfgDataLen, condition, i);
if (len_idx >= 0) {
i += len_idx;
cmp_str = mfg_data;
match = true;
} else {
match = false;
break;
}
} else if (dev_name != nullptr && strstr(cond_str, "name") != nullptr) {
cmp_str = dev_name;
} else if (svc_uuid != nullptr && strstr(cond_str, "uuid") != nullptr) {
cmp_str = svc_uuid;
} else {
break;
}

cond_str = condition[i + 1].as<const char*>();
if (cond_str) {
if (cmp_str == svc_uuid && !strncmp(cmp_str, "0x", 2)) {
cmp_str += 2;
}

if (strstr(cond_str, "contain") != nullptr) {
if (strstr(cmp_str, condition[i + 2].as<const char*>()) != nullptr) {
match = true;
} else {
match = false;
}
i += 3;
} else if (strstr(cond_str, "index") != nullptr) {
size_t cond_index = condition[i + 2].as<size_t>();
size_t cond_len = strlen(condition[i + 3].as<const char*>());

if (!data_index_is_valid(cmp_str, cond_index, cond_len)) {
DEBUG_PRINT("Invalid data %s; skipping\n", cmp_str);
match = false;
break;
}
DEBUG_PRINT("comparing index: %s to %s at index %u\n",
&cmp_str[condition[i + 2].as<unsigned int>()],
condition[i + 3].as<const char*>(), condition[i + 2].as<unsigned int>());
if (strncmp(&cmp_str[cond_index], condition[i + 3].as<const char*>(), cond_len) == 0) {
match = true;
} else {
match = false;
}
i += 4;
}

cond_str = condition[i].as<const char*>();
}

unsigned int cond_size = condition.size();

if (i < cond_size && cond_str != nullptr) {
if (!match && *cond_str == '|') {
i++;
continue;
} else if (match && *cond_str == '&') {
i++;
match = false;
continue;
} else if (match) { // check for AND case before exit
while (i < cond_size && *cond_str != '&') {
if (!condition[++i].is<const char*>()) {
continue;
}
cond_str = condition[++i].as<const char*>();
}

if (i < condition.size() && cond_str != nullptr) {
i++;
match = false;
continue;
}
}
}
break;
}

/* found a match, extract the data */
if (match) {
if (checkDeviceMatch(doc["condition"], svc_data, mfg_data, dev_name, svc_uuid)) {
jsondata["brand"] = doc["brand"];
jsondata["model"] = doc["model"];
jsondata["model_id"] = doc["model_id"];
Expand All @@ -365,9 +397,8 @@ int TheengsDecoder::decodeBLEJson(JsonObject& jsondata) {
/* Loop through all the devices properties and extract the values */
for (JsonPair kv : properties) {
JsonObject prop = kv.value().as<JsonObject>();
bool cond_met = checkPropCondition(prop["condition"], svc_data, mfg_data);

if (cond_met) {
if (checkPropCondition(prop["condition"], svc_data, mfg_data)) {
JsonArray decoder = prop["decoder"];
if (strstr((const char*)decoder[0], "value_from_hex_data") != nullptr) {
const char* src = svc_data;
Expand Down
4 changes: 3 additions & 1 deletion src/decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ class TheengsDecoder {
double value_from_hex_string(const char* data_str, int offset, int data_length, bool reverse, bool canBeNegative = true);
double bf_value_from_hex_string(const char* data_str, int offset, int data_length, bool reverse, bool canBeNegative = true);
bool data_index_is_valid(const char* str, size_t index, size_t len);
int data_length_is_valid(size_t data_len, size_t default_min, JsonArray& condition, int idx);
int data_length_is_valid(size_t data_len, size_t default_min, const JsonArray& condition, int idx);
bool checkPropCondition(const JsonArray& prop, const char* svc_data, const char* mfg_data);
bool checkDeviceMatch(const JsonArray& condition, const char* svc_data, const char* mfg_data,
const char* dev_name, const char* svc_uuid);
std::string sanitizeJsonKey(const char* key_in);

size_t m_docMax = 7168;
Expand Down