Andrew Chalkley před 9 roky
rodič
revize
24285841c3

+ 5 - 0
.jshintrc

@@ -0,0 +1,5 @@
+{
+    "esversion": 6,
+    "node": true,
+    "strict": true
+}

+ 35 - 4
README.md

@@ -1,17 +1,48 @@
 # Especially Flasher
 
-__Especially Flasher_ is a tool to get JavaScript running natively on 
-the Internet of Things device, ESP8266. This application runs on 
+__Especially Flasher_ is a tool to get JavaScript running natively on
+the Internet of Things device, ESP8266. This application runs on
 Windows, Mac OS X and Linux.
 
 This tool flashes (installs) the Espruino JavaScript run time on ESP8266
 EP-12 devices like the Adafruit Huzzah and Adadfruit Feather Huzzah.
 
+
+## ROM Communication
+The ESP8266 is notoriously finicky about being flashed, we've done our best to abstract that for you.
+
+Here is an example of flashing the ESP8266 with the latest Espruino build.
+
+```javascript
+const log = require("./logger");
+
+const esp = new RomComm({
+    portName: "/dev/cu.SLAB_USBtoUART",
+    baudRate: 115200
+});
+
+esp.open().then((result) => {
+    log.info("ESP is open", result);
+    esp.flashAddressFromFile(0x0000, "/path/to/binaries/boot_v1.4(b1).bin")
+        .then(() => esp.flashAddressFromFile(0x1000, "/path/to/binaries/espruino_esp8266_user1.bin"))
+        .then(() => esp.flashAddressFromFile(0x3FC000, "/path/to/binaries/esp_init_data_default.bin"))
+        .then(() => esp.flashAddressFromFile(0x3FE000, "/path/to/binaries/blank.bin"))
+        .then(() => esp.close())
+        .then((result) => log.info("Flashed to latest Espruino build!", result));
+}).catch((error) => {
+    log.error("Oh noes!", error);
+});
+```
+
+See also `RomComm.flashAddress` for passing just a buffer representation of the file.
+
+We are using [Bunyan](https://github.com/trentm/node-bunyan) for logging, make sure to pipe it through the parser.
+
 -------
 
-## Contributing 
+## Contributing
 
-If you want to contribute to the Especially Flasher clone this repo and 
+If you want to contribute to the Especially Flasher clone this repo and
  run the following commands.
 
 ```bash

+ 90 - 0
back-end/boards.js

@@ -0,0 +1,90 @@
+"use strict";
+
+const log = require("./logger");
+const delay = require("./utilities").delay;
+
+const FLASH_MODES = {
+    qio: 0,
+    qout: 1,
+    dio: 2,
+    dout: 3
+};
+
+const FLASH_FREQUENCIES = {
+    "40m": 0,
+    "26m": 1,
+    "20m": 2,
+    "80m": 0xf
+};
+
+const FLASH_SIZES = {
+    "4m": 0x00,
+    "2m": 0x10,
+    "8m": 0x20,
+    "16m": 0x30,
+    "32m": 0x40,
+    "16m-c1": 0x50,
+    "32m-c1": 0x60,
+    "32m-c2": 0x70
+};
+
+class EspBoard {
+    constructor(port) {
+        this.port = port;
+    }
+
+    portSet(options) {
+        return new Promise((resolve, reject) => {
+            log.info("Setting port", options);
+            this.port.set(options, (err, result) => {
+                if (err) {
+                    reject(err);
+                }
+                resolve(result);
+            });
+        });
+    }
+
+
+    flashInfoAsBytes() {
+        let buffer = new ArrayBuffer(2);
+        let dv = new DataView(buffer);
+        dv.setUint8(0, FLASH_MODES[this.flashMode]);
+        dv.setUint8(1, FLASH_SIZES[this.flashSize] + FLASH_FREQUENCIES[this.flashFrequency]);
+        return new Buffer(buffer);
+    }
+
+    resetIntoBootLoader() {
+        throw new Error("Must define bootloader reset instructions");
+    }
+
+}
+
+/**
+ * Tested: Adafruit Feather Huzzah
+ * Needs testing: Adafruit Huzzah, SparkFun Thing, SparkFun Thing Dev Board
+ */
+class Esp12 extends EspBoard {
+    constructor(port) {
+        super(port);
+        this.flashFrequency = "80m";
+        this.flashMode = "qio";
+        this.flashSize = "32m";
+    }
+
+    resetIntoBootLoader() {
+        // RTS - Request To Send
+        // DTR - Data Terminal Ready
+        // NOTE: Must set values at the same time.
+        log.info("Resetting board");
+        return this.portSet({rts: true, dtr:false})
+            .then(() => delay(5))
+            .then(() => this.portSet({rts: false, dtr: true}))
+            .then(() => delay(50))
+            .then(() => this.portSet({rts: false, dtr: false}));
+    }
+}
+
+module.exports = {
+    Esp12: Esp12
+};

+ 5 - 0
back-end/logger.js

@@ -0,0 +1,5 @@
+"use strict";
+
+const bunyan = require("bunyan");
+
+module.exports = bunyan.createLogger({name: "especially-flasher"});

+ 387 - 0
back-end/rom_comm.js

@@ -0,0 +1,387 @@
+"use strict";
+
+const fs = require("fs");
+const SerialPort = require("serialport").SerialPort;
+const log = require("./logger");
+const slip = require("./streams/slip");
+const boards = require("./boards");
+const delay = require("./utilities").delay;
+const repeatPromise = require("./utilities").repeatPromise;
+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.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]);
+
+/**
+ * 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 {
+    constructor(config) {
+        this._port = new SerialPort(config.portName, {
+            baudRate: config.baudRate,
+            parity: 'none',
+            stopBits: 1,
+            xon: false,
+            xoff: false,
+            rtscts: false,
+            dsrdtr: false
+        }, false);
+        this.bindPort();
+        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;
+        this.isInBootLoader = false;
+    }
+
+    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));
+    }
+
+    /**
+     * 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;
+        }
+        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;
+        }
+        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);
+        // TODO:csd - Make the RomComm an EventEmitter?
+        this.in.emit(commandName, body);
+    }
+
+    /**
+     * 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 {
+                    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...");
+            }));
+    }
+
+    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.isInBootLoader = true;
+                }
+            });
+    }
+
+    connect() {
+        // Eventually responses calm down, but on initial contact, responses are not standardized.
+        // This tries until break through is made.
+        return repeatPromise(5, () => {
+            if (this.isInBootLoader) {
+                log.info("In Bootloader not re-connecting");
+                return true;
+            } else {
+                return this._connectAttempt();
+            }
+        }).then(() => this.sync())
+        .then(() => this.isInBootLoader);
+    }
+
+    _connectAttempt() {
+        return this.board.resetIntoBootLoader()
+                .then(() => delay(100))
+                // And a 5x loop here
+                .then(() => repeatPromise(5, () => {
+                    if (this.isInBootLoader) {
+                        log.info("In Bootloader not syncing");
+                        return true;
+                    } else {
+                        return this._flushAndSync();
+                    }
+                }));
+    }
+
+    _flushAndSync() {
+        return new Promise((resolve, reject) => {
+                    this._port.flush((error) => {
+                        if (error) {
+                            reject(error);
+                        }
+                        log.info("Port flushed");
+
+                        resolve();
+                    });
+            }).then(() => this.sync(true));
+    }
+
+    /**
+     * 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));
+            });
+        });
+    }
+
+    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) => () => this.sendCommand(commands.FLASH_DOWNLOAD_DATA, req));
+                    return promiseChain(promiseFunctions);
+                }).then((result) => resolve(result));
+        });
+    }
+
+    /**
+     * 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.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(5).then(() => {
+                        this._port.drain((drainErr, results) => {
+                            log.info("Draining", drainErr, results);
+                            if (ignoreResponse) {
+                                resolve("Response was ignored");
+                            }
+                        });
+                    });
+               });
+            }
+            if (!ignoreResponse) {
+                let commandName = commandToKey(command);
+                if (this.in.listeners(commandName).length === 0) {
+                    log.info("Listening once", commandName);
+                    this.in.once(commandName, (response) => {
+                        resolve(response);
+                    });
+                } else {
+                    log.warn("Someone is already awaiting for %s", commandName);
+                }
+            }
+        });
+    }
+}
+
+module.exports = RomComm;

+ 3 - 3
back-end/serial_scanner.js

@@ -15,7 +15,7 @@ module.exports = class SerialScanner extends EventEmitter {
                     this.emit("ports", this.ports);
                 });
             }
-        )
+        );
     }
 
     /**
@@ -27,13 +27,13 @@ module.exports = class SerialScanner extends EventEmitter {
         serialport.list(
             (err, ports) => {
                 this._listWithCallback(err, ports, () => {
-                    let newPorts = ports.map(this._portMap);
+                    const newPorts = ports.map(this._portMap);
                     this.checkDeviceRemoved(newPorts);
                     this.checkDeviceAdded(newPorts);
                     this.ports = newPorts;
                 });
             }
-        )
+        );
     }
 
     /**

+ 93 - 0
back-end/streams/slip.js

@@ -0,0 +1,93 @@
+"use strict";
+
+const stream = require("stream");
+const log = require("../logger");
+const Transform = stream.Transform;
+const Readable = stream.Readable;
+
+// https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol
+const CODES = {
+    frameEnd: 0xC0,
+    frameEscape: 0xDB,
+    transposedFrameEnd: 0xDC,
+    transposedFrameEscape: 0xDD
+ };
+
+
+
+class SlipDecoder extends Transform {
+    constructor(options) {
+        super(options);
+        this._slipping = false;
+        this.resetDecoded();
+    }
+
+    resetDecoded() {
+        this.decodedIndex = 0;
+        this.decoded = new Buffer(256);
+    }
+
+    _transform(chunk, encoding, done) {
+        log.info("SlipDecoder._transform", encoding, chunk.length);
+        for (let index = 0; index < chunk.length; index++) {
+            let val = chunk[index];
+            if (val === CODES.frameEnd) {
+                log.debug("frameEnd detected");
+                if (this._slipping) {
+                    this._slipping = false;
+                    // Return all of decoded
+                    this.push(this.decoded.slice(0, this.decodedIndex));
+                    log.debug("Resetting buffer");
+                    this.resetDecoded();
+                } else {
+                    this._slipping = true;
+                }
+                continue;
+            }
+            if (this._slipping) {
+                // Slip decoding
+                if (val === CODES.frameEscape) {
+                    // Move one past the escape char
+                    index++;
+                    if (chunk[index] === CODES.transposedFrameEnd) {
+                        val = CODES.frameEnd;
+                    } else if (chunk[index] === CODES.transposedFrameEscape) {
+                        val = CODES.frameEscape;
+                    }
+                }
+                this.decoded[this.decodedIndex++] = val;
+            }
+        }
+        done();
+    }
+}
+
+class SlipEncoder extends Transform {
+
+    _transform(chunk, encoding, done) {
+        log.info("SlipEncoder._transform", encoding);
+        let encoded = new Buffer(chunk.length + 100);
+        let encodedIndex = 0;
+        encoded[encodedIndex++] = CODES.frameEnd;
+        for (var i = 0; i < chunk.length; i++) {
+            if (chunk[i] === CODES.frameEnd) {
+                encoded[encodedIndex++] = CODES.frameEscape;
+                encoded[encodedIndex++] = CODES.transposedFrameEnd;
+            } else if (chunk[i] === CODES.frameEscape) {
+                encoded[encodedIndex++] = CODES.frameEscape;
+                encoded[encodedIndex++] = CODES.transposedFrameEscape;
+            } else {
+                encoded[encodedIndex++] = chunk[i];
+            }
+        }
+        encoded[encodedIndex++] = CODES.frameEnd;
+        this.push(encoded.slice(0, encodedIndex), encoding);
+        done();
+    }
+
+}
+
+module.exports = {
+    SlipDecoder: SlipDecoder,
+    SlipEncoder: SlipEncoder
+};

+ 52 - 0
back-end/utilities.js

@@ -0,0 +1,52 @@
+"use strict";
+
+const log = require("./logger");
+
+/**
+ * Creates delays in time. Ideal for gaurenteeing time between executions
+ * between {Promise} resolve handlers.
+ * @param time in milliseconds
+ * @returns {Promise}
+ */
+function delay(time) {
+    return new Promise((resolve) => {
+        log.info("Delaying for %d ms", time);
+        setTimeout(resolve, time);
+    });
+}
+
+/**
+ * Repeats a promise a given number of times.
+ * @param times. The number of times to repeat a given promise.
+ * @param callback is a no parameter based function that returns a {Promise}.
+ * @returns {Promise}
+ */
+function repeatPromise(times, callback) {
+    let chain = Promise.resolve();
+    // Range just used for closure based loop
+    let range = new Array(times)
+        .fill(0)
+        .map((value, index) => index);
+    range.forEach(() => {
+        chain = chain.then(() => callback());
+    });
+    return chain;
+}
+
+/**
+ * There is probably a better way to do this.  This returns a synchronized thenable chain.
+ */
+function promiseChain(promiseFunctions) {
+    log.debug("Chaining %d promises", promiseFunctions.length);
+    return promiseFunctions.reduce((prev, current, index) => {
+        log.debug("Chaining promise #%d", index);
+        // Lazily return the promise
+        return prev.then(() => current());
+    }, Promise.resolve());
+}
+
+module.exports = {
+    delay: delay,
+    repeatPromise: repeatPromise,
+    promiseChain: promiseChain
+};

+ 1 - 1
front-end/js/app.js

@@ -4,7 +4,7 @@
 const SerialScanner = require("../back-end/serial_scanner");
 const PortSelect = require("./js/port_select");
 
-function $(id) { return document.getElementById(id) }
+function $(id) { return document.getElementById(id); }
 
 const flashButton = $("flash-button");
 const appStatus = $("status");

+ 1 - 1
front-end/js/port_select.js

@@ -58,4 +58,4 @@ module.exports = class PortSelect {
     set disabled (value) {
         this.selectElement.disabled = value;
     }
-}
+};

+ 3 - 1
package.json

@@ -25,7 +25,9 @@
   "homepage": "https://github.com/thingsSDK/especially-flasher#readme",
   "dependencies": {
     "node-fetch": "^1.4.0",
-    "serialport": "^2.0.6"
+    "bunyan": "^1.8.0",
+    "node-binary": "^1.1.0",
+    "serialport": "^2.0.7-beta1"
   },
   "devDependencies": {
     "electron-prebuilt": "^0.36.9"