Skip to content

Commit

Permalink
Merge pull request #9357 from s-hadinger/alt_json_parser
Browse files Browse the repository at this point in the history
Change replace ArduinoJson with JSMN for JSON parsing - Phase 1
  • Loading branch information
arendst authored Sep 22, 2020
2 parents 75252af + ec52c1a commit a39e393
Show file tree
Hide file tree
Showing 21 changed files with 2,078 additions and 357 deletions.
181 changes: 181 additions & 0 deletions lib/jsmn-shadinger-1.0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# JSMN lightweight JSON parser for Tasmota

Intro:

## Benefits

## How to use

`{"Device":"0x1234","Power":true,"Temperature":25.5}`

The JSON above is split into 8 tokens, and requires 32 bytes of dynamic memory.

- Token 0: `OBJECT`, size=3: `{"Device":"0x1234","Power":true,"Temperature":25.5}`
- Token 1: `KEY`: `Device`
- Token 2: `STRING`: `0x1234`
- Token 3: `KEY`: `Power`
- Token 4: `BOOL_TRUE`: `true`
- Token 5: `KEY`: `Temperature`
- Token 6: `FLOAT`: `25.5`
- Token 7: `INVALID`

If what you need is to parse a JSON Object for values with default values:
```
#include "JsonParser.h"
char json_buffer[] = "{\"Device\":\"0x1234\",\"Power\":true,\"Temperature\":25.6}";
JsonParserObject root = JsonParser(json_buffer).getRootObject();
if (!root) { ResponseCmndChar_P(PSTR("Invalid JSON")); return; }
uint16_t d = root.getUInt(PSTR("DEVICE"), 0xFFFF); // case insensitive
bool b = root.getBool(PSTR("Power"), false);
float f = root.getFloat(PSTR("Temperature), -100);
```

Alternative pattern, if you want to test the existence of the attribute first:
```
#include "JsonParser.h"
char json_buffer[] = "{\"Device\":\"0x1234\",\"Power\":true,\"Temperature\":25.6}";
JsonParserObject root = JsonParser(json_buffer).getRootObject();
if (!root) { ResponseCmndChar_P(PSTR("Invalid JSON")); return; }
JsonParserToken val = root[PSTR("DEVICE")];
if (val) {
d = val.getUInt();
}
val = root[PSTR("Power")];
if (val) {
b = val.getBool();
}
val = root[PSTR("Temperature)];
if (val) {
f = val.getFloat();
}
```

## Types and conversion

JSMN relies on the concept of JSON Tokens `JsonParserToken`. Tokens do not hold any data, but point to token boundaries in JSON string instead. Every jsmn token has a type, which indicates the type of corresponding JSON token. JSMN for Tasmota extends the type from JSMN to ease further parsing.

Types are:
- `INVALID` invalid token or end of stream, see Error Handling below
- `OBJECT` a JSON sub-object, `size()` contains the number of key/values of the object
- `ARRAY` a JSON array, `size()` contains the number of sub values
- `STRING` a JSON string, return the sub-string, unescaped, without surrounding quotes. UTF-8 is supported.
- `PRIMITIVE` an unrecognized JSON token, consider as an error
- `KEY` a JSON key in an object as a string
- `NULL` a JSON `null` value, automatically converted to `0` or `""`
- `BOOL_FALSE` a JSON `false` value, automatically converted to `0` or `""`
- `BOOL_TRUE` a JSON `true` value, automatically converted to `1` or `""`
- `UINT` a JSON unsigned int
- `INT` a JSON negative int
- `FLOAT` a JSON floating point value, i.e. a numerical value containing a decimal ot `.`

Note: values are converted in this priority: 1/ `UINT`, 2/ `INT`, 3/ `FLOAT`.

`JsonParserToken` support the following getters:
- `getBool()`: returns true if 'true' or non-zero int (default false)
- `getUInt()`: converts to unsigned int (default 0), boolean true is 1
- `getInt()`: converts to signed int (default 0), boolean true is 1
- `getULong()`: converts to unsigned 64 bits (default 0), boolean is 1
- `getStr()`: converts to string (default "")

There are variants of the same function for which you can choose the default values. Remember that using a getter if the token type is INVALID returns the default value.

Conversion to `OBJECT` or `ARRAY`:
- `getObject()`: converts token to `OBJECT` or `INVALID`
- `getArray()`: converts token to `ARRAY` or `INVALID`

For `JsonParserKey`:
- `getValue()`: returns the value token associated to the key

## Checking Token types

Type checking for `JsonParserToken`:
- `isSingleToken()` returns `true` for a single level token, `false` for `OBJECT`, `ARRAY` and `INVALID`
- `isKey()` returns `true` for a `KEY` within an `OBJECT`
- `isStr()` returns `true` for `STRING` (note: other types can still be read as strings with `getStr()`)
- `isNull()` returns `true` for `NULL`
- `isBool()` returns `true` for `BOOL_FALSE` or `BOOL_TRUE`
- `isUInt()` returns `true` for `UINT` (see below for number conversions)
- `isInt()` returns `true` for `INT` (see below for number conversions)
- `isFloat()` returns `true` for `FLOAT` (see below for number conversions)
- `isNum()` returns `true` for any numerical value, i.e. `UINT`, `INT` or `FLOAT`
- `isObject()` returns `true` for `OBJECT`
- `isArray()` returns `true` for `ARRAY`
- `isValid()` returns `true`for any type except `INVALID`

JSMN for Tasmota provides sub-classes:
- `JsonParserKey` of type `KEY` or `INVALID`, used as a return value for Object iterators
- `JsonParserObject` of type `OBJECT` or `INVALID`, providing iterators
- `JsonParserArray` of type `ARRAY` or `INVALID`, providing iterators

Converting from Token to Object or Array is done with `token.getObject()` or `token.getArray()`. If the conversion is invalid, the resulting object has type `INVALID` (see Error Handling).

## Iterators and accessors for Objects and Arrays

The `JsonParserObject` provides an easy to use iterator:
```
JsonParserToken obj = <...>
for (auto key : obj) {
// key is of type JsonParserKey
JsonParserToken valie = key.getValue(); // retrieve the value associated to key
}
```

If the object contains only one element, you can retrieve it with `obj.getFirstElement()`.

You can access any key with `obj["Key"]`. Note: the search is on purpose **case insensitive** as it is the norm in Tasmota. The search string can be in PROGMEM. If the token is not found, it is of type `INVALID`.

The `JsonParserArray` provides an easy to use iterator:
```
JsonParserArray arr = <...>
for (auto elt : arr) {
// elt is of type JsonParserToken
}
```

You can access any element in the array with the `[]` operator. Ex: `arr[0]` fof first element. If the index is invalid, the token has type `INVALID`.

## Memory

The `JsonParserToken` fits in 32 bits, so it can be freely returned or copied without any penalty of object copying. Hence it doesn't need the `const` modifier either, since it is always passed by value.

## Error handling

This library uses a `zero error` pattern. This means that calls never report any error nor throw exceptions. If something wrong happens (bad JSON, token not found...), function return an **Invalid Token**. You can call any function on an Invalid Token, they will always return the same Invalid Token (aka fixed point).

You can easily test if the current token is invalid with the following:

Short version:
```
if (token) { /* token is valid */ }
```

Long version:
```
if (token->isValiid()) { /* token is valid */ }
```

This pattern allows to cascade calls and check only the final result:
```
char json_buffer[] = "<json>";
JsonParserObject json = JsonParser(json_buffer).getRootObject();
JsonParserToken zbstatus_tok = json["ZbStatus"];
JsonParserObject zbstatus = zbstatus_tok.getObject();
if (zbstatus) { /* do some stuff */
// reaching this point means: JSON is valid, there is a root object, there is a `ZbStatus` key and it contains a sub-object
}
```

Warning: there is an explicit convert to `bool` to allow the short version. Be careful, `(bool)token` is equivalent to `token->isValid()`, it is **NOT** equivalent to `token->getBool()`.

## Limits

Please keep in mind the current limits for this library:
- Maximum JSON buffer size 2047 bytes
- Maximum 63 JSON tokens
- No support for exponent in floats (i.e. `1.0e3` is invalid)

These limits shouldn't be a problem since buffers in Tasmota are limited to 1.2KB. The support for exponent in floats is commented out and can be added if needed (slight increase in Flash size)
8 changes: 8 additions & 0 deletions lib/jsmn-shadinger-1.0/library.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name=JSMN JSON parser customized and optimized for ESP8266 and Tasmota
version=1.0
author=Serge Zaitsev, Stephan Hadinger
maintainer=Stephan <stephan.hadinger@gmail.com>
sentence=Lightweight in-place JSON parser
paragraph=
url=https://github.com/zserge/jsmn
architectures=esp8266
Loading

0 comments on commit a39e393

Please sign in to comment.