|
@@ -1,437 +0,0 @@
|
|
-"use strict";
|
|
|
|
-
|
|
|
|
-const EventEmitter = require("events");
|
|
|
|
-const fs = require("fs");
|
|
|
|
-const SerialPort = require("serialport");
|
|
|
|
-const log = require("./logger");
|
|
|
|
-const slip = require("./streams/slip");
|
|
|
|
-const boards = require("./boards");
|
|
|
|
-const delay = require("./utilities").delay;
|
|
|
|
-const retryPromiseUntil = require("./utilities").retryPromiseUntil;
|
|
|
|
-const promiseChain = require("./utilities").promiseChain;
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-const commands = {
|
|
|
|
- CMD0: 0x00,
|
|
|
|
- CMD1: 0x01,
|
|
|
|
- FLASH_DOWNLOAD_BEGIN: 0x02,
|
|
|
|
- FLASH_DOWNLOAD_DATA: 0x03,
|
|
|
|
- FLASH_DOWNLOAD_DONE: 0x04,
|
|
|
|
- RAM_DOWNLOAD_BEGIN: 0x05,
|
|
|
|
- RAM_DOWNLOAD_END: 0x06,
|
|
|
|
- RAM_DOWNLOAD_DATA: 0x07,
|
|
|
|
- SYNC_FRAME: 0x08,
|
|
|
|
- WRITE_REGISTER: 0x09,
|
|
|
|
- READ_REGISTER: 0x0A,
|
|
|
|
- SET_FLASH_PARAMS: 0x0B,
|
|
|
|
- NO_COMMAND: 0xFF
|
|
|
|
-};
|
|
|
|
-
|
|
|
|
-const validateCommandSuccess = [
|
|
|
|
- commands.SYNC_FRAME,
|
|
|
|
- commands.FLASH_DOWNLOAD_BEGIN,
|
|
|
|
- commands.FLASH_DOWNLOAD_DATA,
|
|
|
|
- commands.FLASH_DOWNLOAD_DONE
|
|
|
|
-];
|
|
|
|
-
|
|
|
|
-function commandToKey(command) {
|
|
|
|
- // value to key
|
|
|
|
- return Object.keys(commands).find((key) => commands[key] === command);
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-const SYNC_FRAME = new Buffer([0x07, 0x07, 0x12, 0x20,
|
|
|
|
- 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
|
|
|
- 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
|
|
|
- 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
|
|
|
- 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55]);
|
|
|
|
-
|
|
|
|
-const FLASH_BLOCK_SIZE = 0x400;
|
|
|
|
-const SUCCESS = new Buffer([0x00, 0x00]);
|
|
|
|
-const REQUIRED_SUCCESSFUL_SYNC_COUNT = 10;
|
|
|
|
-
|
|
|
|
-/**
|
|
|
|
- * An abstraction of talking to the ever so finicky ESP8266 ROM.
|
|
|
|
- * Many thanks to the C library (https://github.com/igrr/esptool-ck)
|
|
|
|
- * and the Python version of the same regard (https://github.com/themadinventor/esptool/)
|
|
|
|
- * that helped suss out the weird cases.
|
|
|
|
- */
|
|
|
|
-class RomComm extends EventEmitter {
|
|
|
|
- constructor(config) {
|
|
|
|
- super();
|
|
|
|
- this._port = new SerialPort(config.portName, {
|
|
|
|
- autoOpen: false,
|
|
|
|
- baudRate: config.baudRate,
|
|
|
|
- parity: 'none',
|
|
|
|
- stopBits: 1,
|
|
|
|
- xon: false,
|
|
|
|
- xoff: false,
|
|
|
|
- rtscts: false,
|
|
|
|
- dsrdtr: false
|
|
|
|
- });
|
|
|
|
- this.bindPort();
|
|
|
|
- this.resetState();
|
|
|
|
- var boardName = config.boardName ? config.boardName : "Esp12";
|
|
|
|
- var BoardFactory = boards[boardName];
|
|
|
|
- if (BoardFactory === undefined) {
|
|
|
|
- throw new Error("Unkown board " + boardName);
|
|
|
|
- }
|
|
|
|
- this.board = new BoardFactory(this._port);
|
|
|
|
- this.config = config;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- resetState() {
|
|
|
|
- this.setState("Unknown", {});
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- bindPort() {
|
|
|
|
- this._port.on('error', (error) => log.error("PORT ERROR", error));
|
|
|
|
- this.in = new slip.SlipDecoder();
|
|
|
|
- this.out = new slip.SlipEncoder();
|
|
|
|
- this._port.pipe(this.in);
|
|
|
|
- this.out.pipe(this._port);
|
|
|
|
- this.in.on("data", (data) => this.handleResponse(data));
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- setState(status, details) {
|
|
|
|
- this.state = {
|
|
|
|
- display: status,
|
|
|
|
- details: details
|
|
|
|
- };
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * Response from the device are eventually sensical. Hold tight.
|
|
|
|
- */
|
|
|
|
- handleResponse(data) {
|
|
|
|
- // Data coming in here has been SLIP escaped
|
|
|
|
- if (data.length < 8) {
|
|
|
|
- log.error("Missing header");
|
|
|
|
- // Not throwing error, let it fall through
|
|
|
|
- return false;
|
|
|
|
- }
|
|
|
|
- let headerBytes = data.slice(0, 8);
|
|
|
|
- let header = this.headerPacketFrom(headerBytes);
|
|
|
|
- if (header.direction != 0x01) {
|
|
|
|
- log.error("Invaid direction", header.direction);
|
|
|
|
- // Again, intentionally not throwing error, it will communicate correctly eventually
|
|
|
|
- return false;
|
|
|
|
- }
|
|
|
|
- let commandName = commandToKey(header.command);
|
|
|
|
- let body = data.slice(8, 8 + header.size);
|
|
|
|
- // Most commands just return `SUCCESS` or 0x00 0x00
|
|
|
|
- if (header.command in validateCommandSuccess) {
|
|
|
|
- if (!body.equals(SUCCESS)) {
|
|
|
|
- log.error("%s returned %s", commandName, body);
|
|
|
|
- throw new Error("Invalid status from " + commandName + " was " + body);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- log.info("Emitting", commandName, body);
|
|
|
|
- this.emit("RECEIVED-" + commandName, body);
|
|
|
|
- return true;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * Opens the port and flips into bootloader
|
|
|
|
- */
|
|
|
|
- open() {
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
- this._port.open((error) => {
|
|
|
|
- log.info("Opening port...", this._port);
|
|
|
|
- if (error) {
|
|
|
|
- reject(error);
|
|
|
|
- } else {
|
|
|
|
- this.portIsOpen = true;
|
|
|
|
- resolve();
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
- }).then(() => this.connect());
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * Leaves bootloader mode and closes the port
|
|
|
|
- */
|
|
|
|
- close() {
|
|
|
|
- return this.flashAddress(0, 0)
|
|
|
|
- .then((result) => this.flashFinish(false))
|
|
|
|
- .then((result) => this._port.close((err) => {
|
|
|
|
- log.info("Closing port...");
|
|
|
|
- this.portIsOpen = false;
|
|
|
|
- }));
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- calculateChecksum(data) {
|
|
|
|
- // Magic Checksum starts with 0xEF
|
|
|
|
- var result = 0xEF;
|
|
|
|
- for (var i = 0; i < data.length; i++) {
|
|
|
|
- result ^= data[i];
|
|
|
|
- }
|
|
|
|
- return result;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * The process of syncing gets the software and hardware aligned.
|
|
|
|
- * Due to the whacky responses, you can't really wait for a proper response
|
|
|
|
- */
|
|
|
|
- sync(ignoreResponse) {
|
|
|
|
- log.info("Syncing");
|
|
|
|
- return this.sendCommand(commands.SYNC_FRAME, SYNC_FRAME, ignoreResponse)
|
|
|
|
- .then((response) => {
|
|
|
|
- if (!ignoreResponse) {
|
|
|
|
- log.info("Sync response completed!", response);
|
|
|
|
- this.board.isInBootLoader = true;
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- connect() {
|
|
|
|
- // Eventually responses calm down, but on initial contact, responses are not standardized.
|
|
|
|
- // This tries until break through is made.
|
|
|
|
- this._listenForSuccessfulSync();
|
|
|
|
- return retryPromiseUntil(() => this._connectAttempt(), () => this.board.isInBootLoader);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- _connectAttempt() {
|
|
|
|
- return this.board.resetIntoBootLoader()
|
|
|
|
- .then(() => delay(100))
|
|
|
|
- .then(() => retryPromiseUntil(() => this._flushAndSync(), () => this.board.isInBootLoader, 10));
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- _flushAndSync() {
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
- this._port.flush((error) => {
|
|
|
|
- if (error) {
|
|
|
|
- reject(error);
|
|
|
|
- }
|
|
|
|
- log.info("Port flushed");
|
|
|
|
-
|
|
|
|
- resolve();
|
|
|
|
- });
|
|
|
|
- }).then(() => this.sync(true));
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- _listenForSuccessfulSync() {
|
|
|
|
- let commandName = commandToKey(commands.SYNC_FRAME);
|
|
|
|
- let successfulSyncs = 0;
|
|
|
|
-
|
|
|
|
- this.on("RECEIVED-" + commandName, (response) => {
|
|
|
|
- successfulSyncs++;
|
|
|
|
- if (successfulSyncs >= REQUIRED_SUCCESSFUL_SYNC_COUNT) {
|
|
|
|
- log.info("Got enough successful syncs");
|
|
|
|
- this.board.isInBootLoader = true;
|
|
|
|
- this.removeAllListeners("RECEIVED-" + commandName);
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * Send appropriate C struct header along with command as required
|
|
|
|
- * SEE: https://github.com/igrr/esptool-ck/blob/master/espcomm/espcomm.h#L49
|
|
|
|
- */
|
|
|
|
- headerPacketFor(command, data) {
|
|
|
|
- let buf = new ArrayBuffer(8);
|
|
|
|
- let dv = new DataView(buf);
|
|
|
|
- let checksum = 0;
|
|
|
|
- if (command === commands.FLASH_DOWNLOAD_DATA) {
|
|
|
|
- // There are additional headers here....
|
|
|
|
- checksum = this.calculateChecksum(data.slice(16));
|
|
|
|
- } else if (command === commands.FLASH_DOWNLOAD_DONE) {
|
|
|
|
- // Nothing to see here
|
|
|
|
- } else {
|
|
|
|
- // Most commands want the checksum of the entire data packet
|
|
|
|
- checksum = this.calculateChecksum(data);
|
|
|
|
- }
|
|
|
|
- dv.setUint8(0, 0x00); // Direction, 0x00 is request
|
|
|
|
- dv.setUint8(1, command); // Command, see commands constant
|
|
|
|
- dv.setUint16(2, data.byteLength, true); // Size of request
|
|
|
|
- dv.setUint32(4, checksum, true);
|
|
|
|
- return new Buffer(buf);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * Unpack the response header
|
|
|
|
- */
|
|
|
|
- headerPacketFrom(buffer) {
|
|
|
|
- let header = {};
|
|
|
|
- header.direction = buffer.readUInt8(0);
|
|
|
|
- header.command = buffer.readUInt8(1);
|
|
|
|
- header.size = buffer.readUInt16LE(2);
|
|
|
|
- header.checksum = buffer.readUInt32LE(4);
|
|
|
|
- return header;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- determineNumBlocks(blockSize, length) {
|
|
|
|
- return Math.floor((length + blockSize - 1) / blockSize);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * Erases the area before flashing it
|
|
|
|
- */
|
|
|
|
- prepareFlashAddress(address, size) {
|
|
|
|
- log.info("Preparing flash address", address, size);
|
|
|
|
- let numBlocks = this.determineNumBlocks(FLASH_BLOCK_SIZE, size);
|
|
|
|
- let sectorsPerBlock = 16;
|
|
|
|
- let sectorSize = 4096;
|
|
|
|
- let numSectors = Math.floor((size + sectorSize - 1) / sectorSize);
|
|
|
|
- let startSector = Math.floor(address / sectorSize);
|
|
|
|
- // Leave some room for header space
|
|
|
|
- let headSectors = sectorsPerBlock - (startSector % sectorsPerBlock);
|
|
|
|
- if (numSectors < headSectors) {
|
|
|
|
- headSectors = numSectors;
|
|
|
|
- }
|
|
|
|
- let eraseSize = (numSectors - headSectors) * sectorSize;
|
|
|
|
- // TODO:csd - Research this...
|
|
|
|
- /* SPIEraseArea function in the esp8266 ROM has a bug which causes extra area to be erased.
|
|
|
|
- If the address range to be erased crosses the block boundary,
|
|
|
|
- then extra head_sector_count sectors are erased.
|
|
|
|
- If the address range doesn't cross the block boundary,
|
|
|
|
- then extra total_sector_count sectors are erased.
|
|
|
|
- */
|
|
|
|
- if (numSectors < (2 * headSectors)) {
|
|
|
|
- eraseSize = ((numSectors + 1) / 2) * sectorSize;
|
|
|
|
- }
|
|
|
|
- var buffer = new ArrayBuffer(16);
|
|
|
|
- var dv = new DataView(buffer);
|
|
|
|
- dv.setUint32(0, eraseSize, true);
|
|
|
|
- dv.setUint32(4, numBlocks, true);
|
|
|
|
- dv.setUint32(8, FLASH_BLOCK_SIZE, true);
|
|
|
|
- dv.setUint32(12, address, true);
|
|
|
|
- return this.sendCommand(commands.FLASH_DOWNLOAD_BEGIN, new Buffer(buffer));
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- flashAddressFromFile(address, fileName) {
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
- fs.readFile(fileName, (err, data) => {
|
|
|
|
- if (err) {
|
|
|
|
- reject(err);
|
|
|
|
- }
|
|
|
|
- return this.flashAddress(address, data)
|
|
|
|
- .then((result) => resolve(result));
|
|
|
|
- });
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- flashSpecifications(specs) {
|
|
|
|
- let totalBytes = specs.reduce((counter, spec) => {
|
|
|
|
- return counter + spec.buffer.length;
|
|
|
|
- }, 0);
|
|
|
|
- let details = {
|
|
|
|
- totalFiles: specs.length,
|
|
|
|
- totalBytes: totalBytes,
|
|
|
|
- flashedBytes: 0
|
|
|
|
- };
|
|
|
|
- this.setState("Flashing", details);
|
|
|
|
- let promiseFunctions = specs.map((spec, index) => () => {
|
|
|
|
- details.currentIndex = index;
|
|
|
|
- details.currentAddress = spec.address;
|
|
|
|
- details.currentSize = spec.buffer.length;
|
|
|
|
- return this.flashAddress(Number.parseInt(spec.address), spec.buffer);
|
|
|
|
- });
|
|
|
|
- return promiseChain(promiseFunctions);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- flashAddress(address, data) {
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
- this.prepareFlashAddress(address, data.length)
|
|
|
|
- .then(() => {
|
|
|
|
- let numBlocks = this.determineNumBlocks(FLASH_BLOCK_SIZE, data.length);
|
|
|
|
- let requests = [];
|
|
|
|
- for (let seq = 0; seq < numBlocks; seq++) {
|
|
|
|
- let startIndex = seq * FLASH_BLOCK_SIZE;
|
|
|
|
- let endIndex = Math.min((seq + 1) * FLASH_BLOCK_SIZE, data.length);
|
|
|
|
- let block = data.slice(startIndex, endIndex);
|
|
|
|
- // On the first block of the first sequence, override the flash info...
|
|
|
|
- if (address === 0 && seq === 0 && block[0] === 0xe9) {
|
|
|
|
- // ... which lives in the 3rd and 4th bytes
|
|
|
|
- let flashInfoBuffer = this.board.flashInfoAsBytes();
|
|
|
|
- block[2] = flashInfoBuffer[0];
|
|
|
|
- block[3] = flashInfoBuffer[1];
|
|
|
|
- }
|
|
|
|
- // On the last block
|
|
|
|
- if (endIndex === data.length) {
|
|
|
|
- // Pad the remaining bits
|
|
|
|
- let padAmount = FLASH_BLOCK_SIZE - block.length;
|
|
|
|
- block = Buffer.concat([block, new Buffer(padAmount).fill(0xFF)]);
|
|
|
|
- }
|
|
|
|
- var buffer = new ArrayBuffer(16);
|
|
|
|
- var dv = new DataView(buffer);
|
|
|
|
- dv.setUint32(0, block.length, true);
|
|
|
|
- dv.setUint32(4, seq, true);
|
|
|
|
- dv.setUint32(8, 0, true); // Uhhh
|
|
|
|
- dv.setUint32(12, 0, true); // Uhhh
|
|
|
|
- requests.push(Buffer.concat([new Buffer(buffer), block]));
|
|
|
|
- }
|
|
|
|
- let promiseFunctions = requests.map((req, index) => () => {
|
|
|
|
- // 16 is the header
|
|
|
|
- this.reportBlockProgress(req.length - 16, index, requests.length);
|
|
|
|
- return this.sendCommand(commands.FLASH_DOWNLOAD_DATA, req);
|
|
|
|
- });
|
|
|
|
- return promiseChain(promiseFunctions);
|
|
|
|
- }).then((result) => resolve(result));
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- reportBlockProgress(length, index, total) {
|
|
|
|
- this.state.details.flashedBytes += length;
|
|
|
|
- this.state.details.blockSize = length;
|
|
|
|
- this.state.details.blockIndex = index;
|
|
|
|
- this.state.details.totalBlocks = total;
|
|
|
|
- this.emit('progress', this.state);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * Must be called after flashing has occurred to switch modes
|
|
|
|
- */
|
|
|
|
- flashFinish(reboot) {
|
|
|
|
- let buffer = new ArrayBuffer(4);
|
|
|
|
- let dv = new DataView(buffer);
|
|
|
|
- // FIXME:csd - That inverted logic is correct...probably a better variable name than reboot
|
|
|
|
- dv.setUint32(0, reboot ? 0 : 1, true);
|
|
|
|
- return this.sendCommand(commands.FLASH_DOWNLOAD_DONE, new Buffer(buffer))
|
|
|
|
- .then((result) => {
|
|
|
|
- log.info("Received result", result);
|
|
|
|
- this.board.isInBootLoader = false;
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * Sends defined commands to ESP8266 and patiently awaits response through asynchronous nature of
|
|
|
|
- * node-serialport.
|
|
|
|
- */
|
|
|
|
- sendCommand(command, data, ignoreResponse) {
|
|
|
|
- // https://github.com/themadinventor/esptool/blob/master/esptool.py#L108
|
|
|
|
- // https://github.com/igrr/esptool-ck/blob/master/espcomm/espcomm.c#L103
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
- if (command != commands.NO_COMMAND) {
|
|
|
|
- let sendHeader = this.headerPacketFor(command, data);
|
|
|
|
- let message = Buffer.concat([sendHeader, data], sendHeader.length + data.length);
|
|
|
|
- this.out.write(message, 'buffer', (err, res) => {
|
|
|
|
- delay(10).then(() => {
|
|
|
|
- if (ignoreResponse) {
|
|
|
|
- resolve("Response was ignored");
|
|
|
|
- }
|
|
|
|
- if (this.portIsOpen) {
|
|
|
|
- this._port.drain((drainErr, results) => {
|
|
|
|
- log.info("Draining after write", drainErr, results);
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
- if (!ignoreResponse) {
|
|
|
|
- let commandName = commandToKey(command);
|
|
|
|
- let key = "RECEIVED-" + commandName;
|
|
|
|
- if (this.listeners(key).length === 0) {
|
|
|
|
- log.info("Listening once", commandName);
|
|
|
|
- this.once(key, (response) => {
|
|
|
|
- resolve(response);
|
|
|
|
- });
|
|
|
|
- } else {
|
|
|
|
- log.warn("Someone is already awaiting for %s", commandName);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-module.exports = RomComm;
|
|
|