/****************************************************************** Ported version of jsmn-JSON Parser for picoC on Loxone Original version of jsmn can be found here: https://github.com/zserge/jsmn/ Please find the license reference for jsmn at the end of this source file Andrej Konkow, 03/2026, v01 ...Pico-C is a Diva... ******************************************************************/ // The picoC port of jsmn makes use of strict mode and uses the parent links #define NUMBERTOKENS 1000 /* Only key/values are supported. Unsupported are for example: array [ "a", "b", "c" ] */ // Maximum values to be addressed for printing by the user // We have 13 numeric channels and 2 channels for string data output #define MAXCHANNELS 13+2 // Pseudovalue for identfying a "no param" #define STRING_NOPARAM -99999 #define INPUT_HTTP_SERVER getinputtext(0) #define INPUT_HTTP_PAGE getinputtext(1) #define INPUT_JSON_QUERY getinputtext(2) #define INPUT_ITERATION_WAIT_SEC getinput(0) /** * JSON type identifier. Basic types are: * o Object * o Array * o String * o Other primitive: number, boolean (true/false) or null */ enum jsmntype_t { JSMN_UNDEFINED = 0, JSMN_OBJECT = 1 << 0, JSMN_ARRAY = 1 << 1, JSMN_STRING = 1 << 2, JSMN_PRIMITIVE = 1 << 3 }; enum jsmnerr { JSMN_ERROR_NOMEM = -1, /* Not enough tokens were provided */ JSMN_ERROR_INVAL = -2, /* Invalid character inside JSON string */ JSMN_ERROR_PART = -3 /* The string is not a full JSON packet, more bytes expected */ }; /* Recognized datatypes for being printed */ enum json_datatype { DT_NONE = 0, DT_INT = 1, DT_FLOAT = 2, DT_BOOL = 3, DT_STRING = 4 }; /** * JSON token description. * type type (object, array, string etc.) * start start position in JSON data string * end end position in JSON data string */ struct jsmntok { enum jsmntype_t type; int start; int end; int size; int parent; }; // picoc behaves better when tokens area defined globally static struct jsmntok tokens[NUMBERTOKENS]; /** * JSON parser. Contains an array of token blocks available. Also stores * the string being parsed now and current position in that string. */ struct jsmn_parser { unsigned int pos; /* offset in the JSON string */ unsigned int toknext; /* next token to allocate */ int tokencount; /* current tokencount or status */ int toksuper; /* superior token node, e.g. parent object or array */ }; // picoc behaves better when parser is defined globally static struct jsmn_parser parser; /* Holds information about a specified value */ struct json_pointer { char *name; int arrayindex; int datatype; struct json_pointer *oneDeeper; }; /* Structure holding values to be printed. Due to delays we first calculate all values and print them then at once */ struct printValue { int intValue; float floatValue; char *stringValue; int stringChannel; }; // Maps for making efficient decisions static char MapIsHexChar[127]; static char MapIsPrimitive[127]; static char MapIsPrimitiveNumber[127]; static char MapIsDisplayableChar[127]; static char MapIsEOL[127]; /* Helperfunctions for initialization */ void init(char *array, int size, int value) { int x; for (x=0; xname = NULL; p->datatype = DT_NONE; p->arrayindex = 0; p->oneDeeper = NULL; } } /** * Allocates an unused token */ struct json_pointer *json_alloc_element(int dt) { struct json_pointer *element; element = (struct json_pointer*) malloc(sizeof(struct json_pointer)); element->name = NULL; element->datatype = dt; element->arrayindex = 0; element->oneDeeper = NULL; return element; } struct jsmntok *jsmn_alloc_token_AndFill(enum jsmntype_t type, int start, int end) { struct jsmntok *tok; if (parser.toknext >= NUMBERTOKENS) return NULL; tok = &tokens[parser.toknext]; parser.toknext++; tok->start = start; tok->end = end; tok->type = type; tok->size = 0; tok->parent = -1; return tok; } /** * Fills token type and boundaries. */ void jsmn_fill_token(struct jsmntok *token, enum jsmntype_t type, int start, int end) { token->type = type; token->start = start; token->end = end; token->size = 0; } /** * Creates a new parser based over a given buffer with an array of tokens * available. */ void jsmn_init() { parser.pos = 0; parser.toknext = 0; parser.tokencount = 0; parser.toksuper = -1; } /* Helperfunctions for messaging output */ void messageParam (char *msg, int val) { messageParamTimed (msg, val, 1000); } void messageParamTimed(char *msg, int val, int sleepMs) { messageParamTimedChannel(msg, val, sleepMs, 0); } void messageParamTimedChannel(char *msg, int val, int sleepMs, int channel) { if (val == STRING_NOPARAM) { setoutputtext(channel, msg); } else { char szOutMsg[150]; sprintf(szOutMsg,"%s %d", msg, val); setoutputtext(channel, szOutMsg); } if (sleepMs > 0) sleep(sleepMs); } /* * Check token name */ int jsoneq(char *json, struct jsmntok *tok, char *s) { if (tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && strncmp(json + tok->start, s, tok->end - tok->start) == 0) { return 0; } return -1; } /* Extract a specific String from the main json */ char *getString(char *dest, char* src, int start, int end) { strncpy(dest, &src[start], end-start); dest[end-start] = 0; return dest; } /* Extract value from the found token */ struct jsmntok *getValue(char *json, int tokenidx, struct json_pointer *element) { struct jsmntok *token; int current = tokenidx+1, // move one token ahead idx = 0; token = &tokens[current]; if (token->type == JSMN_OBJECT) { if (element->oneDeeper == NULL) return NULL; return searchValue(json, current, element->oneDeeper); } else if (token->type == JSMN_ARRAY) { while (idx != element->arrayindex+1 && tokenidx < parser.tokencount) // Iterate to the specified element { tokenidx++; token = &tokens[tokenidx]; if (token->parent == current) idx++; } if (token->type == JSMN_OBJECT || token->type == JSMN_ARRAY) { if (element->oneDeeper == NULL) return NULL; return searchValue(json, tokenidx, element->oneDeeper); } if (element->oneDeeper != NULL && jsoneq(json, token, element->oneDeeper->name) == 0) return &tokens[tokenidx+1]; return NULL; } else return token; // String or Primitive } /* Search for the right value in the token pool */ struct jsmntok *searchValue(char *json, int parent, struct json_pointer *element) { int i, current; struct jsmntok *token; if (element == NULL || element->datatype == DT_NONE) return NULL; for (i = parent+1; i < parser.tokencount; i++) { token = &tokens[i]; if (token->parent != parent) // Only work with token having the right parent continue; // Little optimization for picoC: Check the first character // for equalness before making string-actions and externalize getValue()-code if (element->name[0] == json[token->start] && jsoneq(json, token, element->name) == 0) return getValue(json, i, element); } return NULL; } /* Helperfunction for formatting output-values, printing etc. */ void padValue(char *retValue, int value) { if (value <10) sprintf(retValue, "0%d", value); else sprintf(retValue, "%d", value); } /* Gets the current time and returns it formatted. */ char *getFormattedTime(char *buffer, int timeonly, int time) { char s_month[3], s_day[3]; char s_hour[3], s_minute[3], s_second[3]; padValue(s_hour, gethour(time, 1)); padValue(s_minute, getminute(time, 1)); padValue(s_second, getsecond(time, 1)); if (timeonly) { sprintf (buffer, "%s:%s:%s", s_hour, s_minute, s_second); } else { padValue(s_month, getmonth(time, 1)); padValue(s_day, getday(time, 1)); sprintf (buffer, "%s.%s %s:%s:%s", s_day, s_month, s_hour, s_minute, s_second); } return buffer; } /* Print final results on the output channels */ void printResults(char *json, struct json_pointer *elements) { struct printValue printValues[MAXCHANNELS], *pv; struct json_pointer *element; struct jsmntok *token; int i, printChannel=0, stringChannel=1; char szHlp[150]; for (i=0; iintValue = 0; else if (element->datatype == DT_INT) pv->intValue = atoi(&json[token->start]); else if (element->datatype == DT_FLOAT) pv->floatValue = atof(&json[token->start]); else if (element->datatype == DT_STRING || element->datatype == DT_NONE) { getString(szHlp, json, token->start, token->end); pv->stringValue = strdup(szHlp); pv->stringChannel = stringChannel; stringChannel++; } else if (element->datatype == DT_BOOL) { if (json[token->start] == 't') pv->intValue = 1; else pv->intValue = 0; } } for (i=0; idatatype == DT_INT || element->datatype == DT_BOOL) setoutput(printChannel, pv->intValue); else if (element->datatype == DT_FLOAT) setoutput(printChannel, pv->floatValue); else if (element->datatype == DT_NONE) setoutput(printChannel, 0); else if (element->datatype == DT_STRING) { if (pv->stringValue != NULL) { setoutputtext(pv->stringChannel, pv->stringValue); free(pv->stringValue); } else { setoutputtext(pv->stringChannel, "NULL value"); } printChannel--; } printChannel++; } } /*********************************************************** Functions for parsing the queries Parse the query to the end of the line and terminate it */ int parseElement(char *toParse, struct json_pointer *key) { int idx=0, idxClose=0, len_toParse = strlen(toParse); key->name = toParse; for (idx=0; ! MapIsEOL[toParse[idx]]; idx++) { if (toParse[idx] == '.') { toParse[idx] = 0; idx++; key->oneDeeper = json_alloc_element(key->datatype); return idx + parseElement(&toParse[idx], key->oneDeeper); } else if (toParse[idx] == '[') { toParse[idx] = 0; idx++; while (toParse[idx+idxClose] != ']') idxClose++; toParse[idx+idxClose] = 0; key->arrayindex = atoi(&toParse[idx]); idx += idxClose; } else if (toParse[idx] == ' ') { while (!MapIsEOL[toParse[idx]]) idx++; return idx; } } if (toParse[idx] != 0) { toParse[idx] = 0; idx++; } return idx; } /* Map character to datatype */ int getDatatype(char c, char d) { if (d != ':') return DT_NONE; if (c == 'i') return DT_INT; else if (c == 'f') return DT_FLOAT; else if (c == 'b') return DT_BOOL; else if (c == 's') return DT_STRING; return DT_NONE; } /* Get the queried data string and parse it. */ void parseQuery(char *json_queries, struct json_pointer *elements, int size) { int startIdx = 0, elementIdx = 0, len = strlen(json_queries); struct json_pointer *element; initElements(elements, size); do { // Just in case: Skip all leading whitespaces while (startIdxdatatype = getDatatype(json_queries[startIdx], json_queries[startIdx+1]); startIdx+=2; // Skip the datatype, for example: f: startIdx += parseElement(&json_queries[startIdx], element); } } while (startIdxtype == JSMN_OBJECT) { parser.tokencount = JSMN_ERROR_INVAL; return; } t->size++; token->parent = parser.toksuper; } parser.toksuper = parser.toknext - 1; parser.tokencount++; } /* Externalize code for faster execution in picoC */ void closeBracket(enum jsmntok type) { struct jsmntok *token; if (parser.toknext < 1) { parser.tokencount = JSMN_ERROR_INVAL; return; } token = &tokens[parser.toknext - 1]; for (;;) { if (token->start != -1 && token->end == -1) { if (token->type != type) { parser.tokencount = JSMN_ERROR_INVAL; return; } token->end = parser.pos + 1; parser.toksuper = token->parent; break; } if (token->parent == -1) { if (token->type != type || parser.toksuper == -1) { parser.tokencount = JSMN_ERROR_INVAL; return; } } token = &tokens[token->parent]; } } /* Externalize code for faster execution in picoC */ void openingQuotes(char *json, int jsonLen) { struct jsmntok *token; char c; int start = parser.pos; parser.pos++; // Skip starting quote for (; parser.pos < jsonLen && json[parser.pos] != '\0'; parser.pos++) { // Fast forward for picoC... while (parser.pos < jsonLen && MapIsPrimitive[json[parser.pos]]) parser.pos++; /* Quote: end of string */ if (json[parser.pos] == '\"') { token = jsmn_alloc_token_AndFill(JSMN_STRING, start + 1, parser.pos); if (token == NULL) { parser.pos = start; parser.tokencount = JSMN_ERROR_NOMEM; return; } token->parent = parser.toksuper; break; } /* Backslash: Quoted symbol expected */ if (json[parser.pos] == '\\' && parser.pos + 1 < jsonLen) { handleEscapes(json, jsonLen, start); if (parser.tokencount < 0) return; } } if (parser.pos == jsonLen || json[parser.pos] == '\0') { parser.pos = start; parser.tokencount = JSMN_ERROR_PART; return; } if (parser.toksuper != -1) { token = &tokens[parser.toksuper]; token->size++; } parser.tokencount++; } /* Handles escape characters. Externalized from statement due to performance reasons */ void handleEscapes(char *json, int jsonLen, int start) { char c; int i; parser.pos++; c = json[parser.pos]; /* Allowed escaped symbols */ if (c == '\"' || c == '/' || c == '\\' || c == 'b' || c == 'f' || c == 'r' || c == 'n' || c == 't') { return; } else if (c == 'u') // For example \u2019 means the specific unicode character { parser.pos++; for (i = 0; i < 4 && parser.pos < jsonLen && json[parser.pos] != 0; i++) { /* If it isn't a hex character we have an error */ if (!MapIsHexChar[json[parser.pos]]) { parser.pos = start; parser.tokencount = JSMN_ERROR_INVAL; return; } parser.pos++; } parser.pos--; } else { /* Unexpected symbol */ parser.pos = start; parser.tokencount = JSMN_ERROR_INVAL; return; } } /* Externalize code for faster execution in picoC */ void isPrimitiveNumber(char *json, int jsonLen) { struct jsmntok *token, *t; char c; int start = parser.pos; if (parser.toksuper != -1) t = &tokens[parser.toksuper]; /* And they must not be keys of the object */ if (parser.toksuper != -1) { if (t->type == JSMN_OBJECT || (t->type == JSMN_STRING && t->size != 0)) { parser.tokencount = JSMN_ERROR_INVAL; return; } } for (; parser.pos < jsonLen && json[parser.pos] != 0; parser.pos++) { // Fast forward for picoC... while (parser.pos < jsonLen && MapIsPrimitive[json[parser.pos]]) parser.pos++; c = json[parser.pos]; if (!MapIsDisplayableChar[c] || c == ',' || c == ']' || c == '}') { token = jsmn_alloc_token_AndFill(JSMN_PRIMITIVE, start, parser.pos); if (token == NULL) { parser.pos = start; parser.tokencount = JSMN_ERROR_NOMEM; return; } token->parent = parser.toksuper; parser.pos--; break; } } if (parser.pos == jsonLen || json[parser.pos] == 0) { parser.pos = start; parser.tokencount = JSMN_ERROR_PART; return; } if (parser.toksuper != -1) t->size++; parser.tokencount++; } /* End of JSON parsing functions *************************************************** Main function initiating everything */ void main() { int i, r, jsonLen, start, stringChannel, sleeptimeSec=0, now; char szHlp[150], c, *json, *json_queries, *server, *page; struct jsmntok *t; struct json_pointer elements[MAXCHANNELS], *element; json_queries = INPUT_JSON_QUERY; // All the queries specified by the user server = INPUT_HTTP_SERVER; page = INPUT_HTTP_PAGE; sleeptimeSec = INPUT_ITERATION_WAIT_SEC; if (sleeptimeSec == 0) sleeptimeSec = 5; parseQuery(json_queries, elements, MAXCHANNELS); while (TRUE) { jsmn_init(); sprintf(szHlp, "Fetching data from: http://%s%s", server, page); messageParam(szHlp, STRING_NOPARAM); json = httpget(server,page); jsonLen = strlen(json); messageParam("Fetching done. Parsing...", STRING_NOPARAM); now = getcurrenttime(); /* We have three cases for the beginning of the JSON structure: 1. An object starting with { 2. An array directly starting with [ 3. An array starting with a key/arrayvalue like: "key" : [ Currently only case 1 is supported. */ // picoC: Fast forward to first JSON relevant character while (parser.pos < jsonLen && json[parser.pos] != '{') parser.pos++; for (; parser.pos < jsonLen; parser.pos++) { // picoC: Fast forward ignores while (parser.pos < jsonLen && ! MapIsDisplayableChar[json[parser.pos]]) parser.pos++; c = json[parser.pos]; if (c == 0) break; if (MapIsPrimitiveNumber[c]) /* In strict mode primitives are: numbers and booleans */ isPrimitiveNumber(json, jsonLen); else if (c == '{') openBracket(JSMN_OBJECT); else if (c == '[') openBracket(JSMN_ARRAY); else if (c == '}') closeBracket(JSMN_OBJECT); else if (c == ']') closeBracket(JSMN_ARRAY); else if (c == '\"') openingQuotes(json, jsonLen); else if (c == ':') parser.toksuper = parser.toknext - 1; else if (c == ',') { if (parser.toksuper != -1) { t = &tokens[parser.toksuper]; if (t->type != JSMN_ARRAY && t->type != JSMN_OBJECT) parser.toksuper = t->parent; } } else parser.tokencount = JSMN_ERROR_INVAL; /* Unexpected char in strict mode */ if (parser.tokencount < 0) break; } for (i = parser.toknext - 1; i >= 0 && parser.tokencount >= 0; i--) { t = &tokens[i]; /* Unmatched opened object or array */ if (t->start != -1 && t->end == -1) { parser.tokencount = JSMN_ERROR_PART; break; } } if (parser.tokencount < 0) // Default: We expect a valid JSON with a top-level object-element, else: error { if (parser.tokencount == -1) sprintf(szHlp, "Tokenpool too small: %d. Increase NUMBERTOKENS.", NUMBERTOKENS); else sprintf(szHlp, "Failed to parse JSON with errorcode %d", parser.tokencount); messageParamTimed(szHlp, STRING_NOPARAM, 2000); sprintf(szHlp, "Parseerror(%d). Next JSON check: %s", parser.tokencount, getFormattedTime(szHlp, 1, getcurrenttime() + sleeptimeSec)); } else { sprintf(szHlp, "Parsed %d token in %ds", parser.toknext-1, getcurrenttime() - now); messageParam(szHlp, STRING_NOPARAM); printResults(json, &elements); sprintf(szHlp, "Next JSON check: %s", getFormattedTime(szHlp, 1, getcurrenttime() + sleeptimeSec)); } free(json); messageParam(szHlp, STRING_NOPARAM); sleeps(sleeptimeSec); } } main(); /* * Regarding the jsmn Parser part: Please take notice of the following MIT License * * MIT License * * Copyright (c) 2010 Serge Zaitsev * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */