bossa-web
Version:
Port of BOSSA to TypeScript with support for WebSerial API
403 lines (402 loc) • 15.4 kB
JavaScript
/// <reference types="w3c-web-serial" />
import { sleep, Uint8Buffer, toByteArray } from './util';
// Timeouts
const DEFAULT_TIMEOUT = 3000; // timeout for most flash operations
const CHIP_ERASE_TIMEOUT = 300000; // timeout for full chip erase
const MAX_TIMEOUT = CHIP_ERASE_TIMEOUT * 2; // longest any command can run
const SYNC_TIMEOUT = 100; // timeout for syncing with bootloader
const ERASE_REGION_TIMEOUT_PER_MB = 30000; // timeout (per megabyte) for erasing a region
const MEM_END_ROM_TIMEOUT = 50;
const TIMEOUT_QUICK = 100;
const TIMEOUT_NORMAL = 1000;
const TIMEOUT_LONG = 5000;
export class SamBAError extends Error {
constructor(msg) {
super(msg);
}
}
const emptyByteArray = new Uint8Array();
export class SamBA {
constructor(serialPort, options) {
// readLoop state
this.closed = true;
this.readLoopPromise = undefined;
this.serialReader = undefined;
this.inputBuffer = new Uint8Buffer(64);
this._canChipErase = false;
this._canWriteBuffer = false;
this._canChecksumBuffer = false;
this._canProtect = false;
this._sendCommandBuffer = new Uint8Buffer();
this.options = Object.assign({
flashSize: 4 * 1024 * 1024,
logger: console,
debug: false,
trace: false
}, options || {});
this.serialPort = serialPort;
this._canChipErase = false;
this._canWriteBuffer = false;
this._canChecksumBuffer = false;
this._canProtect = false;
this._readBufferSize = 0;
}
get canChecksumBuffer() {
return this._canChecksumBuffer;
}
get canProtect() {
return this._canProtect;
}
get canChipErase() {
return this._canChipErase;
}
get canWriteBuffer() {
return this._canWriteBuffer;
}
get writeBufferSize() { return 4096; }
get logger() {
return this.options.logger;
}
async checksumBuffer(start_addr, size) {
if (!this._canWriteBuffer)
throw new SamBAError('Cannot write buffer');
if (size > this.checksumBufferSize())
throw new SamBAError('Size too large for checksum buffer');
if (this.options.debug)
this.options.logger.debug('checksumBuffer(start_addr=0x', this.hex(start_addr), ',size=0x', this.hex(size), ')');
let result = await this.sendCommand('Z' + this.hex(start_addr) + ',' + this.hex(size), 12, undefined, 0, TIMEOUT_LONG);
if (!result || result[0] != 0x5A /* 'Z' */) // Expects "Z00000000#\n\r"
throw new SamBAError('Board response for \'Z\' command wrong');
let value = this.decodeResponse(result);
value = value.substr(1, 8);
let num = parseInt(value, 16);
if (num == NaN) {
throw new SamBAError('Invalid checksum returned');
}
return num;
}
checksumBufferSize() { return 4096; }
checksumCalc(c, crc) {
return -1;
}
async chipErase(start_addr) {
if (!this._canChipErase)
throw new SamBAError('Chip erase not supported');
if (this.options.debug)
this.options.logger.debug('chipErase(start_addr=0x', this.hex(start_addr), ')');
let result = await this.sendCommand('X' + this.hex(start_addr), 3, undefined, 0, TIMEOUT_LONG);
if (!result || result[0] != 0x58 /* 'X' */)
throw new SamBAError('Board response for \'X\' command wrong');
}
async writeBuffer(src_addr, dst_addr, size) {
if (!this._canWriteBuffer)
throw new SamBAError('Cannot write buffer');
if (size > this.checksumBufferSize())
throw new SamBAError('Size too large for checksum buffer');
if (this.options.debug)
this.options.logger.debug('writeBuffer(src_addr=0x', this.hex(src_addr), ',dst_addr=0x', this.hex(dst_addr), ',size=0x', this.hex(size), ')');
let result = await this.sendCommand('Y' + this.hex(src_addr) + ',0', 3, undefined, 0, TIMEOUT_QUICK);
if (!result || result[0] != 0x59 /* 'Y' */)
throw new SamBAError('Board response for \'Y\' command wrong');
// await sleep(50);
result = await this.sendCommand('Y' + this.hex(dst_addr) + ',' + this.hex(size), 3, undefined, 0, TIMEOUT_LONG * 2);
await sleep(50);
if (!result || result[0] != 0x59 /* 'Y' */)
throw new SamBAError('Board response for \'Y\' command wrong');
}
/**
* Send a byte stream to the device
*/
async writeToStream(msg) {
if (this.serialPort.writable) {
const writer = this.serialPort.writable.getWriter();
let chunkSize = 63;
try {
await sleep(50);
await writer.write(msg);
}
finally {
writer.releaseLock();
}
}
}
hex(value, digits = 8) {
var result = value.toString(16);
while (result.length < digits) {
result = '0' + result;
}
return result;
}
writeByte(addr, value) {
if (this.options.debug)
this.options.logger.debug('writeByte(addr=0x', this.hex(addr), ',value=0x', this.hex(value, 2), ')');
this.sendCommand('O' + this.hex(addr) + ',' + this.hex(value, 2), 0);
}
async readByte(addr) {
if (this.options.debug)
this.options.logger.debug('readByte(addr=0x', this.hex(addr), ')');
let result = await this.sendCommand('o' + this.hex(addr) + ",4", 1);
if (result) {
let value = result[0];
if (this.options.debug)
this.options.logger.debug('readByte(addr=0x', this.hex(addr), ')=0x', this.hex(value, 2));
return value;
}
throw new SamBAError('Reading');
}
async writeWord(addr, value) {
if (this.options.debug)
this.options.logger.debug('writeWord(addr=0x', this.hex(addr), ',value=0x', this.hex(value), ')');
await this.sendCommand('W' + this.hex(addr) + ',' + this.hex(value, 8), 0);
}
async readWord(addr) {
if (this.options.debug)
this.options.logger.debug('readWord(addr=0x', this.hex(addr), ')');
let result = await this.sendCommand('w' + this.hex(addr) + ',4', 4);
if (result) {
let value = (result[3] << 24 | result[2] << 16 | result[1] << 8 | result[0] << 0);
if (this.options.debug)
this.options.logger.debug('readByte(addr=0x', this.hex(addr), ')=0x', this.hex(value, 8));
return value;
}
throw new SamBAError('Reading');
}
async write(addr, buffer, size = buffer.length) {
if (this.options.debug)
this.options.logger.debug('write(addr=0x', this.hex(addr), ',size=0x', this.hex(size), ')');
await this.sendCommand('S' + this.hex(addr) + ',' + this.hex(size, 8), 0, buffer, size, TIMEOUT_LONG);
}
async read(addr, buffer, size) {
if (this.options.debug)
this.options.logger.debug('read(addr=0x', this.hex(addr), ',size=0x', this.hex(size), ')');
var start = 0;
// The SAM firmware has a bug reading powers of 2 over 32 bytes
// via USB. If that is the case here, then read the first byte
// with a readByte and then read one less than the requested size.
if (this._readBufferSize == 0 && size > 32 && !(size & (size - 1))) {
buffer[start] = await this.readByte(addr);
addr++;
start++;
size--;
}
while (size > 0) {
var chunk = size;
// Handle any limitations on the size of the read
if (this._readBufferSize > 0 && size > this._readBufferSize)
chunk = this._readBufferSize;
var result = await this.sendCommand('R' + this.hex(addr) + ',' + this.hex(chunk), chunk);
if (result) {
for (var i = 0; i < chunk; i++) {
buffer[start++] = result[i];
}
}
else
throw new SamBAError('Reading binary');
size -= chunk;
addr += chunk;
start += chunk;
}
}
async go(addr) {
if (this.options.debug)
this.options.logger.debug('go(addr=0x', this.hex(addr), ')');
await this.sendCommand('G' + this.hex(addr), 0);
}
/**
* @param rebootWaitMs how long it may take to reboot
* Start the read loop up.
*/
async connect(rebootWaitMs = 1000) {
if (this.readLoopPromise) {
throw "already open";
}
await this.serialPort.open({
dataBits: 8,
stopBits: 1,
parity: 'none',
bufferSize: 63,
flowControl: 'hardware',
baudRate: 921600
});
await sleep(50);
await this._connect();
}
async _connect() {
this.closed = false;
this.readLoopPromise = (async () => {
this.readLoop()
.catch((reason) => {
if (reason.name == 'NetworkError' && reason.code == 19) {
console.log("readLoop terminated because the connection was closed.");
}
});
this.readLoopPromise = undefined;
})();
// Clear the pipe
await this.readBuffer(SYNC_TIMEOUT);
await this.setBinaryMode();
let version = await this.readVersion();
var extIndex = version.indexOf('[Arduino:');
if (this.options.debug)
this.options.logger.debug('Version-Info from bootloader: ' + version);
if (extIndex != -1) {
extIndex += 9;
while (extIndex < version.length && version[extIndex] != ']') {
switch (version[extIndex]) {
case 'X':
this._canChipErase = true;
break;
case 'Y':
this._canWriteBuffer = true;
break;
case 'Z':
this._canChecksumBuffer = true;
break;
case 'P':
this._canProtect = true;
break;
}
extIndex++;
}
// All SAMD-based Arduino/AdaFruit boards have a bug in their bootloader
// that trying to read 64 bytes or more over USB corrupts the data.
// We must limit these boards to read chunks of 63 bytes.
this._readBufferSize = 63;
}
}
async setBinaryMode() {
return this.sendCommand('N', 2);
}
/**
* Read the Arduino version information from the board
*
* @returns A promise providing the version string
*/
async readVersion() {
let buffer = await this.sendCommand('V', 256);
if (buffer) {
return this.decodeResponse(buffer);
}
throw new SamBAError('No data received');
}
decodeResponse(buffer) {
if (buffer.length > 2) {
// Strip CR/LF if found
if ((buffer[buffer.length - 1] = 0x0c) && (buffer[buffer.length - 2] = 0x0a)) {
buffer = buffer.subarray(0, buffer.length - 2);
}
}
return new TextDecoder("ascii").decode(buffer);
}
async sendCommand(cmd, responseSize = 2, data = undefined, size = 0, timeout = DEFAULT_TIMEOUT) {
this.inputBuffer.reset();
const packet = this._sendCommandBuffer;
packet.reset();
packet.copy(toByteArray(cmd));
packet.push(0x23); // #
const res = packet.view();
if (this.options.trace) {
this.logger.debug("Writing ", this.hex(res.length), " byte" + (res.length == 1 ? "" : "s") + ":", res.slice(0, packet.length));
}
await this.writeToStream(res);
if (data) {
// if (this.options.debug) {
// this.logger.debug("writing buffer", this.hex(data.length), " byte" + (res.length == 1 ? "" : "s"));
// }
await sleep(50);
await this.writeToStream(data);
}
// if (this.options.debug) {
// this.logger.debug("done writing");
// }
if (responseSize > 0)
return await this.readBuffer(timeout, responseSize);
else
return null;
}
/**
* Change the baud rate for the serial port.
*/
async setBaudRate(baud) {
this.logger.log("Attempting to change baud rate to", baud, "...");
// Close the read loop and port
await this.disconnect();
await this.serialPort.close();
// Reopen the port and read loop
await this.serialPort.open({ baudRate: baud });
await sleep(50);
this._connect();
// Baud rate was changed
this.logger.log("Changed baud rate to", baud);
}
/**
* Shutdown the read loop.
*/
async disconnect() {
const p = this.readLoopPromise;
const reader = this.serialReader;
if (!p || !reader) {
throw "not open";
}
this.closed = true;
await reader.cancel();
await p;
return;
}
async readBuffer(timeout = DEFAULT_TIMEOUT, responseSize = undefined) {
let reply = [];
const stamp = Date.now();
while (Date.now() - stamp < timeout) {
if (this.inputBuffer.length > 0) {
const c = this.inputBuffer.shift() || 0;
if (this.options.debug) {
}
reply.push(c);
}
else {
await sleep(10);
}
if (reply.length > 1 && (reply[reply.length - 1] == 0x0)) {
break;
}
if (responseSize && reply.length == responseSize) {
break;
}
}
// Check to see if we have a complete packet. If not, we timed out.
if (reply.length == 0) {
this.logger.log("Timed out after", timeout, "milliseconds");
return null;
}
if (this.options.trace) {
this.logger.debug("Reading", reply.length, "byte" + (reply.length == 1 ? "" : "s") + ":", reply);
}
return Uint8Array.from(reply);
}
async readLoop() {
this.inputBuffer.reset();
if (this.serialPort.readable) {
const appReadable = this.serialPort.readable;
this.serialReader = appReadable.getReader();
try {
while (!this.closed) {
const { value, done } = await this.serialReader.read();
if (done) {
break;
}
if (value) {
if (this.options.trace)
this.logger.debug("Received " + value);
this.inputBuffer.copy(value);
}
}
}
finally {
await this.serialReader.cancel();
this.serialReader.releaseLock();
this.serialReader = undefined;
this.closed = true;
}
}
}
}