Automatic VBUS acquisition and DFU mode

- 3 tabs layout: automatic VBUS measurement, console commands and DFU.
- use Bootstrap CSS
- switch console commands to the new packetized interface.
- for DFU: add a firmware store to flash the current version / backup the dongle.
  (DFU not fully completed)

Change-Id: If01fd246aa2b4aca213fda810e70f4e904781036
Signed-off-by: Vincent Palatin <vpalatin@chromium.org>
diff --git a/README.md b/README.md
index 004e2ea..10fcc5c 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# WebTwinkie
+# TWebkie
 
 A install-less WebUSB-based tool for the Twinkie USB Power Delivery monitoring
 dongle.
@@ -6,7 +6,7 @@
 ## Quick-start
 
 - Plug the Twinkie dongle to your host
-- Open your Chrome browser and visit [webtwinkie.org](https://storage.googleapis.com/webtwinkie.org/tool.html)
+- Open your Chrome browser and visit [twebkie.org](https://twebkie.org)
  (or click on the pop-up)
 
 ## Just a console for now ...
@@ -15,4 +15,4 @@
 
 - The Twinkie development Wiki: [dev.chromium.org/chromium-os/twinkie](http://dev.chromium.org/chromium-os/twinkie)
 - The firmware code base: [chromium.googlesource.com/chromiumos/platform/ec](https://chromium.googlesource.com/chromiumos/platform/ec/+/firmware-twinkie-9628.B)
-- This tool [sources](https://chromium.googlesource.com/chromiumos/webtwinkie)
+- This tool [sources](https://chromium.googlesource.com/chromiumos/twebkie)
diff --git a/decoder.js b/decoder.js
new file mode 100644
index 0000000..ec1f8f0
--- /dev/null
+++ b/decoder.js
@@ -0,0 +1,101 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+'use strict';
+
+var decoder = {};
+
+// Power roles
+decoder.ROLE_SINK = 0;
+decoder.ROLE_SOURCE = 1;
+// Data role
+decoder.ROLE_UFP = 0;
+decoder.ROLE_DFP = 1;
+// Vconn role
+decoder.ROLE_VCONN_OFF = 0;
+decoder.ROLE_VCONN_ON = 1;
+
+// Control Message type
+decoder.CtrlType = {
+  0: 'reserved',
+  1: 'GOOD CRC',
+  2: 'GOTO MIN',
+  3: 'ACCEPT',
+  4: 'REJECT',
+  5: 'PING',
+  6: 'PS RDY',
+  7: 'GET SOURCE CAP',
+  8: 'GET SINK CAP',
+  9: 'DR SWAP',
+  10: 'PR SWAP',
+  11: 'VCONN SWAP',
+  12: 'WAIT',
+  13: 'SOFT RESET',
+  14: 'reserved',
+  15: 'reserved',
+  // PD 3.0
+  16: 'NOT SUPPORTED',
+  17: 'GET SOURCE CAP EXT',
+  18: 'GET STATUS',
+  19: 'FR SWAP',
+  20: 'GET PPS STATUS',
+  21: 'GET COUNTRY CODES',
+};
+
+// Data message type
+decoder.DataType = {
+  0: 'reserved',
+  1: 'SOURCE CAP',
+  2: 'REQUEST',
+  3: 'BIST',
+  4: 'SINK CAP',
+  5: 'BATTERY STATUS',
+  6: 'ALERT',
+  7: 'GET COUNTRY INFO',
+  15: 'VDM',
+};
+
+decoder.PDPacket = function(head, payload) {
+  this.head_ = head;
+  this.payload_ = payload;
+  this.cnt_ = (head >> 12) & 7;
+  this.pkttype_ = head & 0xf; // TODO: update for PD3.0
+  this.prole_ = (head >> 8) & 1;
+  this.drole_ = (head >> 5) & 1;
+};
+
+decoder.PDPacket.prototype.isGoodCRC = function() {
+  return this.pkttype_ === 1 && this.cnt_ === 0;
+};
+
+decoder.PDPacket.prototype.isSource = function() {
+  return this.prole_ === decoder.ROLE_SOURCE;
+};
+
+decoder.PDPacket.prototype.fullRole = function() {
+  return (this.prole_ === decoder.ROLE_SOURCE ? 'SRC' : 'SNK') + '/' +
+         (this.drole_ === decoder.ROLE_DFP ? 'DFP' : 'UFP');
+};
+
+decoder.PDPacket.prototype.packetType = function() {
+  return this.cnt_ === 0 ? decoder.CtrlType[this.pkttype_] :
+                           decoder.DataType[this.pkttype_];
+};
+
+function toHex(digits, val) { // TODO: remove me
+  var s = '';
+  for (var i = 0; i < digits; ++i) {
+    s = ('0123456789abcdef'.charAt(val & 0xF)) + s;
+    val >>= 4;
+  }
+  return s;
+}
+
+decoder.PDPacket.prototype.description = function() {
+  var desc = '[' + toHex(4, this.head_) + '] ';
+  for (var i = 0; i < this.payload_.length; i++) {
+    desc += ' ' + toHex(8, this.payload_[i]);
+  }
+  return desc;
+};
diff --git a/dfu.js b/dfu.js
new file mode 100644
index 0000000..8914bce
--- /dev/null
+++ b/dfu.js
@@ -0,0 +1,383 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+dfu = {};
+
+'use strict';
+
+// DFU commands numbers in bRequest.
+dfu.DFU_DETACH = 0;
+dfu.DFU_DNLOAD = 1;
+dfu.DFU_UPLOAD = 2;
+dfu.DFU_GETSTATUS = 3;
+dfu.DFU_CLRSTATUS = 4;
+dfu.DFU_GETSTATE = 5;
+dfu.DFU_ABORT = 6;
+
+// DFU Interface parameters.
+dfu.INTERFACE_CLASS = 0xFE;    /* Application Specific Class */
+dfu.INTERFACE_SUBCLASS = 0x01; /* Device Firmware Upgrade Code */
+
+// iInterface string for the STM32F072 Flash DFU interface.
+dfu.STM32F072_INTERFACE_NAME = '@Internal Flash  /0x08000000/064*0002Kg';
+
+dfu.Status = {
+  OK: 0x00, /* No error condition is present. */
+  errTARGET: 0x01, /* File is not targeted for use by this device. */
+  errFILE: 0x02, /* File is for this device but fails some vendor-specific verification test. */
+  errWRITE: 0x03, /* Device is unable to write memory. */
+  errERASE: 0x04, /* Memory erase function failed. */
+  errCHECK_ERASED: 0x05, /* Memory erase check failed. */
+  errPROG: 0x06, /* Program memory function failed. */
+  errVERIFY: 0x07, /* Programmed memory failed verification. */
+  errADDRESS: 0x08, /* Cannot program memory due to received address that is out of range. */
+  errNOTDONE: 0x09, /* Received DFU_DNLOAD with wLength = 0, but device does not think it has all of the data yet. */
+  errFIRMWARE: 0x0A, /* Device’s firmware is corrupt. It cannot return to run-time (non-DFU) operations. */
+  errVENDOR: 0x0B, /* iString indicates a vendor-specific error. */
+  errUSBR: 0x0C, /* Device detected unexpected USB reset signaling. */
+  errPOR: 0x0D, /* Device detected unexpected power on reset. */
+  errUNKNOWN: 0x0E, /* Something went wrong, but the device does not know what it was. */
+  errSTALLEDPKT: 0x0F,
+};
+
+dfu.State = {
+  appIDLE: 0, /* Device is running its normal application. */
+  appDETACH: 1, /* Device has received the DFU_DETACH request */
+  dfuIDLE: 2, /* Device is operating in the DFU mode and is waiting for requests. */
+  dfuDNLOAD_SYNC: 3, /* Device has received a block and is waiting for the host */
+  dfuDNBUSY: 4, /* Device is programming a control-write block */
+  dfuDNLOAD_IDLE: 5, /* Device is processing a download operation. */
+  dfuMANIFEST_SYNC: 6, /* Device has received the final block of firmware or ... */
+  dfuMANIFEST: 7, /* Device is in the Manifestation phase. */
+  dfuMANIFEST_WAIT_RESET: 8, /* Device is waiting for a USB reset or a power on reset */
+  dfuUPLOAD_IDLE: 9, /* The device is processing an upload operation. */
+  dfuERROR: 10, /* An error has occurred. Awaiting the DFU_CLRSTATUS request. */
+};
+
+// DfuSe (DFU with ST Microsystems extensions) protocol.
+dfu.STM_CMD_GET_COMMANDS = 0x00;
+dfu.STM_CMD_SET_ADDRESS = 0x21;
+dfu.STM_CMD_ERASE = 0x41;
+dfu.STM_CMD_READ_UNPROTECT = 0x92;
+
+dfu.STM_FLASH_ADDRESS = 0x08000000;
+dfu.STM_MAX_SIZE = 2048;
+dfu.STM_PAGE_SIZE = 2048; // MCU specific TODO: parse it from the interface string
+
+// USB identifiers.
+dfu.STM_VENDOR_ID = 0x0483;
+dfu.STM_PRODUCT_ID = 0xdf11;
+
+dfu.isDFU = function(device) {
+  return device.vendorId === dfu.STM_VENDOR_ID &&
+         device.productId === dfu.STM_PRODUCT_ID;
+};
+
+dfu.Dongle = function(device) {
+  this.device_ = device;
+  console.log(device);
+};
+
+dfu.Dongle.prototype.send = function(req, val, data) {
+  var xfer = {
+    requestType: 'class',
+    recipient: 'interface',
+    request: req,
+    value: val,
+    index: this.ifaceNum_,
+  };
+  return this.device_.controlTransferOut(xfer, data ? data : new ArrayBuffer());
+};
+
+dfu.Dongle.prototype.recv = function(req, val, len) {
+  return new Promise(function(resolve, reject) {
+    var xfer = {
+      requestType: 'class',
+      recipient: 'interface',
+      request: req,
+      value: val,
+      index: this.ifaceNum_,
+    };
+    this.device_.controlTransferIn(xfer, len).then(result => {
+      if (result.status === 'ok') {
+        resolve(new Uint8Array(result.data.buffer));
+      } else {
+        console.log('Transfer failed ' + result.status);
+        reject(result.status);
+      }
+    });
+  }.bind(this));
+};
+
+// --- Standard DFU requests ---
+
+// 10100001b DFU_GETSTATE Zero Interface 1 State
+dfu.Dongle.prototype.getState = function() {
+  return new Promise(function(resolve) {
+    this.recv(dfu.DFU_GETSTATE, 0, 1).then(data => {
+      resolve(data[0]);
+    });
+  }.bind(this));
+};
+
+// 10100001b DFU_GETSTATUS Zero Interface 6 Status
+dfu.Dongle.prototype.getStatus = function() {
+  return this.recv(dfu.DFU_GETSTATUS, 0, 6);
+};
+
+// 00100001b DFU_CLRSTATUS Zero Interface Zero None
+dfu.Dongle.prototype.clearStatus = function() {
+  return this.send(dfu.DFU_CLRSTATUS, 0, null);
+};
+
+// 00100001b DFU_DNLOAD wBlockNum Interface Length Firmware
+dfu.Dongle.prototype.download = function(blockNum, fw) {
+  return this.send(dfu.DFU_DNLOAD, blockNum, fw);
+};
+
+// 10100001b DFU_UPLOAD Zero Interface Length Firmware
+dfu.Dongle.prototype.upload = function(blockNum, len) {
+  return this.recv(dfu.DFU_UPLOAD, blockNum, len);
+};
+
+// 00100001b DFU_ABORT Zero Interface Zero None
+dfu.Dongle.prototype.abort = function() {
+  return this.send(dfu.DFU_ABORT, 0, null);
+};
+
+// 00100001b DFU_DETACH wTimeout Interface Zero None
+dfu.Dongle.prototype.clearStatus = function() {
+  return this.send(dfu.DFU_CLRSTATUS, 0, null);
+};
+
+dfu.Dongle.prototype.checkStatus = function(cond) {
+  return new Promise(function(resolve, reject) {
+    if (!cond) {
+      cond = {status: dfu.Status.OK};
+    }
+
+    this.getStatus().then(pkt => {
+      var bStatus = pkt[0];
+      var bState = pkt[4];
+      var result = true;
+      if (cond.state && bState !== cond.state) {
+        result = false;
+      }
+      if (cond.status && bStatus !== cond.status) {
+        result = false;
+      }
+
+      if (result) {
+        resolve();
+      } else {
+        reject('Invalid Status ' + bState + '/' + bStatus +
+               ' expected: ' + cond);
+      }
+    }).catch(error => {
+      reject('Cannot get status: ' + error);
+    });
+  }.bind(this));
+};
+// ---  DFUse requests ---
+dfu.Dongle.prototype.stmGetCommands = function() {
+  return this.recv(dfu.DFU_UPLOAD, 0, 4);
+};
+
+dfu.Dongle.prototype.stmErase = function(addr) {
+  var cmd = new Uint8Array(5); // 2 for mass-erase
+  cmd[0] = dfu.STM_CMD_ERASE;
+  cmd[1] = (addr >> 0) & 0xFF;
+  cmd[2] = (addr >> 8) & 0xFF;
+  cmd[3] = (addr >> 16) & 0xFF;
+  cmd[4] = (addr >> 24) & 0xFF;
+  return this.send(dfu.DFU_DNLOAD, 0, cmd)
+      .then(() => this.checkStatus({state: dfu.State.dfuDNBUSY}))
+      .then(() => this.checkStatus())
+      .then(() => this.abort()) // Go back to dfuIDLE
+      .then(() => this.checkStatus());
+};
+
+dfu.Dongle.prototype.stmSetAddress = function(addr) {
+  var cmd = new Uint8Array(5);
+  cmd[0] = dfu.STM_CMD_SET_ADDRESS;
+  cmd[1] = (addr >> 0) & 0xFF;
+  cmd[2] = (addr >> 8) & 0xFF;
+  cmd[3] = (addr >> 16) & 0xFF;
+  cmd[4] = (addr >> 24) & 0xFF;
+  return this.send(dfu.DFU_DNLOAD, 0, cmd)
+      .then(() => this.checkStatus({state: dfu.State.dfuDNBUSY}))
+      .then(() => this.checkStatus())
+      .then(() => this.abort()) // Go back to dfuIDLE
+      .then(() => this.checkStatus());
+};
+
+dfu.Dongle.prototype.stmReadUnprotect = function() {
+  var cmd = new Uint8Array(1);
+  cmd[0] = dfu.STM_CMD_READ_UNPROTECT;
+  return this.send(dfu.DFU_DNLOAD, 0, cmd)
+      .then(() => this.checkStatus({state: dfu.State.dfuDNBUSY}));
+      // Device reboots here
+};
+
+dfu.Dongle.prototype.stmLeaveDfu = function() {
+  return this.send(dfu.DFU_DNLOAD, 0, 0);
+};
+
+dfu.Dongle.prototype.stmEraseMemory = function(addr, size) {
+  return new Promise(function(resolve, reject) {
+    if (size % dfu.STM_PAGE_SIZE) {
+      reject('Invalid erase size ' + size);
+    }
+
+    var remaining = size;
+
+    let eraseOnePage = () => {
+      this.stmErase(addr)
+          .then(() => {
+            addr += dfu.STM_PAGE_SIZE;
+            remaining -= dfu.STM_PAGE_SIZE;
+            if (remaining === 0) {
+              resolve();
+            } else {
+              eraseOnePage();
+            }
+          }).catch(error => {
+            reject('Erase error: ' + error);
+          });
+    };
+
+    eraseOnePage();
+  }.bind(this));
+};
+
+dfu.Dongle.prototype.stmReadMemory = function(addr, size) {
+  return new Promise(function(resolve, reject) {
+    var remaining = size;
+    var offset = 0;
+    var mem = new Uint8Array(size);
+
+    let readBlock = () => {
+      var len = Math.min(remaining, dfu.STM_MAX_SIZE);
+      var blockNum = offset / len + 2;
+      this.upload(blockNum, len)
+          .then(pkt => {
+            mem.set(pkt, offset);
+            offset += pkt.length;
+            remaining -= pkt.length;
+            if (remaining === 0) {
+              resolve(mem);
+            } else {
+              readBlock();
+            }
+          }).catch(error => {
+            reject('Read error: ' + error);
+          });
+    };
+
+    this.stmSetAddress(addr)
+        .then(() => {
+          readBlock();
+        });
+  }.bind(this));
+};
+
+dfu.Dongle.prototype.stmWriteMemory = function(addr, image, total_size) {
+  if (!total_size) {
+    var modulo = image.length % dfu.STM_PAGE_SIZE;
+    total_size = image.length;
+    if (modulo) {
+      total_size += dfu.STM_PAGE_SIZE - modulo;
+    }
+    // TODO: extend image ?
+    // var mem = new Uint8Array(size);
+  }
+
+  return new Promise(function(resolve, reject) {
+    var remaining = total_size;
+    var offset = 0;
+
+    let writeBlock = () => {
+      var len = Math.min(remaining, dfu.STM_MAX_SIZE);
+      var blockNum = offset / len + 2;
+      this.download(blockNum, len)
+          .then(() => this.checkStatus({state: dfu.State.dfuDNBUSY}))
+          .then(() => this.checkStatus())
+          .then(() => {
+            offset += len;
+            remaining -= len;
+            if (remaining === 0) {
+              resolve();
+            } else {
+              writeBlock();
+            }
+          }).catch(error => {
+            reject('Write error: ' + error);
+          });
+    };
+
+    // TODO: erase
+    this.stmSetAddress(addr)
+        .then(() => {
+          writeBlock();
+        });
+  }.bind(this));
+};
+// -------------------------
+
+dfu.Dongle.prototype.connect = function() {
+  return this.device_.open().then(() => {
+    var ifaces = this.device_.configuration.interfaces;
+    var dfuIface = ifaces[0];
+    for (var iface = 0; iface < ifaces.length; iface++) {
+      for (var alt = 0; alt < ifaces[iface].alternates.length; alt++) {
+        var i = ifaces[iface].alternates[alt];
+        if (i.interfaceClass === dfu.INTERFACE_CLASS &&
+            i.interfaceSubclass === dfu.INTERFACE_SUBCLASS) {
+          console.log(i);
+          // console.log(i.interfaceName);
+          if (i.interfaceName === dfu.STM32F072_INTERFACE_NAME) {
+            dfuIface = ifaces[iface];
+            break;
+          }
+        }
+      }
+    }
+    this.ifaceNum_ = dfuIface.interfaceNumber;
+  })
+  .then(() => this.device_.claimInterface(this.ifaceNum_));
+};
+
+dfu.Dongle.prototype.disconnect = function() {
+  return this.device_.close();
+};
+
+dfu.Dongle.prototype.gotoIdle = function() {
+  return this.getState()
+      .then(st => {
+        if (st === dfu.State.dfuERROR) {
+          return this.clearStatus()
+              .then(() => this.getStatus());
+        }
+      })
+      .then(() => this.abort())
+      .then(() => this.checkStatus({
+        state: dfu.State.OK,
+        status: dfu.Status.dfuIDLE,
+      }));
+};
+
+dfu.Dongle.prototype.backupFw = function() {
+  return this.gotoIdle()
+      .then(() => this.stmReadMemory(dfu.STM_FLASH_ADDRESS, 128 * 1024));
+};
+
+dfu.Dongle.prototype.updateFw = function(img) {
+  return this.gotoIdle()
+      .then(() => {
+        this.stmEraseMemory(dfu.STM_FLASH_ADDRESS + 64 * 1024, 64 * 1024);
+      })
+      .then(() => {
+        this.stmWriteMemory(dfu.STM_FLASH_ADDRESS + 64 * 1024, img, 64 * 1024);
+      });
+};
diff --git a/firmware_store.js b/firmware_store.js
new file mode 100644
index 0000000..94f316d
--- /dev/null
+++ b/firmware_store.js
@@ -0,0 +1,77 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+store = {};
+
+'use strict';
+
+// Firmware types.
+store.Type = {
+  RELEASE: 0,
+  TESTING: 1,
+  UPLOAD: 2,
+  BACKUP: 3,
+};
+
+//
+store.server_firmwares = [
+  {
+    type: store.Type.RELEASE,
+    name: 'twinkie_v1.11.15',
+    url: 'firmwares/twinkie_v1.11.15-4e11b1179.bin',
+  },
+  {
+    type: store.Type.TESTING,
+    name: 'twinkie_v1.11.18',
+    url: 'firmwares/twinkie_v1.11.18-c51928443.bin',
+  },
+];
+
+store.FwStore = function() {
+
+};
+
+store.FwStore.prototype.init = function() {
+  var local_firmwares =
+    JSON.parse(window.localStorage.getItem('local_firmwares')) || [];
+  console.log(local_firmwares);
+
+  function addFirmware(fname, t, buf) {
+    var entry = {type: t, name: fname, data: buf};
+    local_firmwares.push(entry);
+    // console.log(local_firmwares);
+    // console.log(JSON.stringify(local_firmwares));
+    console.log(JSON.stringify(buf));
+    window.localStorage.setItem('firmwares', JSON.stringify(local_firmwares));
+    // console.log(window.localStorage);
+    //'<a href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P48/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==">Downl</a>'
+  }
+
+  function fileUpload(evt) {
+    var f = evt.target.files[0];
+    var reader = new FileReader();
+    reader.onload = (function(f) {
+      return function(evt) {
+        addFirmware(f, store.Type.UPLOAD, new Uint8Array(evt.target.result));
+      };
+    })(f);
+    reader.readAsArrayBuffer(f);
+  }
+  var upload_img_btn = document.getElementById('upload_img');
+  upload_img_btn.addEventListener('change', fileUpload, false);
+
+  // Get fw from server
+  var req = new XMLHttpRequest();
+  req.open('GET', '/firmwares/twinkie_v1.11.15-4e11b1179.bin', true);
+  req.responseType = 'arraybuffer';
+
+  req.onload = function() {
+    var arrayBuffer = req.response;
+    if (arrayBuffer) {
+      var byteArray = new Uint8Array(arrayBuffer);
+      console.log(byteArray);
+    }
+  };
+  req.send(null);
+};
diff --git a/firmwares/twinkie_v1.11.15-4e11b1179.bin b/firmwares/twinkie_v1.11.15-4e11b1179.bin
new file mode 100644
index 0000000..9aa29fd
--- /dev/null
+++ b/firmwares/twinkie_v1.11.15-4e11b1179.bin
Binary files differ
diff --git a/firmwares/twinkie_v1.11.18-c51928443.bin b/firmwares/twinkie_v1.11.18-c51928443.bin
new file mode 100755
index 0000000..6f347e1
--- /dev/null
+++ b/firmwares/twinkie_v1.11.18-c51928443.bin
Binary files differ
diff --git a/publish.sh b/publish.sh
new file mode 100755
index 0000000..013a990
--- /dev/null
+++ b/publish.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+SITE="twebkie.org"
+BUCKET="gs://${SITE}/testing/"
+META_PREFIX="x-goog-meta-"
+META_SHA="${META_PREFIX}git-sha"
+
+REV="HEAD"
+GIT_SHA="$(git rev-parse ${REV})"
+
+echo "Pushing files at rev ${GIT_SHA} to ${BUCKET}"
+for f in $(git ls-tree -r --name-only HEAD)
+do
+  GS_FILE="${BUCKET}${f}"
+  echo "Pushing ${f}"
+  gsutil cp -a public-read "${f}" "${GS_FILE}"
+  gsutil setmeta -h "${META_SHA}:${GIT_SHA}" "${GS_FILE}"
+done
diff --git a/tool.html b/tool.html
index 558ffa1..b22d85b 100644
--- a/tool.html
+++ b/tool.html
@@ -28,17 +28,22 @@
     </style>
 
     <script type="text/javascript" src="twinkie.js"></script>
+    <script type="text/javascript" src="decoder.js"></script>
+    <script type="text/javascript" src="dfu.js"></script>
+    <script type="text/javascript" src="firmware_store.js"></script>
     <script type="text/javascript" src="tool.js"></script>
   </head>
   <body>
     <div class="container theme-showcase" role="main">
       <div class="panel panel-default">
-        <div class="panel-heading"><h3 class="panel-title">Twinkie control</h3></div>
+        <div class="panel-heading"><h3 class="panel-title" id="title">Twinkie control</h3></div>
         <div class="panel-body">
           <div class="row">
             <div class="col-md-4">
               <button class="btn btn-primary" id="connect">Connect</button>
-              <button class="btn btn-default" id="vbus">read VBUS</button>
+              <button class="btn btn-default" id="vbus">VBUS</button>
+              <button class="btn btn-default" id="mode">USB-PD Sink</button>
+              <button class="btn btn-warning" id="normal_dfu">Goto DFU</button>
             </div>
             <div class="col-md-8">
               <div class="alert alert-warning" role="alert" id="status">Uninitialized</div>
@@ -47,11 +52,44 @@
         </div>
       </div>
       <div class="panel panel-default">
-        <div class="panel-heading"><h3 class="panel-title">Console</h3></div>
         <div class="panel-body">
-          <textarea id="log" class="form-control" style="resize: vertical;" readonly cols="120" rows="50"></textarea>
-          <textarea id="command" class="form-control" cols="120" rows="1" placeholder="command to send" autofocus></textarea>
-          <p><a href="https://chromium.googlesource.com/chromiumos/webtwinkie/+/master/README.md" style="font-size:80%">
+          <ul class="nav nav-tabs">
+            <li role="presentation"><a href="#" id="sw_sniffer">Sniffer</a></li>
+            <li role="presentation" class="active"><a href="#" id="sw_console">Console</a></li>
+            <li role="presentation"><a href="#" id="sw_fwupdate">FW update</a></li>
+          </ul>
+
+          <!-- Tab panes -->
+          <div class="tab-content">
+            <div role="tabpanel" class="tab-pane" id="tab_sniffer">
+              <button class="btn btn-default" id="auto">auto tracing</button>
+              <span class="label label-info">VBUS</span>
+              <span id="vbus_mv">9999</span>&nbsp;<span>mV</span>&nbsp;<span id="vbus_ma">9999</span>&nbsp;<span>mA</span>
+              <table class="table table-condensed" id="sniffer_log">
+                <tr><th>#</th><th>time</th><th>type</th><th>packet</th><th>desc.</th></tr>
+              </table>
+            </div>
+            <div role="tabpanel" class="tab-pane active" id="tab_console">
+             <textarea id="log" class="form-control" style="resize: vertical;" readonly cols="120" rows="25"></textarea>
+             <textarea id="command" class="form-control" cols="120" rows="1" placeholder="command to send" autofocus></textarea>
+            </div>
+            <div role="tabpanel" class="tab-pane" id="tab_fwupdate">
+              <div class="row">
+                <div class="col-md-2"><button class="btn btn-info" id="fw_backup">Backup current FW</button></div>
+                <div class="col-md-2"><button class="btn btn-warning" id="fw_leave">Go back to normal mode</button></div>
+              </div>
+              <table class="table table-condensed table-hover">
+                <tr><th>Type</th><th>Description</th><th>name</th><th></th></tr>
+                <tr class="success"><td>Release</td><td>twinkie_v1.11.15</td><td>ec.bin</td><td><button class="btn btn-xs btn-danger" id="fw_update">flash</button></td></tr>
+                <tr class="active"><td>Testing</td><td>twinkie_v1.11.xx</td><td>ec.bin</td><td></td></tr>
+                <tr class="warning"><td>Uploaded</td><td>00:00:04.3333</td><td>ec.bin</td><td><button type="button" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></td></tr>
+                <tr class="info"><td>Backup</td><td>00:02:04.2000</td><td>ec.bin</td><td><button class="btn btn-xs btn-danger" id="fw_update">flash</button><button type="button" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></td></td></tr>
+              </table>
+               <input type="file" name='upload_img' id='upload_img'>
+              <a href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" download="toto.png" >Download</a>
+            </div>
+          </div>
+          <p><a href="https://chromium.googlesource.com/chromiumos/twebkie/+/master/README.md" style="font-size:80%">
             more about the Twinkie WebUSB tool
           </a></p>
         </div>
diff --git a/tool.js b/tool.js
index c26a880..93cb981 100644
--- a/tool.js
+++ b/tool.js
@@ -4,75 +4,355 @@
 
 'use strict';
 
-document.addEventListener('DOMContentLoaded', event => {
-  var connectButton = document.getElementById('connect');
-  var statusDisplay = document.getElementById('status');
-  var vbusButton = document.getElementById('vbus');
-  var cmdLine = document.getElementById('command');
-  var textLog = document.getElementById('log');
-  var port;
+var tool = {};
+var ui = null;
 
-  function connect() {
-    port.connect().then(() => {
-      statusDisplay.textContent = '';
-      connectButton.textContent = 'Disconnect';
+tool.FirmwareMode = {
+  DISCONNECTED: 'Disconnected',
+  SNIFFER: 'Sniffer',
+  SINK: 'USB-PD Sink',
+  DFU: 'DFU',
+};
 
-      port.onReceive = data => {
-        var dec = new TextDecoder();
-        textLog.value += dec.decode(data);
-        textLog.scrollTop = textLog.scrollHeight;
+tool.TabList = ['sniffer', 'console', 'fwupdate'];
+
+tool.UI = function() {
+  this.mode_ = tool.FirmwareMode.DISCONNECTED;
+  this.cmdline_ = document.getElementById('command');
+  this.title_ = document.getElementById('title');
+  this.status_ = document.getElementById('status');
+  this.text_log_ = document.getElementById('log');
+  var sniffer_log = document.getElementById('sniffer_log');
+  this.sniffer_table_ = sniffer_log.getElementsByTagName('tbody')[0];
+  this.connect_button_ = document.getElementById('connect');
+  this.mode_button_ = document.getElementById('mode');
+  this.vbus_display_ = [document.getElementById('vbus_mv'),
+                        document.getElementById('vbus_ma')];
+  this.store_ = new store.FwStore();
+  this.twinkie_ = null;
+  this.dfu_ = null;
+  this.setMode(tool.FirmwareMode.DISCONNECTED);
+  this.store_.init();
+  this.last_vbus = [0, 0];
+  this.sniffer_index = 0;
+
+  this.addListeners();
+};
+
+var diffmV = 500;
+var diffmA = 400;
+
+/* eslint-disable max-len */
+tool.UI.prototype.addListeners = function() {
+  navigator.usb.onconnect = this.onUsbConnect.bind(this);
+  navigator.usb.ondisconnect = this.onUsbDisconnect.bind(this);
+
+  this.cmdline_.addEventListener('input', this.onCommandInput.bind(this));
+  this.connect_button_.addEventListener('click', this.onConnectClick.bind(this));
+  this.mode_button_.addEventListener('click', this.onFwMode.bind(this));
+  document.getElementById('vbus').addEventListener('click', this.onVbusClick.bind(this));
+  document.getElementById('auto').addEventListener('click', this.onAutoClick.bind(this));
+  document.getElementById('normal_dfu').addEventListener('click', this.onDfuSwitch.bind(this));
+  document.getElementById('fw_update').addEventListener('click', this.onFwUpdateClick.bind(this));
+  document.getElementById('fw_backup').addEventListener('click', this.onFwBackupClick.bind(this));
+  document.getElementById('fw_leave').addEventListener('click', this.onFwLeaveClick.bind(this));
+
+  document.getElementById('sw_sniffer').addEventListener('click', this.tabSwitch.bind(this, 'sniffer'));
+  document.getElementById('sw_console').addEventListener('click', this.tabSwitch.bind(this, 'console'));
+  document.getElementById('sw_fwupdate').addEventListener('click', this.tabSwitch.bind(this, 'fwupdate'));
+};
+/* eslint-enable max-len */
+
+tool.UI.prototype.setMode = function(mode) {
+  this.mode_ = mode;
+  this.title_.innerHTML = 'Twebkie: <b>' + mode + '</b>';
+  if (mode === tool.FirmwareMode.SNIFFER) {
+    this.mode_button_.textContent = tool.FirmwareMode.SINK;
+  } else if (mode === tool.FirmwareMode.SINK) {
+    this.mode_button_.textContent = tool.FirmwareMode.SNIFFER;
+  }
+
+  if (mode === tool.FirmwareMode.DISCONNECTED) {
+    this.connect_button_.textContent = 'Connect';
+  } else {
+    this.connect_button_.textContent = 'Disconnect';
+  }
+};
+
+tool.UI.prototype.setStatus = function(stat) {
+    this.status_.textContent = stat;
+};
+
+tool.UI.prototype.tabSwitch = function(select_tab) {
+  for (var t in tool.TabList) {
+    var tab = tool.TabList[t];
+    var tabHead = document.getElementById('sw_'+tab);
+    var tabPane = document.getElementById('tab_'+tab);
+    if (tab == select_tab) {
+      tabHead.parentNode.classList.add("active");
+      tabPane.classList.add("active");
+    } else {
+      tabHead.parentNode.classList.remove("active");
+      tabPane.classList.remove("active");
+    }
+  }
+};
+
+tool.LogType = {
+  VBUS: 'active',
+  GOODCRC: 'warning',
+  PDSRC: 'success',
+  PDSNK: 'info',
+  OTHER: 'danger',
+};
+
+tool.UI.prototype.snifferLog = function(logt, role, packet, desc) {
+  var now = new Date(Date.now());
+  var iso = now.toISOString();
+  var vbus_row = this.sniffer_table_.insertRow(-1);
+  vbus_row.classList.add(logt);
+  vbus_row.insertCell(0).innerHTML = this.sniffer_index++;
+  vbus_row.insertCell(1).innerHTML = iso.slice(iso.indexOf('T') + 1, -1);
+  vbus_row.insertCell(2).innerHTML = role;
+  vbus_row.insertCell(3).innerHTML = packet;
+  vbus_row.insertCell(4).innerHTML = desc;
+};
+
+tool.UI.prototype.doConnect = function(usb_dev) {
+  var tw = new twinkie.Dongle(usb_dev);
+  tw.connect().then(() => {
+    this.twinkie_ = tw;
+    this.setStatus('Firmware version: ' + this.twinkie_.fwVersion);
+    if (this.twinkie_.fwCopy === 'RW') {
+      this.setMode(tool.FirmwareMode.SINK);
+    } else {
+      this.setMode(tool.FirmwareMode.SNIFFER);
+    }
+
+    this.twinkie_.onVBusMeasure = vbus => {
+      var mV = vbus[0];
+      var mA = vbus[1];
+      this.vbus_display_[0].textContent = mV;
+      this.vbus_display_[1].textContent = mA;
+      if ((Math.abs(this.last_vbus[0] - mV) > diffmV) ||
+          (Math.abs(this.last_vbus[1] - mA) > diffmA)) {
+        this.last_vbus[0] = mV;
+        this.last_vbus[1] = mA;
+        // Log significant VBUS variations.
+        this.snifferLog(tool.LogType.VBUS, 'VBUS',
+                        String(mV) + ' mV ' + String(mA) + ' mA', '');
       }
-      port.onReceiveError = error => {
-        console.error(error);
+    };
+
+    this.twinkie_.onPdPacket = (timestamp, head, payload) => {
+      var pkt = new decoder.PDPacket(head, payload);
+      var logtype = pkt.isSource() ? tool.LogType.PDSRC : tool.LogType.PDSNK;
+      if (pkt.isGoodCRC()) {
+        logtype = tool.LogType.GOODCRC;
       }
-    }, error => {
-      statusDisplay.textContent = error;
+      this.snifferLog(logtype, pkt.fullRole(),
+                      pkt.packetType(), pkt.description());
+    };
+    this.twinkie_.onReceiveError = error => {
+      console.error(error);
+    };
+  }, error => {
+    this.setStatus(error);
+  });
+};
+
+tool.UI.prototype.doDisconnect = function(active) {
+  if (active) {
+    if (this.twinkie_) {
+      this.twinkie_.disconnect();
+    }
+    if (this.dfu_) {
+      this.dfu_.disconnect();
+    }
+  }
+  this.setStatus('--- disconnected ---');
+  this.setMode(tool.FirmwareMode.DISCONNECTED);
+  this.twinkie_ = null;
+  this.dfu_ = null;
+};
+
+tool.UI.prototype.doConnectDFU = function(usb_dev) {
+  var bootloader = new dfu.Dongle(usb_dev);
+  bootloader.connect().then(() => {
+    this.dfu_ = bootloader;
+    this.setStatus('STM32 DFU mode');
+    this.setMode(tool.FirmwareMode.DFU);
+  }, error => {
+    this.setStatus(error);
+  });
+};
+
+tool.UI.prototype.doConnectDevices = function(devices) {
+  var twinkies = devices.filter(twinkie.isTwinkie);
+  var bootloaders = devices.filter(dfu.isDFU);
+  if (twinkies.length === 0 && bootloaders.length === 0) {
+    this.setStatus('No device found.');
+  } else {
+    console.log('reconnection ...');
+    this.setStatus('Connecting...');
+    if (twinkies.length) {
+      this.doConnect(twinkies[0]);
+    } else {
+      this.doConnectDFU(bootloaders[0]);
+    }
+  }
+};
+
+tool.UI.prototype.tryReconnect = function() {
+  navigator.usb.getDevices().then(devices => {
+    this.doConnectDevices(devices);
+  });
+};
+
+tool.UI.prototype.onUsbConnect = function(evt) {
+  console.log('USB Connection');
+  console.log(evt);
+
+  this.doConnectDevices([evt.device]);
+};
+
+tool.UI.prototype.onUsbDisconnect = function(evt) {
+  console.log('USB Disconnection');
+  console.log(evt);
+  if ((this.twinkie_ && evt.device === this.twinkie_.device_) ||
+      (this.dfu_ && evt.device === this.dfu_.device_)) {
+    this.doDisconnect(false);
+  }
+};
+
+tool.UI.prototype.onConnectClick = function() {
+  if (this.twinkie_ || this.dfu_) {
+    this.doDisconnect(true);
+  } else {
+    const filters = [
+      {
+        // Twinkie EC firmware
+        vendorId: twinkie.USB_VENDOR_ID,
+        productId: twinkie.USB_PRODUCT_ID,
+      },
+      {
+        // STM32 DFU mode
+        vendorId: dfu.STM_VENDOR_ID,
+        productId: dfu.STM_PRODUCT_ID,
+      },
+    ];
+    return navigator.usb.requestDevice({filters: filters}).then(usb_dev => {
+      this.doConnectDevices([usb_dev]);
+      this.cmdline_.focus();
+    }).catch(error => {
+      this.setStatus(error);
     });
   }
+};
 
-  function onVBUS() {
-    if (!port) {
-      return;
-    }
-
-    port.send("tw vbus\n");
-    cmdLine.focus();
+tool.UI.prototype.onFwMode = function() {
+  if (!this.twinkie_) {
+    return;
   }
 
-  vbusButton.addEventListener('click', onVBUS);
+  if (this.mode_ === tool.FirmwareMode.SNIFFER) {
+    this.twinkie_.goPartition('rw');
+  } else if (this.mode_ === tool.FirmwareMode.SINK) {
+    this.twinkie_.goPartition('ro');
+  }
+};
 
-  connectButton.addEventListener('click', function() {
-    if (port) {
-      port.disconnect();
-      connectButton.textContent = 'Connect';
-      statusDisplay.textContent = '';
-      port = null;
-    } else {
-      twinkie.requestDongle().then(selectedDongle => {
-        port = selectedDongle;
-        connect();
-        cmdLine.focus();
-      }).catch(error => {
-        statusDisplay.textContent = error;
-      });
+tool.UI.prototype.onDfuSwitch = function() {
+  if (!this.twinkie_) {
+    return;
+  }
+
+  this.twinkie_.send('dfu');
+  // TBD
+};
+
+tool.UI.prototype.onVbusClick = function() {
+  if (!this.twinkie_) {
+    return;
+  }
+
+  this.twinkie_.send('tw vbus');
+
+  this.twinkie_.readVbus().then(vbus => console.log(vbus));
+  this.cmdline_.focus();
+};
+
+tool.UI.prototype.onAutoClick = function() {
+  if (!this.twinkie_) {
+    return;
+  }
+  this.twinkie_.trace_mode(true);
+};
+
+tool.UI.prototype.onFwUpdateClick = function() {
+  if (!this.dfu_) {
+    this.setStatus('No device in DFU mode');
+    return;
+  }
+
+  this.setStatus('Updating...');
+  this.dfu_.updateFw().then(() => {
+    console.log('DONE');
+    this.setStatus('Updating done.');
+  }).catch(error => {
+    console.log(error);
+    this.setStatus(error);
+  });
+};
+
+tool.UI.prototype.onCommandInput = function() {
+  var cmd = this.cmdline_.value;
+  if (cmd.endsWith('\n')) {
+    this.cmdline_.value = '';
+    if (this.twinkie_) {
+      this.twinkie_.command(cmd.slice(0, -1)).then(function(txt) {
+        txt = cmd + txt + '\n> ';
+        this.text_log_.value += txt;
+        this.text_log_.scrollTop = this.text_log_.scrollHeight;
+      }.bind(this));
     }
-  });
+  }
+};
 
-  cmdLine.addEventListener('input', function() {
-    var cmd = cmdLine.value;
-    if (cmd.endsWith('\n') && port) {
-      port.send(cmd);
-      cmdLine.value = "";
-   }
-  });
+tool.UI.prototype.onFwBackupClick = function() {
+  if (!this.dfu_) {
+    this.setStatus('No device in DFU mode');
+    return;
+  }
 
-  twinkie.getDongles().then(ports => {
-    if (ports.length == 0) {
-      statusDisplay.textContent = 'No device found.';
-    } else {
-      statusDisplay.textContent = 'Connecting...';
-      port = ports[0];
-      connect();
-    }
+  this.setStatus('Backup...');
+  this.dfu_.backupFw().then(img => {
+    console.log('DONE ' + img.length + ' bytes');
+    this.setStatus('Backup done.');
+  }).catch(error => {
+    this.setStatus(error);
   });
+};
+
+tool.UI.prototype.onFwLeaveClick = function() {
+  if (!this.dfu_) {
+    this.setStatus('No device in DFU mode');
+    return;
+  }
+
+  this.dfu_.gotoIdle()
+  // .then(() => selected.stmLeaveDfu())
+  .then(() => this.dfu_.stmReadUnprotect())
+  .then(() => {
+    console.log('DONE');
+  }).catch(error => {
+    console.log(error);
+    this.setStatus(error);
+  });
+};
+
+
+document.addEventListener('DOMContentLoaded', () => {
+  ui = new tool.UI();
+  ui.tryReconnect();
 });
diff --git a/twinkie.js b/twinkie.js
index f1b0924..b874661 100644
--- a/twinkie.js
+++ b/twinkie.js
@@ -6,32 +6,36 @@
 
 'use strict';
 
-twinkie.getDongles = function() {
-  return navigator.usb.getDevices().then(devices => {
-    return devices.map(device => new twinkie.Dongle(device));
-  });
-};
+// USB identifiers.
+twinkie.USB_VENDOR_ID = 0x18d1;
+twinkie.USB_PRODUCT_ID = 0x500a;
 
-twinkie.requestDongle = function() {
-  const filters = [
-    { 'vendorId': 0x18d1, 'productId': 0x500a }, // Twinkie EC firmware
-    { 'vendorId': 0x0483, 'productId': 0xdf11 }, // STM32 DFU mode
-  ];
-  return navigator.usb.requestDevice({ 'filters': filters }).then(
-    device => new twinkie.Dongle(device)
-  );
+twinkie.EP_SNIFFER = 3;
+
+twinkie.isTwinkie = function(device) {
+  return device.vendorId === twinkie.USB_VENDOR_ID &&
+         device.productId === twinkie.USB_PRODUCT_ID;
 };
 
 twinkie.Dongle = function(device) {
   this.device_ = device;
+  this.enc_ = new TextEncoder('utf-8');
+  this.dec_ = new TextDecoder();
+  this.vbus_re_ = new RegExp('VBUS = (-?[0-9]+) mV ; (-?[0-9]+) mA');
+  this.tracing_mode_ = false;
   console.log(device);
 };
 
 twinkie.Dongle.prototype.connect = function() {
   let readLoop = () => {
-    this.device_.transferIn(this.epIn_, 64).then(result => {
-      if (result.data.byteLength)
-        this.onReceive(result.data);
+    this.device_.transferIn(twinkie.EP_SNIFFER, 64).then(result => {
+      if (this.tracing_mode_ && result.data.byteLength) {
+        var pkt = new Uint32Array(result.data.buffer);
+        var head = pkt[2] & 0xffff;
+        var cnt = (head >> 12) & 0x7;
+        var tstamp = pkt[0];
+        this.onPdPacket(tstamp, head, pkt.slice(3, 3 + cnt));
+      }
       readLoop();
     }, error => {
       this.onReceiveError(error);
@@ -41,36 +45,43 @@
   return this.device_.open()
       .then(() => {
         if (this.device_.configuration === null) {
-          return this.device_.selectConfiguration(0);
+          return this.device_.selectConfiguration(1);
         }
       })
       .then(() => {
         // Default to the first interface if there is no vendor one
         var ifaces = this.device_.configuration.interfaces;
         var vendor = ifaces[0];
-        for (var iface in ifaces) {
-          if (ifaces[iface].alternates[0].interfaceClass == 255) {
+        for (var iface = 0; iface < ifaces.length; iface++) {
+          if (ifaces[iface].alternates[0].interfaceClass === 255 &&
+              ifaces[iface].alternates[0].interfaceSubclass === 0) {
             vendor = ifaces[iface];
             break;
           }
         }
-        console.log(vendor);
         this.ifaceNum_ = vendor.interfaceNumber;
         var eps = vendor.alternates[0].endpoints;
         this.epIn_ = 1;
         this.epOut_ = 1;
-        for (var ep in eps) {
-          if (eps[ep].direction == "in" && eps[ep].type == "bulk")
+        for (var ep = 0; ep < eps.length; ep++) {
+          if (eps[ep].direction === 'in' && eps[ep].type === 'bulk') {
             this.epIn_ = eps[ep].endpointNumber;
-          if (eps[ep].direction == "out" && eps[ep].type == "bulk")
+          } else if (eps[ep].direction === 'out' && eps[ep].type === 'bulk') {
             this.epOut_ = eps[ep].endpointNumber;
+          }
         }
       })
       .then(() => this.device_.claimInterface(this.ifaceNum_))
-      .then(() => {
-        this.send("help\n");
+      .then(() => this.device_.claimInterface(1))
+      .then(() => this.getVersion())
+      .then(ver => {
+        console.log('Version: ' + ver);
+        this.fwVersion = ver;
+        return this.getCopy();
       })
-      .then(() => {
+      .then(cpy => {
+        console.log('FW Copy: ' + cpy);
+        this.fwCopy = cpy;
         readLoop();
       });
 };
@@ -80,6 +91,102 @@
 };
 
 twinkie.Dongle.prototype.send = function(text) {
-  var enc = new TextEncoder("utf-8");
-  return this.device_.transferOut(this.epOut_, enc.encode(text).buffer);
+  return this.device_.transferOut(this.epOut_, this.enc_.encode(text).buffer);
+};
+
+twinkie.Dongle.prototype.command = function(cmd) {
+  return new Promise(function(resolve, reject) {
+    var resp = '';
+
+    let readResponse = () => {
+      this.device_.transferIn(this.epIn_, 64).then(result => {
+        if (result.status !== 'ok') {
+          reject('Read failed: ' + result.status);
+        }
+        var len = result.data.byteLength;
+        if (len) {
+          resp += this.dec_.decode(result.data);
+        }
+        if (len === 64) {
+          /* more to come */
+          readResponse();
+        } else {
+          /* response fully received */
+          resolve(resp);
+        }
+      }, error => {
+        reject('Read error: ' + error);
+      });
+    };
+
+    this.device_.transferOut(this.epOut_, this.enc_.encode(cmd).buffer)
+        .then(() => {
+          readResponse();
+        });
+  }.bind(this));
+};
+
+twinkie.Dongle.prototype.vbusDone = function(info) {
+  var mtch = info.match(this.vbus_re_);
+  if (mtch && mtch.length === 3) {
+    var vbus_pair = [mtch[1], mtch[2]];
+    this.onVBusMeasure(vbus_pair);
+  }
+  if (this.tracing_mode_ === true) {
+    this.command('tw vbus').then(this.vbusDone.bind(this));
+  }
+};
+
+twinkie.Dongle.prototype.trace_mode = function(enable) {
+  if (this.tracing_mode_ === enable) {
+    return;
+  }
+
+  if (enable) {
+    this.send('tw trace raw');
+  } else {
+    this.send('tw trace off');
+  }
+  this.tracing_mode_ = enable;
+  this.command('tw vbus').then(this.vbusDone.bind(this));
+};
+
+twinkie.Dongle.prototype.getVersion = function() {
+  return this.command('version').then(function(ver) {
+    var versionRE = new RegExp('^Build:\\s+(\\S+)$', 'm');
+    var mtch = ver.match(versionRE);
+    if (mtch && mtch.length === 2) {
+      return mtch[1];
+    }
+
+    return 'unknown';
+  });
+};
+
+twinkie.Dongle.prototype.getCopy = function() {
+  return this.command('sysinfo').then(function(info) {
+    var copyRE = new RegExp('^Copy:\\s+(\\S+)$', 'm');
+    var mtch = info.match(copyRE);
+    if (mtch && mtch.length === 2) {
+      return mtch[1];
+    }
+
+    return 'RO';
+  });
+};
+
+twinkie.Dongle.prototype.goPartition = function(part) {
+  this.send('sysjump ' + part);
+};
+
+twinkie.Dongle.prototype.readVbus = function() {
+  return this.command('tw vbus').then(function(info) {
+    var mtch = info.match(this.vbus_re_);
+    if (mtch && mtch.length === 3) {
+      var vbus_pair = [mtch[1], mtch[2]];
+      this.onVBusMeasure(vbus_pair);
+      return vbus_pair;
+    }
+    return [0, 0];
+  }.bind(this));
 };