stk500-v2
Version:
STK500 v2 programmer in javascript
282 lines (241 loc) • 8.4 kB
JavaScript
var c = require('./constants-v2');
var EventEmitter = require("events").EventEmitter;
module.exports = function(serialPort){
var o = ext(
new EventEmitter,
{
constants:c,
port:serialPort,
boundOpen:false,
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 = Buffer.from(body);
var timeout = this._commandTimeout(body[0]);
var messageLen = Buffer.from([0,0]);
messageLen.writeUInt16BE(body.length,0);
//MESSAGE_START,SEQUENCE_NUMBER,MESSAGE_SIZE,TOKEN,MESSAGE_BODY,CMD_READ/PROGRAM_FLASH/EEPROM,CHECKSUM
var out = Buffer.concat([Buffer.from([c.MESSAGE_START,this._seq(),messageLen[0],messageLen[1],c.TOKEN]),body]);
var checksum = this.checksum(out);
this._queue.push({buf:Buffer.concat([out,Buffer.from([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(typeByte === 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 portIsOpen = typeof serialPort.isOpen === 'function' ? serialPort.isOpen() : serialPort.isOpen;
// if the serialport is not open yet. Check on node && chrome.
if(!portIsOpen){
var z = this;
if(!this.boundOpen) serialPort.once('open',function(){
z._send();
});
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(message.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',message.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:[],
raw:[],
message:[],
checksum:0,
}
},
_stateMachine:function(curByte){
var pkt = this.pkt;
switch(this.state) {
case 0:
// always reset packet.
pkt = 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);
}
pkt.seq = curByte;
++this.state;
break;
case 2:
pkt.len.push(curByte);
++this.state;
break;
case 3:
pkt.len.push(curByte);
pkt.len = (pkt.len[0] << 8) | pkt.len[1];
++this.state;
break;
case 4:
if (curByte !== 0x0e) {
this.state = 0;
pkt.error = new Error("Invalid message token byte. got: " + curByte);
pkt.error.code = "E_PARSE";
return this.emit('log','parser',this.pkt.error);
}
++this.state;
// can stk500 send empty messages? probably not. avrdude doesnt support it.
if(!pkt.len) ++this.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
pkt.error = new Error("send checksum error");
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) ++this.state;
break;
case 6:
pkt.checksum = this.checksum(pkt.raw);
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 = Buffer.from(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.
pkt.len = pkt.message.length;
delete pkt.raw;
this._resolveCurrent(pkt.error?pkt.error:false,pkt);
break;
}
if(pkt.raw) pkt.raw.push(curByte);
},
_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 = [];
}
clearTimeout(toCall.timeout);
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).once('error',cleanup).once('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);
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;
}