NMEA0183 parser object, implemented in ES script language

// NMEA183 parser
//
// -----------------------------------------------------------------------------------
// Talker sentences
// 
// $TTSSS[,D0,D1,....][*HH]<CR><LF>
// TT - talker ID
// SSS - sentence ID
// D0..DN - data fields
// *HH - optional hex checksum byte = All sentence chars between $ and *, XORED.
//
// Sentence length (between $ and <CR><LF>) is variable, up to max value of 80 bytes.
//
// -----------------------------------------------------------------------------------
// Query sentences
//
// $TTLLQ,SSS[*HH][<CR>][<LF>] 
// TT - talker (requester) ID
// LL - listener ID (device, being queried)
// Q - literal query sentence identifier
//
// -----------------------------------------------------------------------------------
// Proprietary sentences
//
// $PMMM[MD0..MDN][,D0,D1,...][*HH]<CR><LF>
// P - literal proprietary sentence identifier
// MMM - manufacturer ID
// MD0..MDN - manufacturer-specific data
// The rest of the sentence is formatted the same way as the generic one.
//

object EsNmea0183
{
  var m_re,     //< Sentence regex parser 
    m_tok,      //< Sentence tokenizer
    m_buff,     //< Receive buffer
    m_handler;  //< NMEA event handler object
  
  // Reset reception buffer
  function receptionBufferReset()
  {
    m_buff = EsVar::as(EsVariantType$$VAR_BIN_BUFFER);
  }
  
  // Calculate sentence checksum, given the input sentence string.
  function checksumGet(sentence)
  var sum = 0, char;
  {
    foreach(char in sentence)
      sum ^= char#asByte();
    
    return sum#asByte();
  }
  
  // Helper method - string send over the channel
  function strSend(chnl, str)
  var buff = EsStr::toByteString("$" + str);
  {
    return chnl.putBytes( 
      buff,
      chnl.txTimeEstimateGet(
        buff#countGet()
      )
    ) == buff#countGet();  
  }
  
  // Send query sentence
  function querySend(chnl, talkerId, listenerId, sentenceId, withChecksum, withCrLf)
  var cs, str;
  {
    str = talkerId#asString() + listenerId#asString() + "Q," + sentenceId#asString();

    if( withChecksum )
    {
      cs = checksumGet(str);
      str += EsStr::format("*%02X", cs);
    }
    
    if( withCrLf )
    {
      str += "\r\n";
    }
    
    return strSend(chnl, str);
  }
  
  // Send proprietary sentence
  function proprietarySend(chnl, manufacturerId, manufacturerData, data, withChecksum, withCrLf)
  var cs, str;
  {
    str = "P" + manufacturerId#asString() + manufacturerData#asString();
    
    if( !data#isEmpty() )
    {
      if( data#isCollection() )
        str += EsStr::fromArray(data, ",", ",", "");
      else
        str += "," + data#asString();
    }
    
    if( withChecksum )
    {
      cs = checksumGet(str);
      str += EsStr::format("*%02X", cs);
    }
    
    if( withCrLf )
    {
      str += "\r\n";
    }
    
    return strSend(chnl, str);
  }
  
  // Parse sentence(s) from input string
  function parseString(input)
  var rngSentence, 
    rngID1, rngID2, 
    rngData,
    rngChsum,
    cs, csReceived, csOk,
    endPos, data;
  {
    if( !input#isString() )
      throw "Wrong parser input type. String expected.";
  
    if( m_handler#isEmpty() || input#countGet() < 8 )
      return;
  
    m_re$text = input;
    
    while( m_re$matches )
    {
      csOk = true;
      rngSentence = m_re.matchRangeGet(0); //< Entire sentence match range
      rngData = m_re.matchRangeGet(7);
      rngChsum = m_re.matchRangeGet(8);
      
      // Check if sentence checksum is OK
      if( rngChsum )
      {
        cs = input#sliceGet(rngChsum[0], rngChsum[1]);
        csReceived = (EsStr::hexToBinNibble(cs[0]) << 4) + 
          EsStr::hexToBinNibble(cs[1]);
        
        endPos = rngSentence[1]-1;
        if( '\n' == input[endPos] )
          --endPos;
        if( '\r' == input[endPos] )
          --endPos;
        endPos -= 2;  

        cs = checksumGet(
          input#sliceGet(rngSentence[0]+1, endPos)
        );
        
//        EsScriptDebug::log("inCS = %d; calcCS '%s' = %d", csReceived, input#sliceGet(rngSentence[0]+1, endPos), cs);
        csOk = cs == csReceived;
      }
      
      if( csOk )
      {
        // Tokenize data, if any
        if( rngData )
        {
          m_tok$text = input#sliceGet(rngData[0], rngData[1]);
          
//          EsScriptDebug::log( input#sliceGet(rngData[0], rngData[1]) );
          
          data = [];
          while( m_tok$moreTokens )
            data += m_tok$nextToken;
        }
        else
          data = null;

        rngID1 = m_re.matchRangeGet(3);   //< Match 3..4 is common sentence
        if( rngID1 )
        {
          rngID2 = m_re.matchRangeGet(4);
          
          m_handler.onNmeaTalkerSentence(          
            input#sliceGet(rngID1[0], rngID1[1]),
            input#sliceGet(rngID2[0], rngID2[1]),
            data
          );
          
          goto nextMatch;
        }

        rngID1 = m_re.matchRangeGet(5);   //< Match 5..6 is proprietary sentence
        if( rngID1 )
        {
          rngID2 = m_re.matchRangeGet(6);
          
          m_handler.onNmeaProprietarySentence(          
            input#sliceGet(rngID1[0], rngID1[1]),
            input#sliceGet(rngID2[0], rngID2[1]),
            data
          );
          
          goto nextMatch;
        }
        
        rngID1 = m_re.matchRangeGet(1);   //< Match 1..2 is query sentence
        if( rngID1 )
        {
          rngID2 = m_re.matchRangeGet(2);
          
          m_handler.onNmeaQuerySentence(          
            input#sliceGet(rngID1[0], rngID1[1]),
            input#sliceGet(rngID2[0], rngID2[1]),
            data
          );
        }
      }
      
    label nextMatch:      
      m_re$offset = rngSentence[1];
    }
    
    return m_re$offset;
  }
  
  // Parse sentence(s) from input byte buffer
  function parseBinBuffer(bb)
  {
    if( EsVariantType$$VAR_BIN_BUFFER != bb#typeGet() )
      throw "Wrong parser input type. Byte buffer is expected";
      
    return parseString(
      EsStr::fromByteString(bb, EsStrByteEncoding$$ASCII)
    );
  }
  
  // Parse sentence(s) from input string|string collection|byte buffer)
  function parse(input)
  var item;
  {    
    if( input#isCollection() )
    {
      foreach(item in input)
      {
        if( EsVariantType$$VAR_BIN_BUFFER == item#typeGet() )
          parseBinBuffer( item );
        else
          parseString( item#asString() );
      }
    }
    else if( EsVariantType$$VAR_BIN_BUFFER == input#typeGet() )
      parseBinBuffer( input );
    else
      parseString( input#asString() );  
  }
  
  // Read sentence(s) from the IO channel, and parse them
  function sentencesReceive(chnl)
  var tmp, 
    chEstimate = chnl#isEmpty() ? 1 : chnl.txTimeEstimateGet(1),
    parseEnd;
  {
    if( m_buff#find(B'$')#isEmpty() )
    {
      // Try to read $ from channel, given N retries, and chEstimate ms per each try
      if( EsByteReadStatus$$Success == 
          EsCommUtils::specificByteReceive(
            chnl, 
            B'$', 
            256, 
            chEstimate
          ) 
      )
      {
        m_buff += B'$';
//        EsScriptDebug::log("Sentence start synchronized");
      }
      else
      {
//        EsScriptDebug::log("Sentence start not synchronized");      
        return;
      }
    }
    
    tmp = chnl.bytesGet(128, 128*chEstimate);
    if( !tmp#isEmpty() )
    {
      m_buff += tmp;
      parseEnd = parseBinBuffer(m_buff);
      if( !parseEnd#isEmpty() ) //< Exclude parsed sequence part from the buffer
        m_buff = m_buff#sliceGet(parseEnd, m_buff#countGet());
      else //< Nothing was parsed - reset the buffer
        receptionBufferReset();
    }
  }

  new()
  {
    m_re = new EsRegEx(
      "\\$(?:(?:([A-Z]{2})([A-Z]{2})Q)|(?:([A-Z]{2})([A-Z]{3}))|(?:P([A-Z]{3})([^\\*,\\r\\n]+)?)),?([^\\*\\r\\n\\$]+)?(?:\\*([0-9A-F]{2}))?\\r?\\n",
      EsRegExCompileFlag$$DEFAULT
    );
    
    m_tok = new EsStringTokenizer();
    m_tok$separators = ",";
    m_tok$skipMultipleSeparators = false;
    
    receptionBufferReset();
  }
  
  // Properties
  //
  
  // NMEA event handler
  property handler;
  read: { return m_handler; }
  write: 
  { 
    if( __value )
      // Assign weak reference to the handler variable, to avoid circular referencing
      m_handler = __value$weakReference;
    else
      m_handler = null;
  }
}