Commit 99c85f7d authored by Ryan Day's avatar Ryan Day
Browse files

adding wip parser refactor

parent fbc39a83
Loading
Loading
Loading
Loading

lib/constants-v2.js

0 → 100644
+92 −0
Original line number Diff line number Diff line
 
// STK message constants
module.exports.MESSAGE_START = '\x1B'
module.exports.TOKEN         = '\x0E'

// STK general command constants
module.exports.CMD_SIGN_ON               = '\x01'
module.exports.CMD_SET_PARAMETER         = '\x02'
module.exports.CMD_GET_PARAMETER         = '\x03'
module.exports.CMD_SET_DEVICE_PARAMETERS = '\x04'
module.exports.CMD_OSCCAL                = '\x05'
module.exports.CMD_LOAD_ADDRESS          = '\x06'
module.exports.CMD_FIRMWARE_UPGRADE      = '\x07'

// STK ISP command constants
module.exports.CMD_ENTER_PROGMODE_ISP  = '\x10'
module.exports.CMD_LEAVE_PROGMODE_ISP  = '\x11'
module.exports.CMD_CHIP_ERASE_ISP      = '\x12'
module.exports.CMD_PROGRAM_FLASH_ISP   = '\x13'
module.exports.CMD_READ_FLASH_ISP      = '\x14'
module.exports.CMD_PROGRAM_EEPROM_ISP  = '\x15'
module.exports.CMD_READ_EEPROM_ISP     = '\x16'
module.exports.CMD_PROGRAM_FUSE_ISP    = '\x17'
module.exports.CMD_READ_FUSE_ISP       = '\x18'
module.exports.CMD_PROGRAM_LOCK_ISP    = '\x19'
module.exports.CMD_READ_LOCK_ISP       = '\x1A'
module.exports.CMD_READ_SIGNATURE_ISP  = '\x1B'
module.exports.CMD_READ_OSCCAL_ISP     = '\x1C'
module.exports.CMD_SPI_MULTI           = '\x1D'

// STK PP command constants
module.exports.CMD_ENTER_PROGMODE_PP   = '\x20'
module.exports.CMD_LEAVE_PROGMODE_PP   = '\x21'
module.exports.CMD_CHIP_ERASE_PP       = '\x22'
module.exports.CMD_PROGRAM_FLASH_PP    = '\x23'
module.exports.CMD_READ_FLASH_PP       = '\x24'
module.exports.CMD_PROGRAM_EEPROM_PP   = '\x25'
module.exports.CMD_READ_EEPROM_PP      = '\x26'
module.exports.CMD_PROGRAM_FUSE_PP     = '\x27'
module.exports.CMD_READ_FUSE_PP        = '\x28'
module.exports.CMD_PROGRAM_LOCK_PP     = '\x29'
module.exports.CMD_READ_LOCK_PP        = '\x2A'
module.exports.CMD_READ_SIGNATURE_PP   = '\x2B'
module.exports.CMD_READ_OSCCAL_PP      = '\x2C'
module.exports.CMD_SET_CONTROL_STACK   = '\x2D'

// STK HVSP command constants
module.exports.CMD_ENTER_PROGMODE_HVSP = '\x30'
module.exports.CMD_LEAVE_PROGMODE_HVSP = '\x31'
module.exports.CMD_CHIP_ERASE_HVSP     = '\x32'
module.exports.CMD_PROGRAM_FLASH_HVSP  = '\x33'
module.exports.CMD_READ_FLASH_HVSP     = '\x34'
module.exports.CMD_PROGRAM_EEPROM_HVSP = '\x35'
module.exports.CMD_READ_EEPROM_HVSP    = '\x36'
module.exports.CMD_PROGRAM_FUSE_HVSP   = '\x37'
module.exports.CMD_READ_FUSE_HVSP      = '\x38'
module.exports.CMD_PROGRAM_LOCK_HVSP   = '\x39'
module.exports.CMD_READ_LOCK_HVSP      = '\x3A'
module.exports.CMD_READ_SIGNATURE_HVSP = '\x3B'
module.exports.CMD_READ_OSCCAL_HVSP    = '\x3C'

// STK status constants
// Success
module.exports.STATUS_CMD_OK = '\x00'
// Warnings
module.exports.STATUS_CMD_TOUT          = '\x80'
module.exports.STATUS_RDY_BSY_TOUT      = '\x81'
module.exports.STATUS_SET_PARAM_MISSING = '\x82'
// Errors
module.exports.STATUS_CMD_FAILED  = '\xC0'
module.exports.STATUS_CKSUM_ERROR = '\xC1'
module.exports.STATUS_CMD_UNKNOWN = '\xC9'

// STK parameter constants
module.exports.STATUS_BUILD_NUMBER_LOW  = '\x80'
module.exports.STATUS_BUILD_NUMBER_HIGH = '\x81'
module.exports.STATUS_HW_VER            = '\x90'
module.exports.STATUS_SW_MAJOR          = '\x91'
module.exports.STATUS_SW_MINOR          = '\x92'
module.exports.STATUS_VTARGET           = '\x94'
module.exports.STATUS_VADJUST           = '\x95'
module.exports.STATUS_OSC_PSCALE        = '\x96'
module.exports.STATUS_OSC_CMATCH        = '\x97'
module.exports.STATUS_SCK_DURATION      = '\x98'
module.exports.STATUS_TOPCARD_DETECT    = '\x9A'
module.exports.STATUS_STATUS            = '\x9C'
module.exports.STATUS_DATA              = '\x9D'
module.exports.STATUS_RESET_POLARITY    = '\x9E'
module.exports.STATUS_CONTROLLER_INIT   = '\x9F'

// STK answer constants
module.exports.ANSWER_CKSUM_ERROR = '\xB0'

lib/parser-v2.js

0 → 100644
+255 −0
Original line number Diff line number Diff line
var c = require('./lib/constants');
var EventEmitter = require("events").EventEmitter;

module.exports = function(serialport){

  var o = ext(
    new EventEmitter,
    {
    constants:c,
    port:serialport,
    closed:false,
    // write
    _inc:-1,
    _queue:[],
    _current:false,
    // parser.
    states:["Start", "GetSequenceNumber", "GetMessageSize1", "GetMessageSize2", "GetToken", "GetData", "GetChecksum", "Done"],
    state:0,
    pkt:false,
    // public interface,
    send:function(body,cb){
      if(this.closed) return setImmediate(function(){
        var e = new Error('this parser is closed.');
        e.code = "E_CLOSED";
        cb(e);
      });

      if(!Buffer.isBuffer(body)) body = new Buffer(body);

      var timeout = this._commandTimeout(body[0]);

      var messageLen = new Buffer([0,0]);
      messageLen.writeUInt16BE(body.length);    

      //MESSAGE_START,SEQUENCE_NUMBER,MESSAGE_SIZE,TOKEN,MESSAGE_BODY,CMD_READ/PROGRAM_FLASH/EEPROM,CHECKSUM
      var out = Buffer.concat(new Buffer([c.MESSAGE_START,this._seq(),messageLen[0],messageLen[1],c.TOKEN]),body);
      
      var checksum = this.checksum(out);
      this.queue.push({buf:Buffer.concat(out,new Buffer([checksum])),seq:this._inc,cb:cb,timeout:timeout});

      // if not waiting for another command to return. send this command
      this._send();
    },
    checksum:function(buf){
      var checksum = 0;
      for(var i=0;i<buf.length;++i){
        checksum ^= buf[i]; 
      }
      return checksum;
    },
    _seq:function(){
      this._inc++;
      if(this._inc > 0xff) this._inc = 0;
      return this._inc;
    },
    _commandTimeout:function(typeByte){
      //The total timeout period is from a command is sent to the answer must be completely
      //received. The total timeout period is 200 ms for the CMD_SIGN_ON command, 5
      //seconds for the CMD_READ/PROGRAM_FLASH/EEPROM commands, and 1
      //second for all other commands.
      timeout = 1000;
      if(body[0] === c.CMD_SIGN_ON) timeout = 200;
      else {
        // grab the constant names.
        var keys = Object.keys(c);
        for(var i=0;i<keys.length;++i){
          if(c[keys[i]] === typeByte) {
            if(keys[i].indexOf('CMD_READ') > -1 || keys[i].indexOf('PROGRAM_FLASH') > -1 || keys[i].indexOf('EEPROM') > -1) {
              timeout = 5000;
            }
            break;
          }
        }
      }
      return timeout;
    },
    _send:function(){
      if(this.closed) return false;
      if(this._current) return;
      if(!this._queue.length) return;   

      var message = this._queue.shift();
      var current = this._current = {
        timeout:false,
        seq:message.seq,
        cb:message.cb
      };

      this._current
      this.state = 0;  
      var z = this;

      this.port.write(buf);
      this.port.drain(function(){
        if(current !== z._current) return z.emit('log',"current was no longer the current message after drain callback"); 
        current.timeout = setTimeout(function(){
          var err = new Error("stk500 timeout. "+message.timeout+"ms")
          err.code = "E_TIMEOUT";
          z._resolveCurrent(err);
        },message.timeout);
      });
      this.emit('rawinput',buf);
    },
    _handle:function(data){
      var current = this._current;
      this.emit('raw',data);
      if(!current) return this.emit('log','notice',"dropping data",'data');
      // put state machine here. proove this works foolio.
      
      for(var i=0;i<data.length;++i) {
        this._stateMachine(data.readUInt8(i));
      }
    },
    _pkt:function(){
      return {
        seq:-1,
        len:[],
        message:[],
        checksum:0,
      }
    },
    _stateMachine:function(curByte){
      
      switch(state) {
      case 0:
        // always reset packet.
        this.pkt = this._pkt();
        
        if (curByte !== 0x1b) {
          // the spec says "update statistics".
          // the avrdude source just logs this out and does not treat it as a hard failure
          // https://github.com/arduino/avrdude/blob/master/stk500v2.c#L399
          return this.emit('log','parser',"Invalid header byte expected 27 got: " + curByte);
        }
        ++this.state;
        break;
      case 1:
        if (curByte !== this._current.seq) {
          this.state = 0;
          return this.emit('log','parser',"Invalid sequence number. back to start. got: " + curByte);
        }
        this.pkt.seq = curByte;
        ++this.state;
        break;
      case 2:
        pkt.len.push(curByte);
        ++state;
        break;
      case 3:
        pkt.len.push(curByte);
        pkt.len = (pkt.messageLen[0] << 8) | pkt.messageLen[1];
        ++state;
        break;
      case 4:
        if (curByte !== 0x0e) {
          this.state = 0;
          this.pkt.error = new Error("Invalid message token byte. got: " + curByte);
          this.pkt.error.code = "E_PARSE";
          return this.emit('log','parser',this.pkt.error);
        }
        ++state;
        // can stk500 send empty messages? probably not. avrdude doesnt support it.
        if(!pkt.len) ++state;
        break;
      case 5:
        if(pkt.len === 0 && curByte === c.STATUS_CKSUM_ERROR){
          // the message was corrupted in transit or some such error.
          // i could retry send the message for these errors!
          // i dont buffer the message right now TODO
          this.pkt.error = new Error("send checksum error");
          this.pkt.error.code = "E_STATUS_CKSUM";

          // TODO check to see if the first byte of all messages is a has errored byte.
          // this checksum error is the only error checked for in avrdude source
        }

        pkt.message.push(curByte);
        if (--pkt.len == 0) ++state;
        break;
      case 6:
        pkt.checksum = this.checksum(this.pkt.message);
        pkt.checksum = (pkt.checksum === curByte) ? true : false;
        if(!pkt.checksum){
          pkt.error = new Error("recv cecksum didn't match");
          pkt.error.code = "E_RECV_CKSUM";
        }

        pkt.message = new Buffer(pkt.message);
        this.emit('data',pkt);
        this.state++;// sets state to 7. the parser is not interested in any other bytes until a message is queued.
        this._resolveCurrent(s.pkt.error?this.pkt.error:false,pkt);
        break;
      }
    },
    _resolveCurrent:function(err,pkt){
      var toCall = this._current;
      this._current = false;
      // write the next pending command.
      // on timeout i error out the whole pending stack if any command errors.
      // im fairly certain that this is the only responsible thing to do in the case of any parse error.

      var q = false;
      if(err.code == "E_PARSE" || err.code == "E_TIMEOUT" || this.closed){
        q = this.queue;
        this.queue = [];
      }

      toCall.cb(err,pkt);
      if(q){
        var e = err;
        if(!this.closed) {
          e = new Error("a call queued before this call errored leaving the protocol in an upredictable state. timidly refusing to run queued commands.");
          e.code = "E_DEPENDENT";
          e.prev_code = err.code;
        } 
        while(q.lenth) q.shift()(err);
      }

      this._send();
    }
  });
 
  serialPort.on('data',dataHandler).on('error',cleanup).on('close',cleanup);

  return o;

  function dataHandler(data){
    o._handle(data);
  };

  function cleanup(err){
    // prevent new commands from writing to serial.
    o.closed = true;
    // stop sending data
    serialPort.removeListener('data',dataHandler);
    serialPort.removeListener('error',cleanup);
    serialPort.removeListener('close',cleanup);

    if(!err) {
      err = new Error('serial closed.');
      err.code = "E_CLOSED";
    }

    if(o._current) o._resolveCurrent(err);
    this.emit('closed');
  }

}

function ext(o1,o2){
  Object.keys(o2).forEach(function(k){
    o1[k] = o2[k];
  })
  return o1;
}