Commit b3eb7941 authored by Ryan Day's avatar Ryan Day

Merge pull request #4 from Pinoccio/refactor

Refactor
parents fbc39a83 6112ebd4
......@@ -11,5 +11,6 @@ pids
logs
results
old
npm-debug.log
node_modules
js-stk500
stk500
=========
[STK500](http://www.atmel.com/tools/stk500.aspx) javascript implementation for node and browserify
It's not there yet, but the goals are:
Fully javascript stk500v2 and stk500v1 programmer. Allows you to program Arduinos straight from node (or browser for that matter). No more avrdude system calls or using the arduino IDE.
* Use browserify so it can be put into NPM and can be used either via node or browser
* Get it cleaned up and working for any chip, not just the 256RFR2.
* Merge Serial.js and Serial2.js, (Serial.js was for Chrome version <= 33, Serial2.js > 33)
* Ensure both STK500 and STK500v2 work (right now it's only tested with STK500v2)
###INSTALL
```
npm install stk500
```
####Program:
* this may not be true. anymore update before 1.0.0 release *
You need an *unconnected* instance of (my fork of) Chris Williams's Node Serial Port at the correct speed for your chip (commonly 115200) with a raw parser.
```
var serialPort = new SerialPort.SerialPort(port.comName, {
baudrate: 115200,
parser: SerialPort.parsers.raw
}, false);
```
Then you can instantiate a programmer.
```
var stk500 = require('stk500');
var programmerv2 = stk500(serialPort);
// or for v1
var programmerv1 = stk500.v1(serialPort);
```
Beyond that you can send stk500 commands. For programming the process is a fairly strict series of async series including connect, reset, sync, setOptions (pagesize is the only necessary option), enterprogrammingmode, program, exitprogrammingmode, disconnect. See uno.js in examples.
###How to get a hex
You can compile by hand yourself with avrdude if you know your stuff, or you can just steal one from Arduino. First make sure you have verbosity enabled in your Arduino preferences: Arduino Preferences -> check Show verbose output during Compilation. Now when you build you'll see a ton of lines on screen. The last couple lines have what you need:
```
/var/folders/zp/bpw8zd0141j5zf7l8m_qtt8w0000gp/T/build6252696906929781517.tmp/Blink.cpp.hex
Sketch uses 896 bytes (2%) of program storage space. Maximum is 32,256 bytes.
Global variables use 9 bytes (0%) of dynamic memory, leaving 2,039 bytes for local variables. Maximum is 2,048 bytes.
```
Grab that hex file and you're good to go.
###Gotchas
* Only works on MacOSX (and probably linux). Requires on Chris Williams excellent nodeserial implementation https://github.com/voodootikigod/node-serialport. However nodeserial doesn't currently support manual rts/dtr signaling so I have a fork with unix bindings https://github.com/jacobrosenthal/node-serialport/tree/controlsignals
* Since I'm forking nodeserial and not hosting a new version yet I've got a postinstall step that tries to run ./postinstall to kick off a fresh build.
* intel-hex and fs are dependancies only for the example file
###CHANGELOG
0.0.1
first
0.0.2
Added loading from fs to example, some example hexes from arduino 1.0.6 for Uno, and instructions on how to find a hex file to load.
0.0.3
Bugs squashed leading to much more stable getsync and less attempts necessary to successfuly programmin. Slight refactor in example and clearer console.log messaging.
0.0.4
Slight require change for browserfy-ability and a few more touchups in example
0.0.5
Fixed instability issue especially in chrome where listeners were not being deregistered
0.0.6
Added ability to verify device signature.
This diff is collapsed.
var stk500 = require('../');
var serialport = require('serialport');
var intel_hex = require('intel-hex');
var fs = require('fs');
var data = fs.readFileSync(__dirname+'/../../panda-attack/bootstrap.hex')+'';
var hex = intel_hex.parse(data).data;
var pageSize = 256;
var baud = 115200;
var delay1 = 10;
var delay2 = 1;
var signature = new Buffer([0x1e, 0xa8, 0x02]);
var options = {
timeout:0xc8,
stabDelay:0x64,
cmdexeDelay:0x19,
synchLoops:0x20,
byteDelay:0x00,
pollValue:0x53,
pollIndex:0x03
};
var comName = '/dev/ttyACM0';
var serialPort = new serialport.SerialPort(comName, {
baudrate: baud,
parser: serialport.parsers.raw
}, function(){
console.log('opened!',serialPort.opened);
console.log(serialPort);
});
var programmer = stk500(serialPort);
// debug
programmer.parser.on('rawinput',function(buf){
console.log("->",buf.toString('hex'));
})
programmer.parser.on('raw',function(buf){
console.log("<-",buf.toString('hex'));
})
// do it!
programmer.sync(5,function(err,data){
console.log('callback sync',err," ",data)
});
programmer.verifySignature(signature,function(err,data){
console.log('callback sig',err," ",data);
});
programmer.enterProgrammingMode(options,function(err,data){
console.log('enter programming mode.',err,data);
});
programmer.upload( hex, pageSize,function(err,data){
console.log('upload> ',err,data);
programmer.exitProgrammingMode(function(err,data){
console.log('exitProgrammingMode> ',err,data)
})
});
var SerialPort = require("serialport");
var intel_hex = require('intel-hex');
// require version 1 of stk500
var stk500 = require('../').v1;
var async = require("async");
var fs = require('fs');
var usbttyRE = /(cu\.usb|ttyACM|COM\d+)/;
var data = fs.readFileSync('arduino-1.0.6/uno/Blink.cpp.hex', { encoding: 'utf8' });
var hex = intel_hex.parse(data).data;
//TODO standardize chip configs
//uno
var pageSize = 128;
var baud = 115200;
var delay1 = 1; //minimum is 2.5us, so anything over 1 fine?
var delay2 = 1;
var signature = new Buffer([0x1e, 0x95, 0x0f]);
var options = {
pagesizelow:pageSize
};
SerialPort.list(function (err, ports) {
ports.forEach(function(port) {
console.log("found " + port.comName);
if(usbttyRE.test(port.comName))
{
console.log("trying" + port.comName);
var serialPort = new SerialPort.SerialPort(port.comName, {
baudrate: baud,
parser: SerialPort.parsers.raw
}, false);
var programmer = new stk500(serialPort);
async.series([
programmer.connect.bind(programmer),
programmer.reset.bind(programmer,delay1, delay2),
programmer.sync.bind(programmer, 3),
programmer.verifySignature.bind(programmer, signature),
programmer.setOptions.bind(programmer, options),
programmer.enterProgrammingMode.bind(programmer),
programmer.upload.bind(programmer, hex, pageSize),
programmer.exitProgrammingMode.bind(programmer)
], function(error){
programmer.disconnect();
if(error){
console.log("programing FAILED: " + error);
process.exit(1);
}else{
console.log("programing SUCCESS!");
process.exit(0);
}
});
}else{
console.log("skipping " + port.comName);
}
});
});
//default to v2
module.exports = require('./stk500v2.js');
module.exports.v2 = module.exports;
module.exports.v1 = require('./stk500v1.js');
parsers export should be a function that accepts an open serialport as the first argument. and option options as the second.
the parser object should be an event emitter
if should expose a public send method that takes data,and callback arguments.
an optional middle options arguments may be necessary
all other protocol command implementation should be done in another file.
// STK message constants
module.exports.MESSAGE_START = 0x1B
module.exports.TOKEN = 0x0E
// STK general command constants
module.exports.CMD_SIGN_ON = 0x01
module.exports.CMD_SET_PARAMETER = 0x02
module.exports.CMD_GET_PARAMETER = 0x03
module.exports.CMD_SET_DEVICE_PARAMETERS = 0x04
module.exports.CMD_OSCCAL = 0x05
module.exports.CMD_LOAD_ADDRESS = 0x06
module.exports.CMD_FIRMWARE_UPGRADE = 0x07
// STK ISP command constants
module.exports.CMD_ENTER_PROGMODE_ISP = 0x10
module.exports.CMD_LEAVE_PROGMODE_ISP = 0x11
module.exports.CMD_CHIP_ERASE_ISP = 0x12
module.exports.CMD_PROGRAM_FLASH_ISP = 0x13
module.exports.CMD_READ_FLASH_ISP = 0x14
module.exports.CMD_PROGRAM_EEPROM_ISP = 0x15
module.exports.CMD_READ_EEPROM_ISP = 0x16
module.exports.CMD_PROGRAM_FUSE_ISP = 0x17
module.exports.CMD_READ_FUSE_ISP = 0x18
module.exports.CMD_PROGRAM_LOCK_ISP = 0x19
module.exports.CMD_READ_LOCK_ISP = 0x1A
module.exports.CMD_READ_SIGNATURE_ISP = 0x1B
module.exports.CMD_READ_OSCCAL_ISP = 0x1C
module.exports.CMD_SPI_MULTI = 0x1D
// STK PP command constants
module.exports.CMD_ENTER_PROGMODE_PP = 0x20
module.exports.CMD_LEAVE_PROGMODE_PP = 0x21
module.exports.CMD_CHIP_ERASE_PP = 0x22
module.exports.CMD_PROGRAM_FLASH_PP = 0x23
module.exports.CMD_READ_FLASH_PP = 0x24
module.exports.CMD_PROGRAM_EEPROM_PP = 0x25
module.exports.CMD_READ_EEPROM_PP = 0x26
module.exports.CMD_PROGRAM_FUSE_PP = 0x27
module.exports.CMD_READ_FUSE_PP = 0x28
module.exports.CMD_PROGRAM_LOCK_PP = 0x29
module.exports.CMD_READ_LOCK_PP = 0x2A
module.exports.CMD_READ_SIGNATURE_PP = 0x2B
module.exports.CMD_READ_OSCCAL_PP = 0x2C
module.exports.CMD_SET_CONTROL_STACK = 0x2D
// STK HVSP command constants
module.exports.CMD_ENTER_PROGMODE_HVSP = 0x30
module.exports.CMD_LEAVE_PROGMODE_HVSP = 0x31
module.exports.CMD_CHIP_ERASE_HVSP = 0x32
module.exports.CMD_PROGRAM_FLASH_HVSP = 0x33
module.exports.CMD_READ_FLASH_HVSP = 0x34
module.exports.CMD_PROGRAM_EEPROM_HVSP = 0x35
module.exports.CMD_READ_EEPROM_HVSP = 0x36
module.exports.CMD_PROGRAM_FUSE_HVSP = 0x37
module.exports.CMD_READ_FUSE_HVSP = 0x38
module.exports.CMD_PROGRAM_LOCK_HVSP = 0x39
module.exports.CMD_READ_LOCK_HVSP = 0x3A
module.exports.CMD_READ_SIGNATURE_HVSP = 0x3B
module.exports.CMD_READ_OSCCAL_HVSP = 0x3C
// STK status constants
// Success
module.exports.STATUS_CMD_OK = 0x00
// Warnings
module.exports.STATUS_CMD_TOUT = 0x80
module.exports.STATUS_RDY_BSY_TOUT = 0x81
module.exports.STATUS_SET_PARAM_MISSING = 0x82
// Errors
module.exports.STATUS_CMD_FAILED = 0xC0
module.exports.STATUS_CKSUM_ERROR = 0xC1
module.exports.STATUS_CMD_UNKNOWN = 0xC9
// STK parameter constants
module.exports.STATUS_BUILD_NUMBER_LOW = 0x80
module.exports.STATUS_BUILD_NUMBER_HIGH = 0x81
module.exports.STATUS_HW_VER = 0x90
module.exports.STATUS_SW_MAJOR = 0x91
module.exports.STATUS_SW_MINOR = 0x92
module.exports.STATUS_VTARGET = 0x94
module.exports.STATUS_VADJUST = 0x95
module.exports.STATUS_OSC_PSCALE = 0x96
module.exports.STATUS_OSC_CMATCH = 0x97
module.exports.STATUS_SCK_DURATION = 0x98
module.exports.STATUS_TOPCARD_DETECT = 0x9A
module.exports.STATUS_STATUS = 0x9C
module.exports.STATUS_DATA = 0x9D
module.exports.STATUS_RESET_POLARITY = 0x9E
module.exports.STATUS_CONTROLLER_INIT = 0x9F
// STK answer constants
module.exports.ANSWER_CKSUM_ERROR = 0xB0
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 = new Buffer(body);
var timeout = this._commandTimeout(body[0]);
var messageLen = new Buffer([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([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(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;
// if the serialport is not open yet.
if(!serialPort.fd){
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 = 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.
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;
}
var DEBUG_MODE = false;
var timeout = 100;
var clientSock;
var myLog = Function.prototype.bind.call(console.log, console);
function debugLog() {
if (DEBUG_MODE) {
var args = Array.prototype.slice.call(arguments, 0);
myLog.apply(console, args);
}
}
chrome.app.runtime.onRestarted.addListener(function(data) {
console.log("We restarted");
});
chrome.runtime.onStartup.addListener(function(details) {
console.log("onStartup ", details);
});
chrome.runtime.onInstalled.addListener(function(details) {
console.log("Installed", details);
});
chrome.runtime.onSuspend.addListener(function() {
});
chrome.runtime.onMessageExternal.addListener(function(msg, sender, responder) {
if (chrome.serial.getDevices) {
PinoccioSerial = PinoccioSerial2;
}
/*
var device = new pinoccio.Device(port);
device.connect(portName, function() {
device.signOn(function() {
console.log("DONE READ");
return;
});
});
*/
var cmds = {
getManifest:function(){
responder(chrome.runtime.getManifest());
},
waitForUnplug:function() {
if (msg.cancel === true) {
pinoccio.cancelUSBPluggedIn();
return responder({msg:"Canceled the usb check."});
}
if (!msg.interval) {
return responder({error:"An interval must be specified"});
}
pinoccio.checkUSBPluggedIn(msg.interval, function() {
responder({unplugged:true, msg:"The device was unplugged"});
});
},
fetchAndProgramPort:function() {
var req = new XMLHttpRequest();
req.onload = function() {
console.log("Loaded ", typeof this.responseText);
var programHex = this.responseText;
var device = new pinoccio.Device();
device.connect(msg.port, function(err) {
if (err) {
responder({error:err});
return;
}
device.saveProgram(programHex.trim(), function(err) {
var resp = {};
if (err) resp.error = err;
responder(resp);
});
});
}
req.onError = function() {
console.error("ZOMG ", arguments);
responder({error:"Unable to fetch the program contents."});
};
console.log("Fetching ", msg.url);
req.open("get", msg.url, true);
req.send();
},
fetchAndProgram:function() {
var req = new XMLHttpRequest();
req.onload = function() {
console.log("Loaded ", typeof this.responseText);
var programHex = this.responseText;
pinoccio.findSerial(function(err, device) {
if (err) {
console.error(err);
return responder({error:err});
}
device.saveProgram(programHex.trim(), function(err) {
var resp = {};
if (err) resp.error = err;
responder(resp);
});
});
};
req.onError = function() {
console.error("ZOMG ", arguments);
responder({error:"Unable to fetch the program contents."});
};