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;
  }

 

Data Types

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" };

 

Name Value Pair Structure

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 ....

 

The State Machine

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;
}  

 

Some simple “Expect a specific char” states

  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;

 

A “Continue in state until received char is not valid for that that state” state.

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;

 

Determine Value Type:

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;
  

Getting Integer

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;

 

Getting Strings, Booleans and Floats

  • The getting of the string value type is easiest as it is terminated by a double quote whereas the others are terminated by either a comma or closing brace. After the getting  string value state it must therefore go into an extra state where it expects either a comma or closing brace.
  • Once the value string for a Boolean is completed it is parsed into true or false by simple string comparison (after lowercasing it).
  • The getting float value branch is very similar to the integer one. The Float value string is parsed into a Float value using:
nameValue.FloatValue = atof(nameValue.StringValue.c_str());

 

 

More coming