This blog covers the Stream Parser - State Machine code in detail. as discussed the previous blog
Ardjson: https://ardjson.codeplex.com
Previous: Ardjson Part-7: Arduino JSon Stream Parser
This version is implemented in 1.3c.1 JsonParserToGetTelemetrySensorValues.zip on the Downloads tab at https://ardjson.codeplex.com
The State Machine States
Implemented as enums. The Global State Variable is of that type.
enum Expecting { startOfArray, startOfRecord, startOfName, gettingName, nameValueSeparator, startOfValue, gettingValue, gettingEndOfValueORRecord, gotEndOfRecord, gettingRecordSeparator, done, error, gettingString, gettingBoolean, gettingInteger, gettingFloat }; //State Variable. Starts expecting a Expecting parseState=startOfArray;
Some states expect a specific character:
char ExpectArray[11] = ""; //X is don't care //Which is used in // For many states when its parse requierment is satified: state <-- state++ void IncrementState() { parseState = (Expecting)((int)parseState + 1); } // For states where state increments by one if the expected character is the // current one in the stream. boolean Expect(char c) { if (c == ExpectArray[parseState]) { IncrementState(); return true; } else { //Expectation wasn't satified so error parseState = error; return false; }
These are enums as well. A string representation of each is put in array so that this can be easily return when printing the data type:
// Type of data for values in name-value pairs enum DataType { tString, tBoolean, tInteger, tFloat, tUnknown }; // String represetation of Types of data as immediately above. // Just use array indexed on data type to get its string String DataTypeToString[5] = { "String", "Boolean", "Integer", "Float", "Unknown" };
The StringValue of the struct isn’t in the overlapping union as it is used when the values are progressively generated by concatenating the characters together. When the value string is generation is finished, the non-string values are generated by a suitable parser. The DataType is recorded so further processing, such as printing, can get the correct value type from the structure.
struct NameValue { String Name; String StringValue; union { boolean BooleanValue; int IntegerValue; float FloatValue; }; DataType DType; }; // All parsed name-value pairs use this. // Could be an array to hold all name-value pairs for one record. NameValue nameValue;
As an example the function .. outputs conditionally depending upon the DataType:
// Formats and prints one name-value pair. // Format depends upon data type void DisplayANameValuePair() { Serial.print("Name: "); Serial.println(nameValue.Name); Serial.print(" Value: "); if (nameValue.DType == tString) { Serial.print('"'); Serial.print(nameValue.StringValue); Serial.println('"'); } else if (nameValue.DType == tBoolean) { if (nameValue.BooleanValue) Serial.println("true"); else Serial.println("false"); } etc ....
Its one big switch-case statement":
boolean ParseJsonString(char c) { boolean result = true; boolean gotANameValuePair = false; switch (parseState) { ... A case for each state ... ... } if (gotANameValuePair) { // Completed a name value pair to format print it. DisplayANameValuePair(); } else if (!result) // If any errors then signal halt to state machine. parseState = error; return result; }
case startOfArray: result = Expect(c); case startOfRecord: result = Expect(c); break; case startOfName: result = Expect(c); if (result) { //Got { so new record. nameValue.Name = ""; nameValue.StringValue = ""; nameValue.BooleanValue = false; nameValue.IntegerValue = 0; nameValue.FloatValue = 0; nameValue.DType = tUnknown; } break;
Eg Get name terminates on double quote
case gettingName: if (c != '"') { //Append to name if (isAlpha(c)) nameValue.Name += c; else result = false; } else { //Got end of name IncrementState(); } break;
case startOfValue: switch (c) { case '"': parseState = gettingString; break; case 'T': parseState = gettingBoolean; nameValue.StringValue += c; break; ... case 'F': parseState = gettingBoolean; nameValue.StringValue += c; break; ... default: if (isDigit(c)) { parseState = gettingInteger; nameValue.StringValue += c; } else result = false; break; } break;
Note parse of string to integer when done.
Also note switch to Float if period is encompassed.
The parsing of the integer value string into an integer is accomplished through:
nameValue.IntegerValue = nameValue.StringValue.toInt();
The Getting Integer Branch:
case gettingInteger: if ((c == '}') || (c == ',')) { nameValue.IntegerValue = nameValue.StringValue.toInt(); nameValue.DType = tInteger; gotANameValuePair = true; if (c == '}') { parseState = gotEndOfRecord; } else //Comma, so start new name-value pair parseState = startOfName; } else if (c == '.') { parseState = gettingFloat; nameValue.StringValue += c; } else if (isDigit(c)) { nameValue.StringValue += c; } else result=false; break;
nameValue.FloatValue = atof(nameValue.StringValue.c_str());
More coming