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> <span>mV</span> <span id="vbus_ma">9999</span> <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">×</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">×</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));
};