add tools page and embed rnode flasher

This commit is contained in:
liamcottle
2024-12-18 23:15:27 +13:00
parent 8cfb5449fd
commit bb62b3331f
17 changed files with 21590 additions and 3 deletions

View File

@@ -111,9 +111,10 @@ app.whenReady().then(async () => {
// open external links in default web browser instead of electron
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
// open internal MeshChat urls starting with http://localhost in electron
// this is needed for pages such as call.html that open in a new window
if(url.startsWith("http://localhost")){
// we want to open call.html in a new electron window
// but all other target="_blank" links should open in the system web browser
// we don't want /rnode-flasher/index.html to open in electron, otherwise user can't select usb devices...
if(url.startsWith("http://localhost") && url.includes("/call.html")){
return {
action: "allow",
};

View File

@@ -103,6 +103,18 @@
</SidebarLink>
</li>
<!-- tools -->
<li>
<SidebarLink :to="{ name: 'tools' }">
<template v-slot:icon>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z" />
</svg>
</template>
<template v-slot:text>Tools</template>
</SidebarLink>
</li>
<!-- settings -->
<li>
<SidebarLink :to="{ name: 'settings' }">

View File

@@ -0,0 +1,40 @@
<template>
<div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px] dark:bg-zinc-950">
<div class="overflow-y-auto space-y-2 p-2">
<!-- appearance -->
<div class="bg-white dark:bg-zinc-800 rounded shadow">
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-gray-200 p-2 font-semibold">Tools</div>
<div class="dark:divide-zinc-700 text-gray-900 dark:text-gray-100 p-2">
A collection of useful tools bundled with MeshChat
</div>
</div>
<!-- rnode flasher -->
<a target="_blank" href="/rnode-flasher/index.html" class="group flex bg-white dark:bg-zinc-800 p-2 rounded shadow hover:bg-gray-50 dark:hover:bg-zinc-700">
<div class="mr-2">
<div class="flex bg-gray-300 text-white rounded shadow">
<img src="/rnode-flasher/reticulum_logo_512.png" class="size-14"/>
</div>
</div>
<div class="my-auto mr-auto dark:text-gray-200">
<div class="font-bold">RNode Flasher</div>
<div class="text-sm">Flash RNode firmware to supported devices.</div>
</div>
<div class="my-auto text-gray-400 group-hover:text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"></path>
</svg>
</div>
</a>
</div>
</div>
</template>
<script>
export default {
name: 'ToolsPage',
}
</script>

View File

@@ -65,6 +65,11 @@ const router = createRouter({
path: '/settings',
component: defineAsyncComponent(() => import("./components/settings/SettingsPage.vue")),
},
{
name: "tools",
path: '/tools',
component: defineAsyncComponent(() => import("./components/tools/ToolsPage.vue")),
},
],
})

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Liam Cottle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,86 @@
# RNode Flasher
A _work-in-progress_ web based firmware flasher for [Reticulum](https://github.com/markqvist/Reticulum) / [RNode_Firmware](https://github.com/markqvist/RNode_Firmware).
- It is written in javascript and uses the [Web Serial APIs](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API).
- It supports putting relevant devices into DFU mode.
- It supports flashing firmware from a zip file.
At this time, it does not support flashing bootloaders or softdevices for the nRF boards.
## How does it work?
I wanted something simple, for flashing RNode firmware to a nRF52 RAK4631 in a web browser.
So, I spent a bit of time working through the source code of [adafruit-nrfutil](https://github.com/adafruit/Adafruit_nRF52_nrfutil) and wrote a javascript implementation of [dfu_transport_serial.py](https://github.com/adafruit/Adafruit_nRF52_nrfutil/blob/master/nordicsemi/dfu/dfu_transport_serial.py)
Generally, you would use the following command to flash a firmware.zip to your device;
```
adafruit-nrfutil dfu serial --package firmware.zip -p /dev/cu.usbmodem14401 -b 115200 -t 1200
```
The [nrf52_dfu_flasher.js](js/nrf52_dfu_flasher.js) in this project implements a javascript, web based version of the above command.
There was an existing package called [pc-nrf-dfu-js](https://github.com/NordicSemiconductor/pc-nrf-dfu-js), however this repo had been archived and didn't appear to support the latest DFU protocol.
## How to use it?
- Open https://liamcottle.github.io/rnode-flasher/ in your web browser.
- Select your device.
- Put your device into DFU mode (for nRF52 boards)
- Select a firmware file and click flash.
- Once flashed, your device should reboot into the new firmware.
- For new devices that have never been provisioned, you should click "Provision" to configure the EEPROM.
- Every time you flash new firmware, you should also click "Set Firmware Hash".
> Note: At this time, firmware hashes for RNode are not automatically configured.
## What is needed to set up a new RNode?
> Note: This is a technical overview of how the RNode device provisioning works.
> Most of this is taken care of by the code base, and this section just makes it easier to understand what is going on.
To set up a new RNode device, you will need to do a few things;
- Obtain supported hardware, such as a RAK4631
- Obtain an RNode firmware file
- Put your device into DFU mode
- Flash the firmware file
- Provision the EEPROM
Once the firmware is flashed to the device, you will need to provision the EEPROM;
- Set firmware hash in eeprom
- Collect device info
- `product`
- `model`
- `hardware_revision`
- `serial_number`
- `made` (unix timestamp of device creation)
- Write device info to eeprom
- Create an MD5 checksum of the device info
- Write 16 byte device info checksum to eeprom
- Sign device info checksum with signing key to use as signature
- Write 128 byte signature to eeprom
- Write `ROM.INFO_LOCK_BYTE` to `ROM.ADDR_INFO_LOCK` in eeprom
- Read eeprom and validate checksums and signatures to ensure all is correct
## TODO
- support configuring eeprom with device signatures and firmware hashes
- support flashing existing firmware files from api
- calculate on air bitrate based on tnc settings
- try using [web-serial-polyfill](https://github.com/google/web-serial-polyfill) to support flashing from Android device?
## License
MIT
## References
- https://github.com/adafruit/Adafruit_nRF52_nrfutil
- https://github.com/adafruit/Adafruit_nRF52_nrfutil/blob/master/nordicsemi/dfu/dfu_transport_serial.py
- https://github.com/markqvist/RNode_Firmware/blob/master/RNode_Firmware.ino
- https://github.com/markqvist/RNode_Firmware/blob/master/Framing.h
- https://github.com/markqvist/RNode_Firmware/blob/master/Utilities.h

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,760 @@
;(function (root, factory) {
if (typeof exports === "object") {
// CommonJS
module.exports = exports = factory();
}
else if (typeof define === "function" && define.amd) {
// AMD
define([], factory);
}
else {
// Global (browser)
root.CryptoJS = factory();
}
}(this, function () {
/**
* CryptoJS core components.
*/
var CryptoJS = CryptoJS || (function (Math, undefined) {
/*
* Local polyfil of Object.create
*/
var create = Object.create || (function () {
function F() {};
return function (obj) {
var subtype;
F.prototype = obj;
subtype = new F();
F.prototype = null;
return subtype;
};
}())
/**
* CryptoJS namespace.
*/
var C = {};
/**
* Library namespace.
*/
var C_lib = C.lib = {};
/**
* Base object for prototypal inheritance.
*/
var Base = C_lib.Base = (function () {
return {
/**
* Creates a new object that inherits from this object.
*
* @param {Object} overrides Properties to copy into the new object.
*
* @return {Object} The new object.
*
* @static
*
* @example
*
* var MyType = CryptoJS.lib.Base.extend({
* field: 'value',
*
* method: function () {
* }
* });
*/
extend: function (overrides) {
// Spawn
var subtype = create(this);
// Augment
if (overrides) {
subtype.mixIn(overrides);
}
// Create default initializer
if (!subtype.hasOwnProperty('init') || this.init === subtype.init) {
subtype.init = function () {
subtype.$super.init.apply(this, arguments);
};
}
// Initializer's prototype is the subtype object
subtype.init.prototype = subtype;
// Reference supertype
subtype.$super = this;
return subtype;
},
/**
* Extends this object and runs the init method.
* Arguments to create() will be passed to init().
*
* @return {Object} The new object.
*
* @static
*
* @example
*
* var instance = MyType.create();
*/
create: function () {
var instance = this.extend();
instance.init.apply(instance, arguments);
return instance;
},
/**
* Initializes a newly created object.
* Override this method to add some logic when your objects are created.
*
* @example
*
* var MyType = CryptoJS.lib.Base.extend({
* init: function () {
* // ...
* }
* });
*/
init: function () {
},
/**
* Copies properties into this object.
*
* @param {Object} properties The properties to mix in.
*
* @example
*
* MyType.mixIn({
* field: 'value'
* });
*/
mixIn: function (properties) {
for (var propertyName in properties) {
if (properties.hasOwnProperty(propertyName)) {
this[propertyName] = properties[propertyName];
}
}
// IE won't copy toString using the loop above
if (properties.hasOwnProperty('toString')) {
this.toString = properties.toString;
}
},
/**
* Creates a copy of this object.
*
* @return {Object} The clone.
*
* @example
*
* var clone = instance.clone();
*/
clone: function () {
return this.init.prototype.extend(this);
}
};
}());
/**
* An array of 32-bit words.
*
* @property {Array} words The array of 32-bit words.
* @property {number} sigBytes The number of significant bytes in this word array.
*/
var WordArray = C_lib.WordArray = Base.extend({
/**
* Initializes a newly created word array.
*
* @param {Array} words (Optional) An array of 32-bit words.
* @param {number} sigBytes (Optional) The number of significant bytes in the words.
*
* @example
*
* var wordArray = CryptoJS.lib.WordArray.create();
* var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]);
* var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6);
*/
init: function (words, sigBytes) {
words = this.words = words || [];
if (sigBytes != undefined) {
this.sigBytes = sigBytes;
} else {
this.sigBytes = words.length * 4;
}
},
/**
* Converts this word array to a string.
*
* @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex
*
* @return {string} The stringified word array.
*
* @example
*
* var string = wordArray + '';
* var string = wordArray.toString();
* var string = wordArray.toString(CryptoJS.enc.Utf8);
*/
toString: function (encoder) {
return (encoder || Hex).stringify(this);
},
/**
* Concatenates a word array to this word array.
*
* @param {WordArray} wordArray The word array to append.
*
* @return {WordArray} This word array.
*
* @example
*
* wordArray1.concat(wordArray2);
*/
concat: function (wordArray) {
// Shortcuts
var thisWords = this.words;
var thatWords = wordArray.words;
var thisSigBytes = this.sigBytes;
var thatSigBytes = wordArray.sigBytes;
// Clamp excess bits
this.clamp();
// Concat
if (thisSigBytes % 4) {
// Copy one byte at a time
for (var i = 0; i < thatSigBytes; i++) {
var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8);
}
} else {
// Copy one word at a time
for (var i = 0; i < thatSigBytes; i += 4) {
thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2];
}
}
this.sigBytes += thatSigBytes;
// Chainable
return this;
},
/**
* Removes insignificant bits.
*
* @example
*
* wordArray.clamp();
*/
clamp: function () {
// Shortcuts
var words = this.words;
var sigBytes = this.sigBytes;
// Clamp
words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8);
words.length = Math.ceil(sigBytes / 4);
},
/**
* Creates a copy of this word array.
*
* @return {WordArray} The clone.
*
* @example
*
* var clone = wordArray.clone();
*/
clone: function () {
var clone = Base.clone.call(this);
clone.words = this.words.slice(0);
return clone;
},
/**
* Creates a word array filled with random bytes.
*
* @param {number} nBytes The number of random bytes to generate.
*
* @return {WordArray} The random word array.
*
* @static
*
* @example
*
* var wordArray = CryptoJS.lib.WordArray.random(16);
*/
random: function (nBytes) {
var words = [];
var r = (function (m_w) {
var m_w = m_w;
var m_z = 0x3ade68b1;
var mask = 0xffffffff;
return function () {
m_z = (0x9069 * (m_z & 0xFFFF) + (m_z >> 0x10)) & mask;
m_w = (0x4650 * (m_w & 0xFFFF) + (m_w >> 0x10)) & mask;
var result = ((m_z << 0x10) + m_w) & mask;
result /= 0x100000000;
result += 0.5;
return result * (Math.random() > .5 ? 1 : -1);
}
});
for (var i = 0, rcache; i < nBytes; i += 4) {
var _r = r((rcache || Math.random()) * 0x100000000);
rcache = _r() * 0x3ade67b7;
words.push((_r() * 0x100000000) | 0);
}
return new WordArray.init(words, nBytes);
}
});
/**
* Encoder namespace.
*/
var C_enc = C.enc = {};
/**
* Hex encoding strategy.
*/
var Hex = C_enc.Hex = {
/**
* Converts a word array to a hex string.
*
* @param {WordArray} wordArray The word array.
*
* @return {string} The hex string.
*
* @static
*
* @example
*
* var hexString = CryptoJS.enc.Hex.stringify(wordArray);
*/
stringify: function (wordArray) {
// Shortcuts
var words = wordArray.words;
var sigBytes = wordArray.sigBytes;
// Convert
var hexChars = [];
for (var i = 0; i < sigBytes; i++) {
var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
hexChars.push((bite >>> 4).toString(16));
hexChars.push((bite & 0x0f).toString(16));
}
return hexChars.join('');
},
/**
* Converts a hex string to a word array.
*
* @param {string} hexStr The hex string.
*
* @return {WordArray} The word array.
*
* @static
*
* @example
*
* var wordArray = CryptoJS.enc.Hex.parse(hexString);
*/
parse: function (hexStr) {
// Shortcut
var hexStrLength = hexStr.length;
// Convert
var words = [];
for (var i = 0; i < hexStrLength; i += 2) {
words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
}
return new WordArray.init(words, hexStrLength / 2);
}
};
/**
* Latin1 encoding strategy.
*/
var Latin1 = C_enc.Latin1 = {
/**
* Converts a word array to a Latin1 string.
*
* @param {WordArray} wordArray The word array.
*
* @return {string} The Latin1 string.
*
* @static
*
* @example
*
* var latin1String = CryptoJS.enc.Latin1.stringify(wordArray);
*/
stringify: function (wordArray) {
// Shortcuts
var words = wordArray.words;
var sigBytes = wordArray.sigBytes;
// Convert
var latin1Chars = [];
for (var i = 0; i < sigBytes; i++) {
var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
latin1Chars.push(String.fromCharCode(bite));
}
return latin1Chars.join('');
},
/**
* Converts a Latin1 string to a word array.
*
* @param {string} latin1Str The Latin1 string.
*
* @return {WordArray} The word array.
*
* @static
*
* @example
*
* var wordArray = CryptoJS.enc.Latin1.parse(latin1String);
*/
parse: function (latin1Str) {
// Shortcut
var latin1StrLength = latin1Str.length;
// Convert
var words = [];
for (var i = 0; i < latin1StrLength; i++) {
words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8);
}
return new WordArray.init(words, latin1StrLength);
}
};
/**
* UTF-8 encoding strategy.
*/
var Utf8 = C_enc.Utf8 = {
/**
* Converts a word array to a UTF-8 string.
*
* @param {WordArray} wordArray The word array.
*
* @return {string} The UTF-8 string.
*
* @static
*
* @example
*
* var utf8String = CryptoJS.enc.Utf8.stringify(wordArray);
*/
stringify: function (wordArray) {
try {
return decodeURIComponent(escape(Latin1.stringify(wordArray)));
} catch (e) {
throw new Error('Malformed UTF-8 data');
}
},
/**
* Converts a UTF-8 string to a word array.
*
* @param {string} utf8Str The UTF-8 string.
*
* @return {WordArray} The word array.
*
* @static
*
* @example
*
* var wordArray = CryptoJS.enc.Utf8.parse(utf8String);
*/
parse: function (utf8Str) {
return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
}
};
/**
* Abstract buffered block algorithm template.
*
* The property blockSize must be implemented in a concrete subtype.
*
* @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0
*/
var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({
/**
* Resets this block algorithm's data buffer to its initial state.
*
* @example
*
* bufferedBlockAlgorithm.reset();
*/
reset: function () {
// Initial values
this._data = new WordArray.init();
this._nDataBytes = 0;
},
/**
* Adds new data to this block algorithm's buffer.
*
* @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8.
*
* @example
*
* bufferedBlockAlgorithm._append('data');
* bufferedBlockAlgorithm._append(wordArray);
*/
_append: function (data) {
// Convert string to WordArray, else assume WordArray already
if (typeof data == 'string') {
data = Utf8.parse(data);
}
// Append
this._data.concat(data);
this._nDataBytes += data.sigBytes;
},
/**
* Processes available data blocks.
*
* This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype.
*
* @param {boolean} doFlush Whether all blocks and partial blocks should be processed.
*
* @return {WordArray} The processed data.
*
* @example
*
* var processedData = bufferedBlockAlgorithm._process();
* var processedData = bufferedBlockAlgorithm._process(!!'flush');
*/
_process: function (doFlush) {
// Shortcuts
var data = this._data;
var dataWords = data.words;
var dataSigBytes = data.sigBytes;
var blockSize = this.blockSize;
var blockSizeBytes = blockSize * 4;
// Count blocks ready
var nBlocksReady = dataSigBytes / blockSizeBytes;
if (doFlush) {
// Round up to include partial blocks
nBlocksReady = Math.ceil(nBlocksReady);
} else {
// Round down to include only full blocks,
// less the number of blocks that must remain in the buffer
nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
}
// Count words ready
var nWordsReady = nBlocksReady * blockSize;
// Count bytes ready
var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes);
// Process blocks
if (nWordsReady) {
for (var offset = 0; offset < nWordsReady; offset += blockSize) {
// Perform concrete-algorithm logic
this._doProcessBlock(dataWords, offset);
}
// Remove processed words
var processedWords = dataWords.splice(0, nWordsReady);
data.sigBytes -= nBytesReady;
}
// Return processed words
return new WordArray.init(processedWords, nBytesReady);
},
/**
* Creates a copy of this object.
*
* @return {Object} The clone.
*
* @example
*
* var clone = bufferedBlockAlgorithm.clone();
*/
clone: function () {
var clone = Base.clone.call(this);
clone._data = this._data.clone();
return clone;
},
_minBufferSize: 0
});
/**
* Abstract hasher template.
*
* @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits)
*/
var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({
/**
* Configuration options.
*/
cfg: Base.extend(),
/**
* Initializes a newly created hasher.
*
* @param {Object} cfg (Optional) The configuration options to use for this hash computation.
*
* @example
*
* var hasher = CryptoJS.algo.SHA256.create();
*/
init: function (cfg) {
// Apply config defaults
this.cfg = this.cfg.extend(cfg);
// Set initial values
this.reset();
},
/**
* Resets this hasher to its initial state.
*
* @example
*
* hasher.reset();
*/
reset: function () {
// Reset data buffer
BufferedBlockAlgorithm.reset.call(this);
// Perform concrete-hasher logic
this._doReset();
},
/**
* Updates this hasher with a message.
*
* @param {WordArray|string} messageUpdate The message to append.
*
* @return {Hasher} This hasher.
*
* @example
*
* hasher.update('message');
* hasher.update(wordArray);
*/
update: function (messageUpdate) {
// Append
this._append(messageUpdate);
// Update the hash
this._process();
// Chainable
return this;
},
/**
* Finalizes the hash computation.
* Note that the finalize operation is effectively a destructive, read-once operation.
*
* @param {WordArray|string} messageUpdate (Optional) A final message update.
*
* @return {WordArray} The hash.
*
* @example
*
* var hash = hasher.finalize();
* var hash = hasher.finalize('message');
* var hash = hasher.finalize(wordArray);
*/
finalize: function (messageUpdate) {
// Final message update
if (messageUpdate) {
this._append(messageUpdate);
}
// Perform concrete-hasher logic
var hash = this._doFinalize();
return hash;
},
blockSize: 512/32,
/**
* Creates a shortcut function to a hasher's object interface.
*
* @param {Hasher} hasher The hasher to create a helper for.
*
* @return {Function} The shortcut function.
*
* @static
*
* @example
*
* var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256);
*/
_createHelper: function (hasher) {
return function (message, cfg) {
return new hasher.init(cfg).finalize(message);
};
},
/**
* Creates a shortcut function to the HMAC's object interface.
*
* @param {Hasher} hasher The hasher to use in this HMAC helper.
*
* @return {Function} The shortcut function.
*
* @static
*
* @example
*
* var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256);
*/
_createHmacHelper: function (hasher) {
return function (message, key) {
return new C_algo.HMAC.init(hasher, key).finalize(message);
};
}
});
/**
* Algorithm namespace.
*/
var C_algo = C.algo = {};
return C;
}(Math));
return CryptoJS;
}));

View File

@@ -0,0 +1,268 @@
;(function (root, factory) {
if (typeof exports === "object") {
// CommonJS
module.exports = exports = factory(require("./core"));
}
else if (typeof define === "function" && define.amd) {
// AMD
define(["./core"], factory);
}
else {
// Global (browser)
factory(root.CryptoJS);
}
}(this, function (CryptoJS) {
(function (Math) {
// Shortcuts
var C = CryptoJS;
var C_lib = C.lib;
var WordArray = C_lib.WordArray;
var Hasher = C_lib.Hasher;
var C_algo = C.algo;
// Constants table
var T = [];
// Compute constants
(function () {
for (var i = 0; i < 64; i++) {
T[i] = (Math.abs(Math.sin(i + 1)) * 0x100000000) | 0;
}
}());
/**
* MD5 hash algorithm.
*/
var MD5 = C_algo.MD5 = Hasher.extend({
_doReset: function () {
this._hash = new WordArray.init([
0x67452301, 0xefcdab89,
0x98badcfe, 0x10325476
]);
},
_doProcessBlock: function (M, offset) {
// Swap endian
for (var i = 0; i < 16; i++) {
// Shortcuts
var offset_i = offset + i;
var M_offset_i = M[offset_i];
M[offset_i] = (
(((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) |
(((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00)
);
}
// Shortcuts
var H = this._hash.words;
var M_offset_0 = M[offset + 0];
var M_offset_1 = M[offset + 1];
var M_offset_2 = M[offset + 2];
var M_offset_3 = M[offset + 3];
var M_offset_4 = M[offset + 4];
var M_offset_5 = M[offset + 5];
var M_offset_6 = M[offset + 6];
var M_offset_7 = M[offset + 7];
var M_offset_8 = M[offset + 8];
var M_offset_9 = M[offset + 9];
var M_offset_10 = M[offset + 10];
var M_offset_11 = M[offset + 11];
var M_offset_12 = M[offset + 12];
var M_offset_13 = M[offset + 13];
var M_offset_14 = M[offset + 14];
var M_offset_15 = M[offset + 15];
// Working varialbes
var a = H[0];
var b = H[1];
var c = H[2];
var d = H[3];
// Computation
a = FF(a, b, c, d, M_offset_0, 7, T[0]);
d = FF(d, a, b, c, M_offset_1, 12, T[1]);
c = FF(c, d, a, b, M_offset_2, 17, T[2]);
b = FF(b, c, d, a, M_offset_3, 22, T[3]);
a = FF(a, b, c, d, M_offset_4, 7, T[4]);
d = FF(d, a, b, c, M_offset_5, 12, T[5]);
c = FF(c, d, a, b, M_offset_6, 17, T[6]);
b = FF(b, c, d, a, M_offset_7, 22, T[7]);
a = FF(a, b, c, d, M_offset_8, 7, T[8]);
d = FF(d, a, b, c, M_offset_9, 12, T[9]);
c = FF(c, d, a, b, M_offset_10, 17, T[10]);
b = FF(b, c, d, a, M_offset_11, 22, T[11]);
a = FF(a, b, c, d, M_offset_12, 7, T[12]);
d = FF(d, a, b, c, M_offset_13, 12, T[13]);
c = FF(c, d, a, b, M_offset_14, 17, T[14]);
b = FF(b, c, d, a, M_offset_15, 22, T[15]);
a = GG(a, b, c, d, M_offset_1, 5, T[16]);
d = GG(d, a, b, c, M_offset_6, 9, T[17]);
c = GG(c, d, a, b, M_offset_11, 14, T[18]);
b = GG(b, c, d, a, M_offset_0, 20, T[19]);
a = GG(a, b, c, d, M_offset_5, 5, T[20]);
d = GG(d, a, b, c, M_offset_10, 9, T[21]);
c = GG(c, d, a, b, M_offset_15, 14, T[22]);
b = GG(b, c, d, a, M_offset_4, 20, T[23]);
a = GG(a, b, c, d, M_offset_9, 5, T[24]);
d = GG(d, a, b, c, M_offset_14, 9, T[25]);
c = GG(c, d, a, b, M_offset_3, 14, T[26]);
b = GG(b, c, d, a, M_offset_8, 20, T[27]);
a = GG(a, b, c, d, M_offset_13, 5, T[28]);
d = GG(d, a, b, c, M_offset_2, 9, T[29]);
c = GG(c, d, a, b, M_offset_7, 14, T[30]);
b = GG(b, c, d, a, M_offset_12, 20, T[31]);
a = HH(a, b, c, d, M_offset_5, 4, T[32]);
d = HH(d, a, b, c, M_offset_8, 11, T[33]);
c = HH(c, d, a, b, M_offset_11, 16, T[34]);
b = HH(b, c, d, a, M_offset_14, 23, T[35]);
a = HH(a, b, c, d, M_offset_1, 4, T[36]);
d = HH(d, a, b, c, M_offset_4, 11, T[37]);
c = HH(c, d, a, b, M_offset_7, 16, T[38]);
b = HH(b, c, d, a, M_offset_10, 23, T[39]);
a = HH(a, b, c, d, M_offset_13, 4, T[40]);
d = HH(d, a, b, c, M_offset_0, 11, T[41]);
c = HH(c, d, a, b, M_offset_3, 16, T[42]);
b = HH(b, c, d, a, M_offset_6, 23, T[43]);
a = HH(a, b, c, d, M_offset_9, 4, T[44]);
d = HH(d, a, b, c, M_offset_12, 11, T[45]);
c = HH(c, d, a, b, M_offset_15, 16, T[46]);
b = HH(b, c, d, a, M_offset_2, 23, T[47]);
a = II(a, b, c, d, M_offset_0, 6, T[48]);
d = II(d, a, b, c, M_offset_7, 10, T[49]);
c = II(c, d, a, b, M_offset_14, 15, T[50]);
b = II(b, c, d, a, M_offset_5, 21, T[51]);
a = II(a, b, c, d, M_offset_12, 6, T[52]);
d = II(d, a, b, c, M_offset_3, 10, T[53]);
c = II(c, d, a, b, M_offset_10, 15, T[54]);
b = II(b, c, d, a, M_offset_1, 21, T[55]);
a = II(a, b, c, d, M_offset_8, 6, T[56]);
d = II(d, a, b, c, M_offset_15, 10, T[57]);
c = II(c, d, a, b, M_offset_6, 15, T[58]);
b = II(b, c, d, a, M_offset_13, 21, T[59]);
a = II(a, b, c, d, M_offset_4, 6, T[60]);
d = II(d, a, b, c, M_offset_11, 10, T[61]);
c = II(c, d, a, b, M_offset_2, 15, T[62]);
b = II(b, c, d, a, M_offset_9, 21, T[63]);
// Intermediate hash value
H[0] = (H[0] + a) | 0;
H[1] = (H[1] + b) | 0;
H[2] = (H[2] + c) | 0;
H[3] = (H[3] + d) | 0;
},
_doFinalize: function () {
// Shortcuts
var data = this._data;
var dataWords = data.words;
var nBitsTotal = this._nDataBytes * 8;
var nBitsLeft = data.sigBytes * 8;
// Add padding
dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000);
var nBitsTotalL = nBitsTotal;
dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = (
(((nBitsTotalH << 8) | (nBitsTotalH >>> 24)) & 0x00ff00ff) |
(((nBitsTotalH << 24) | (nBitsTotalH >>> 8)) & 0xff00ff00)
);
dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = (
(((nBitsTotalL << 8) | (nBitsTotalL >>> 24)) & 0x00ff00ff) |
(((nBitsTotalL << 24) | (nBitsTotalL >>> 8)) & 0xff00ff00)
);
data.sigBytes = (dataWords.length + 1) * 4;
// Hash final blocks
this._process();
// Shortcuts
var hash = this._hash;
var H = hash.words;
// Swap endian
for (var i = 0; i < 4; i++) {
// Shortcut
var H_i = H[i];
H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) |
(((H_i << 24) | (H_i >>> 8)) & 0xff00ff00);
}
// Return final computed hash
return hash;
},
clone: function () {
var clone = Hasher.clone.call(this);
clone._hash = this._hash.clone();
return clone;
}
});
function FF(a, b, c, d, x, s, t) {
var n = a + ((b & c) | (~b & d)) + x + t;
return ((n << s) | (n >>> (32 - s))) + b;
}
function GG(a, b, c, d, x, s, t) {
var n = a + ((b & d) | (c & ~d)) + x + t;
return ((n << s) | (n >>> (32 - s))) + b;
}
function HH(a, b, c, d, x, s, t) {
var n = a + (b ^ c ^ d) + x + t;
return ((n << s) | (n >>> (32 - s))) + b;
}
function II(a, b, c, d, x, s, t) {
var n = a + (c ^ (b | ~d)) + x + t;
return ((n << s) | (n >>> (32 - s))) + b;
}
/**
* Shortcut function to the hasher's object interface.
*
* @param {WordArray|string} message The message to hash.
*
* @return {WordArray} The hash.
*
* @static
*
* @example
*
* var hash = CryptoJS.MD5('message');
* var hash = CryptoJS.MD5(wordArray);
*/
C.MD5 = Hasher._createHelper(MD5);
/**
* Shortcut function to the HMAC's object interface.
*
* @param {WordArray|string} message The message to hash.
* @param {WordArray|string} key The secret key.
*
* @return {WordArray} The HMAC.
*
* @static
*
* @example
*
* var hmac = CryptoJS.HmacMD5(message, key);
*/
C.HmacMD5 = Hasher._createHmacHelper(MD5);
}(Math));
return CryptoJS.MD5;
}));

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,446 @@
/**
* A Web Serial based nRF52 flasher written by liam@liamcottle.com based on dfu_transport.serial.py
* https://github.com/adafruit/Adafruit_nRF52_nrfutil/blob/master/nordicsemi/dfu/dfu_transport_serial.py
*/
class Nrf52DfuFlasher {
DFU_TOUCH_BAUD = 1200;
SERIAL_PORT_OPEN_WAIT_TIME = 0.1;
TOUCH_RESET_WAIT_TIME = 1.5;
FLASH_BAUD = 115200;
HEX_TYPE_APPLICATION = 4;
DFU_INIT_PACKET = 1;
DFU_START_PACKET = 3;
DFU_DATA_PACKET = 4;
DFU_STOP_DATA_PACKET = 5;
DATA_INTEGRITY_CHECK_PRESENT = 1;
RELIABLE_PACKET = 1;
HCI_PACKET_TYPE = 14;
FLASH_PAGE_SIZE = 4096;
FLASH_PAGE_ERASE_TIME = 0.0897;
FLASH_WORD_WRITE_TIME = 0.000100;
FLASH_PAGE_WRITE_TIME = (this.FLASH_PAGE_SIZE/4) * this.FLASH_WORD_WRITE_TIME;
// The DFU packet max size
DFU_PACKET_MAX_SIZE = 512;
constructor(serialPort) {
this.serialPort = serialPort;
this.sequenceNumber = 0;
this.sd_size = 0;
this.total_size = 0;
}
/**
* Waits for the provided milliseconds, and then resolves.
* @param millis
* @returns {Promise<void>}
*/
async sleepMillis(millis) {
await new Promise((resolve) => {
setTimeout(resolve, millis);
});
}
/**
* Writes the provided data to the Serial Port.
* @param data
* @returns {Promise<void>}
*/
async sendPacket(data) {
const writer = this.serialPort.writable.getWriter();
try {
await writer.write(new Uint8Array(data));
} finally {
writer.releaseLock();
}
}
/**
* Puts an nRF52 board into DFU mode by quickly opening and closing a serial port.
* @returns {Promise<void>}
*/
async enterDfuMode() {
// open port
await this.serialPort.open({
baudRate: this.DFU_TOUCH_BAUD,
});
// wait SERIAL_PORT_OPEN_WAIT_TIME before closing port
await this.sleepMillis(this.SERIAL_PORT_OPEN_WAIT_TIME * 1000);
// close port
await this.serialPort.close();
// wait TOUCH_RESET_WAIT_TIME for device to enter into DFU mode
await this.sleepMillis(this.TOUCH_RESET_WAIT_TIME * 1000);
}
/**
* Flashes the provided firmware zip.
* @param firmwareZipBlob
* @param progressCallback
* @returns {Promise<void>}
*/
async flash(firmwareZipBlob, progressCallback) {
// read zip file
const blobReader = new window.zip.BlobReader(firmwareZipBlob);
const zipReader = new window.zip.ZipReader(blobReader);
const zipEntries = await zipReader.getEntries();
// find manifest file
const manifestFile = zipEntries.find((zipEntry) => zipEntry.filename === "manifest.json");
if(!manifestFile){
throw "manifest.json not found in firmware file!";
}
// read manifest file as text
const text = await manifestFile.getData(new window.zip.TextWriter());
// parse manifest json
const json = JSON.parse(text);
const manifest = json.manifest;
// todo softdevice_bootloader
// if self.manifest.softdevice_bootloader:
// self._dfu_send_image(HexType.SD_BL, self.manifest.softdevice_bootloader)
// todo softdevice
// if self.manifest.softdevice:
// self._dfu_send_image(HexType.SOFTDEVICE, self.manifest.softdevice)
// todo bootloader
// if self.manifest.bootloader:
// self._dfu_send_image(HexType.BOOTLOADER, self.manifest.bootloader)
// flash application image
if(manifest.application){
await this.dfuSendImage(this.HEX_TYPE_APPLICATION, zipEntries, manifest.application, progressCallback);
}
}
/**
* Sends the firmware image to the device in DFU mode.
* @param programMode
* @param zipEntries
* @param firmwareManifest
* @param progressCallback
* @returns {Promise<void>}
*/
async dfuSendImage(programMode, zipEntries, firmwareManifest, progressCallback) {
// open port
await this.serialPort.open({
baudRate: this.FLASH_BAUD,
});
// wait SERIAL_PORT_OPEN_WAIT_TIME
await this.sleepMillis(this.SERIAL_PORT_OPEN_WAIT_TIME * 1000);
// file sizes
var softdeviceSize = 0
var bootloaderSize = 0
var applicationSize = 0
// read bin file (firmware)
const binFile = zipEntries.find((zipEntry) => zipEntry.filename === firmwareManifest.bin_file);
const firmware = await binFile.getData(new window.zip.Uint8ArrayWriter());
// read dat file (init packet)
const datFile = zipEntries.find((zipEntry) => zipEntry.filename === firmwareManifest.dat_file);
const init_packet = await datFile.getData(new window.zip.Uint8ArrayWriter());
// only support flashing application for now
if(programMode !== this.HEX_TYPE_APPLICATION){
throw "not implemented";
}
// determine application size
if(programMode === this.HEX_TYPE_APPLICATION){
applicationSize = firmware.length;
}
console.log("Sending DFU start packet");
await this.sendStartDfu(programMode, softdeviceSize, bootloaderSize, applicationSize);
console.log("Sending DFU init packet");
await this.sendInitPacket(init_packet);
console.log("Sending firmware");
await this.sendFirmware(firmware, progressCallback);
// todo
// sleep(self.dfu_transport.get_activate_wait_time())
}
/**
* Calculates CRC16 on the provided binaryData
* @param {Uint8Array} binaryData - Array with data to run CRC16 calculation on
* @param {number} crc - CRC value to start calculation with
* @return {number} - Calculated CRC value of binaryData
*/
calcCrc16(binaryData, crc = 0xffff) {
if(!(binaryData instanceof Uint8Array)){
throw new Error("calcCrc16 requires Uint8Array input");
}
for(let b of binaryData){
crc = (crc >> 8 & 0x00FF) | (crc << 8 & 0xFF00);
crc ^= b;
crc ^= (crc & 0x00FF) >> 4;
crc ^= (crc << 8) << 4;
crc ^= ((crc & 0x00FF) << 4) << 1;
}
return crc & 0xFFFF;
}
/**
* Encode esc characters in a SLIP package.
* Replace 0xC0 with 0xDBDC and 0xDB with 0xDBDD.
* @param dataIn
* @returns {*[]}
*/
slipEncodeEscChars(dataIn) {
let result = [];
for(let i = 0; i < dataIn.length; i++){
let char = dataIn[i];
if(char === 0xC0){
result.push(0xDB);
result.push(0xDC);
} else if(char === 0xDB) {
result.push(0xDB);
result.push(0xDD);
} else {
result.push(char);
}
}
return result;
}
/**
* Creates an HCI packet from the provided frame data.
* https://github.com/adafruit/Adafruit_nRF52_nrfutil/blob/master/nordicsemi/dfu/dfu_transport_serial.py#L332
* @param frame
* @returns {*[]}
*/
createHciPacketFromFrame(frame) {
// increase sequence number, but roll over at 8
this.sequenceNumber = (this.sequenceNumber + 1) % 8;
// create slip header
const slipHeaderBytes = this.createSlipHeader(
this.sequenceNumber,
this.DATA_INTEGRITY_CHECK_PRESENT,
this.RELIABLE_PACKET,
this.HCI_PACKET_TYPE,
frame.length,
);
// create packet data
let data = [
...slipHeaderBytes,
...frame,
];
// add crc of data
const crc = this.calcCrc16(new Uint8Array(data), 0xffff);
data.push(crc & 0xFF);
data.push((crc & 0xFF00) >> 8);
// add escape characters
return [
0xc0,
...this.slipEncodeEscChars(data),
0xc0,
];
}
/**
* Calculate how long we should wait for erasing data.
* @returns {number}
*/
getEraseWaitTime() {
// always wait at least 0.5 seconds
return Math.max(0.5, ((this.total_size / this.FLASH_PAGE_SIZE) + 1) * this.FLASH_PAGE_ERASE_TIME);
}
/**
* Constructs the image size packet sent in the DFU Start packet.
* @param softdeviceSize
* @param bootloaderSize
* @param appSize
* @returns {number[]}
*/
createImageSizePacket(softdeviceSize = 0, bootloaderSize = 0, appSize = 0) {
return [
...this.int32ToBytes(softdeviceSize),
...this.int32ToBytes(bootloaderSize),
...this.int32ToBytes(appSize),
];
}
/**
* Sends the DFU Start packet to the device.
* @param mode
* @param softdevice_size
* @param bootloader_size
* @param app_size
* @returns {Promise<void>}
*/
async sendStartDfu(mode, softdevice_size = 0, bootloader_size = 0, app_size = 0){
// create frame
const frame = [
...this.int32ToBytes(this.DFU_START_PACKET),
...this.int32ToBytes(mode),
...this.createImageSizePacket(softdevice_size, bootloader_size, app_size),
];
// send hci packet
await this.sendPacket(this.createHciPacketFromFrame(frame));
// remember file sizes for calculating erase wait time
this.sd_size = softdevice_size;
this.total_size = softdevice_size + bootloader_size + app_size;
// wait for initial erase
await this.sleepMillis(this.getEraseWaitTime() * 1000);
}
/**
* Sends the DFU Init packet to the device.
* @param initPacket
* @returns {Promise<void>}
*/
async sendInitPacket(initPacket){
// create frame
const frame = [
...this.int32ToBytes(this.DFU_INIT_PACKET),
...initPacket,
...this.int16ToBytes(0x0000), // padding required
];
// send hci packet
await this.sendPacket(this.createHciPacketFromFrame(frame));
}
/**
* Sends the firmware file to the device in multiple chunks.
* @param firmware
* @param progressCallback
* @returns {Promise<void>}
*/
async sendFirmware(firmware, progressCallback) {
const packets = [];
var packetsSent = 0;
// chunk firmware into separate packets
for(let i = 0; i < firmware.length; i += this.DFU_PACKET_MAX_SIZE){
packets.push(this.createHciPacketFromFrame([
...this.int32ToBytes(this.DFU_DATA_PACKET),
...firmware.slice(i, i + this.DFU_PACKET_MAX_SIZE),
]));
}
// send initial progress
if(progressCallback){
progressCallback(0);
}
// send each packet one after the other
for(var i = 0; i < packets.length; i++){
// send packet
await this.sendPacket(packets[i]);
// wait a bit to allow device to write before sending next packet
await this.sleepMillis(this.FLASH_PAGE_WRITE_TIME * 1000);
// update progress
packetsSent++;
if(progressCallback){
const progress = Math.floor((packetsSent / packets.length) * 100);
progressCallback(progress);
}
}
// finished sending firmware, send DFU Stop Data packet
await this.sendPacket(this.createHciPacketFromFrame([
...this.int32ToBytes(this.DFU_STOP_DATA_PACKET),
]));
}
/**
* Creates a SLIP header.
*
* For a description of the SLIP header go to:
* http://developer.nordicsemi.com/nRF51_SDK/doc/7.2.0/s110/html/a00093.html
*
* @param {number} seq - Packet sequence number
* @param {number} dip - Data integrity check
* @param {number} rp - Reliable packet
* @param {number} pktType - Payload packet
* @param {number} pktLen - Packet length
* @return {Uint8Array} - SLIP header
*/
createSlipHeader(seq, dip, rp, pktType, pktLen) {
let ints = [0, 0, 0, 0];
ints[0] = seq | (((seq + 1) % 8) << 3) | (dip << 6) | (rp << 7);
ints[1] = pktType | ((pktLen & 0x000F) << 4);
ints[2] = (pktLen & 0x0FF0) >> 4;
ints[3] = (~(ints[0] + ints[1] + ints[2]) + 1) & 0xFF;
return new Uint8Array(ints);
}
/**
* Converts the provided int32 to 4 bytes.
* @param num
* @returns {number[]}
*/
int32ToBytes(num){
return [
(num & 0x000000ff),
(num & 0x0000ff00) >> 8,
(num & 0x00ff0000) >> 16,
(num & 0xff000000) >> 24,
];
}
/**
* Converts the provided int16 to 2 bytes.
* @param num
* @returns {number[]}
*/
int16ToBytes(num){
return [
(num & 0x00FF),
(num & 0xFF00) >> 8,
];
}
}

View File

@@ -0,0 +1,959 @@
class Utils {
/**
* Waits for the provided milliseconds, and then resolves.
* @param millis
* @returns {Promise<void>}
*/
static async sleepMillis(millis) {
await new Promise((resolve) => {
setTimeout(resolve, millis);
});
}
static bytesToHex(bytes) {
for(var hex = [], i = 0; i < bytes.length; i++){
var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
hex.push((current >>> 4).toString(16));
hex.push((current & 0xF).toString(16));
}
return hex.join("");
}
static md5(data) {
var bytes = [];
const hash = CryptoJS.MD5(CryptoJS.enc.Hex.parse(this.bytesToHex(data)));
for(var i = 0; i < hash.sigBytes; i++){
bytes.push((hash.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff);
}
return bytes;
}
static packUInt32BE(value) {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, value, false);
return new Uint8Array(buffer);
}
static unpackUInt32BE(byteArray) {
const buffer = new Uint8Array(byteArray).buffer;
const view = new DataView(buffer);
return view.getUint32(0, false);
}
}
class RNode {
KISS_FEND = 0xC0;
KISS_FESC = 0xDB;
KISS_TFEND = 0xDC;
KISS_TFESC = 0xDD;
CMD_FREQUENCY = 0x01;
CMD_BANDWIDTH = 0x02;
CMD_TXPOWER = 0x03;
CMD_SF = 0x04;
CMD_CR = 0x05;
CMD_RADIO_STATE = 0x06;
CMD_STAT_RX = 0x21;
CMD_STAT_TX = 0x22
CMD_STAT_RSSI = 0x23;
CMD_STAT_SNR = 0x24;
CMD_BOARD = 0x47;
CMD_PLATFORM = 0x48;
CMD_MCU = 0x49;
CMD_RESET = 0x55;
CMD_RESET_BYTE = 0xF8;
CMD_DEV_HASH = 0x56;
CMD_FW_VERSION = 0x50;
CMD_ROM_READ = 0x51;
CMD_ROM_WRITE = 0x52;
CMD_CONF_SAVE = 0x53;
CMD_CONF_DELETE = 0x54;
CMD_FW_HASH = 0x58;
CMD_UNLOCK_ROM = 0x59;
ROM_UNLOCK_BYTE = 0xF8;
CMD_HASHES = 0x60;
CMD_FW_UPD = 0x61;
CMD_BT_CTRL = 0x46;
CMD_BT_PIN = 0x62;
CMD_DISP_READ = 0x66;
CMD_DETECT = 0x08;
DETECT_REQ = 0x73;
DETECT_RESP = 0x46;
RADIO_STATE_OFF = 0x00;
RADIO_STATE_ON = 0x01;
RADIO_STATE_ASK = 0xFF;
CMD_ERROR = 0x90
ERROR_INITRADIO = 0x01
ERROR_TXFAILED = 0x02
ERROR_EEPROM_LOCKED = 0x03
PLATFORM_AVR = 0x90;
PLATFORM_ESP32 = 0x80;
PLATFORM_NRF52 = 0x70;
MCU_1284P = 0x91;
MCU_2560 = 0x92;
MCU_ESP32 = 0x81;
MCU_NRF52 = 0x71;
BOARD_RNODE = 0x31;
BOARD_HMBRW = 0x32;
BOARD_TBEAM = 0x33;
BOARD_HUZZAH32 = 0x34;
BOARD_GENERIC_ESP32 = 0x35;
BOARD_LORA32_V2_0 = 0x36;
BOARD_LORA32_V2_1 = 0x37;
BOARD_RAK4631 = 0x51;
HASH_TYPE_TARGET_FIRMWARE = 0x01;
HASH_TYPE_FIRMWARE = 0x02;
constructor(serialPort) {
this.serialPort = serialPort;
this.readable = serialPort.readable;
this.writable = serialPort.writable;
}
static async fromSerialPort(serialPort) {
// open port
await serialPort.open({
baudRate: 115200,
});
return new RNode(serialPort);
}
async close() {
try {
await this.serialPort.close();
} catch(e) {
console.log("failed to close serial port, ignoring...", e);
}
}
async write(bytes) {
const writer = this.writable.getWriter();
try {
await writer.write(new Uint8Array(bytes));
} finally {
writer.releaseLock();
}
}
async readFromSerialPort(timeoutMillis) {
return new Promise(async (resolve, reject) => {
// create reader
const reader = this.readable.getReader();
// timeout after provided millis
if(timeoutMillis != null){
setTimeout(() => {
reader.releaseLock();
reject("timeout");
}, timeoutMillis);
}
// attempt to read kiss frame
try {
let buffer = [];
while(true){
const { value, done } = await reader.read();
if(done){
break;
}
if(value){
for(let byte of value){
buffer.push(byte);
if(byte === this.KISS_FEND){
if(buffer.length > 1){
resolve(this.handleKISSFrame(buffer));
return;
}
buffer = [this.KISS_FEND]; // Start new frame
}
}
}
}
} catch (error) {
console.error('Error reading from serial port: ', error);
} finally {
reader.releaseLock();
}
});
}
handleKISSFrame(frame) {
let data = [];
let escaping = false;
// Skip the initial 0xC0 and process the rest
for(let i = 1; i < frame.length; i++){
let byte = frame[i];
if (escaping) {
if (byte === this.KISS_TFEND) {
data.push(this.KISS_FEND);
} else if (byte === this.KISS_TFESC) {
data.push(this.KISS_FESC);
}
escaping = false;
} else {
if (byte === this.KISS_FESC) {
escaping = true;
} else if (byte === this.KISS_FEND) {
// Ignore the end frame delimiter
break;
} else {
data.push(byte);
}
}
}
//console.log('Received KISS frame data:', new Uint8Array(data));
return data;
}
createKissFrame(data) {
let frame = [this.KISS_FEND];
for(let byte of data){
if(byte === this.KISS_FEND){
frame.push(this.KISS_FESC, this.KISS_TFEND);
} else if(byte === this.KISS_FESC){
frame.push(this.KISS_FESC, this.KISS_TFESC);
} else {
frame.push(byte);
}
}
frame.push(this.KISS_FEND);
return new Uint8Array(frame);
}
async sendKissCommand(data) {
await this.write(this.createKissFrame(data));
}
async reset() {
await this.sendKissCommand([
this.CMD_RESET,
this.CMD_RESET_BYTE,
]);
}
async detect() {
// ask if device is rnode
await this.sendKissCommand([
this.CMD_DETECT,
this.DETECT_REQ,
]);
// read response from device
const [ command, responseByte ] = await this.readFromSerialPort();
// device is an rnode if response is as expected
return command === this.CMD_DETECT && responseByte === this.DETECT_RESP;
}
async getFirmwareVersion() {
await this.sendKissCommand([
this.CMD_FW_VERSION,
0x00,
]);
// read response from device
var [ command, majorVersion, minorVersion ] = await this.readFromSerialPort();
if(minorVersion.length === 1){
minorVersion = "0" + minorVersion;
}
// 1.23
return majorVersion + "." + minorVersion;
}
async getPlatform() {
await this.sendKissCommand([
this.CMD_PLATFORM,
0x00,
]);
// read response from device
const [ command, platformByte ] = await this.readFromSerialPort();
return platformByte;
}
async getMcu() {
await this.sendKissCommand([
this.CMD_MCU,
0x00,
]);
// read response from device
const [ command, mcuByte ] = await this.readFromSerialPort();
return mcuByte;
}
async getBoard() {
await this.sendKissCommand([
this.CMD_BOARD,
0x00,
]);
// read response from device
const [ command, boardByte ] = await this.readFromSerialPort();
return boardByte;
}
async getDeviceHash() {
await this.sendKissCommand([
this.CMD_DEV_HASH,
0x01, // anything != 0x00
]);
// read response from device
const [ command, ...deviceHash ] = await this.readFromSerialPort();
return deviceHash;
}
async getTargetFirmwareHash() {
await this.sendKissCommand([
this.CMD_HASHES,
this.HASH_TYPE_TARGET_FIRMWARE,
]);
// read response from device
const [ command, hashType, ...targetFirmwareHash ] = await this.readFromSerialPort();
return targetFirmwareHash;
}
async getFirmwareHash() {
await this.sendKissCommand([
this.CMD_HASHES,
this.HASH_TYPE_FIRMWARE,
]);
// read response from device
const [ command, hashType, ...firmwareHash ] = await this.readFromSerialPort();
return firmwareHash;
}
async getRom() {
await this.sendKissCommand([
this.CMD_ROM_READ,
0x00,
]);
// read response from device
const [ command, ...eepromBytes ] = await this.readFromSerialPort();
return eepromBytes;
}
async getFrequency() {
await this.sendKissCommand([
this.CMD_FREQUENCY,
// request frequency by sending zero as 4 bytes
0x00,
0x00,
0x00,
0x00,
]);
// read response from device
const [ command, ...frequencyBytes ] = await this.readFromSerialPort();
// convert 4 bytes to 32bit integer representing frequency in hertz
const frequencyInHz = frequencyBytes[0] << 24 | frequencyBytes[1] << 16 | frequencyBytes[2] << 8 | frequencyBytes[3];
return frequencyInHz;
}
async getBandwidth() {
await this.sendKissCommand([
this.CMD_BANDWIDTH,
// request bandwidth by sending zero as 4 bytes
0x00,
0x00,
0x00,
0x00,
]);
// read response from device
const [ command, ...bandwidthBytes ] = await this.readFromSerialPort();
// convert 4 bytes to 32bit integer representing bandwidth in hertz
const bandwidthInHz = bandwidthBytes[0] << 24 | bandwidthBytes[1] << 16 | bandwidthBytes[2] << 8 | bandwidthBytes[3];
return bandwidthInHz;
}
async getTxPower() {
await this.sendKissCommand([
this.CMD_TXPOWER,
0xFF, // request tx power
]);
// read response from device
const [ command, txPower ] = await this.readFromSerialPort();
return txPower;
}
async getSpreadingFactor() {
await this.sendKissCommand([
this.CMD_SF,
0xFF, // request spreading factor
]);
// read response from device
const [ command, spreadingFactor ] = await this.readFromSerialPort();
return spreadingFactor;
}
async getCodingRate() {
await this.sendKissCommand([
this.CMD_CR,
0xFF, // request coding rate
]);
// read response from device
const [ command, codingRate ] = await this.readFromSerialPort();
return codingRate;
}
async getRadioState() {
await this.sendKissCommand([
this.CMD_RADIO_STATE,
0xFF, // request radio state
]);
// read response from device
const [ command, radioState ] = await this.readFromSerialPort();
return radioState;
}
async getRxStat() {
await this.sendKissCommand([
this.CMD_STAT_RX,
0x00,
]);
// read response from device
const [ command, ...statBytes ] = await this.readFromSerialPort();
// convert 4 bytes to 32bit integer
const stat = statBytes[0] << 24 | statBytes[1] << 16 | statBytes[2] << 8 | statBytes[3];
return stat;
}
async getTxStat() {
await this.sendKissCommand([
this.CMD_STAT_TX,
0x00,
]);
// read response from device
const [ command, ...statBytes ] = await this.readFromSerialPort();
// convert 4 bytes to 32bit integer
const stat = statBytes[0] << 24 | statBytes[1] << 16 | statBytes[2] << 8 | statBytes[3];
return stat;
}
async getRssiStat() {
await this.sendKissCommand([
this.CMD_STAT_RSSI,
0x00,
]);
// read response from device
const [ command, rssi ] = await this.readFromSerialPort();
return rssi;
}
async disableBluetooth() {
await this.sendKissCommand([
this.CMD_BT_CTRL,
0x00, // stop
]);
}
async enableBluetooth() {
await this.sendKissCommand([
this.CMD_BT_CTRL,
0x01, // start
]);
}
async startBluetoothPairing() {
// enable pairing
await this.sendKissCommand([
this.CMD_BT_CTRL,
0x02, // enable pairing
]);
// attempt to get bluetooth pairing pin
try {
// read response from device
const [ command, ...pinBytes ] = await this.readFromSerialPort(5000);
if(command !== this.CMD_BT_PIN){
throw `unexpected command response: ${command}`;
}
// convert 4 bytes to 32bit integer
const pin = pinBytes[0] << 24 | pinBytes[1] << 16 | pinBytes[2] << 8 | pinBytes[3];
// todo: remove logs
console.log(pinBytes);
console.log(pin);
// todo: convert to string
return pin;
} catch(error) {
throw `failed to get bluetooth pin: ${error}`;
}
}
async readDisplay() {
await this.sendKissCommand([
this.CMD_DISP_READ,
0x01,
]);
// read response from device
const [ command, ...displayBuffer ] = await this.readFromSerialPort();
return displayBuffer;
}
async setFrequency(frequencyInHz) {
const c1 = frequencyInHz >> 24;
const c2 = frequencyInHz >> 16 & 0xFF;
const c3 = frequencyInHz >> 8 & 0xFF;
const c4 = frequencyInHz & 0xFF;
await this.sendKissCommand([
this.CMD_FREQUENCY,
c1,
c2,
c3,
c4,
]);
}
async setBandwidth(bandwidthInHz) {
const c1 = bandwidthInHz >> 24;
const c2 = bandwidthInHz >> 16 & 0xFF;
const c3 = bandwidthInHz >> 8 & 0xFF;
const c4 = bandwidthInHz & 0xFF;
await this.sendKissCommand([
this.CMD_BANDWIDTH,
c1,
c2,
c3,
c4,
]);
}
async setTxPower(db) {
await this.sendKissCommand([
this.CMD_TXPOWER,
db,
]);
}
async setSpreadingFactor(spreadingFactor) {
await this.sendKissCommand([
this.CMD_SF,
spreadingFactor,
]);
}
async setCodingRate(codingRate) {
await this.sendKissCommand([
this.CMD_CR,
codingRate,
]);
}
async setRadioStateOn() {
await this.sendKissCommand([
this.CMD_RADIO_STATE,
this.RADIO_STATE_ON,
]);
}
async setRadioStateOff() {
await this.sendKissCommand([
this.CMD_RADIO_STATE,
this.RADIO_STATE_OFF,
]);
}
// setTNCMode
async saveConfig() {
await this.sendKissCommand([
this.CMD_CONF_SAVE,
0x00,
]);
}
// setNormalMode
async deleteConfig() {
await this.sendKissCommand([
this.CMD_CONF_DELETE,
0x00,
]);
}
async indicateFirmwareUpdate() {
await this.sendKissCommand([
this.CMD_FW_UPD,
0x01,
]);
}
async setFirmwareHash(hash) {
await this.sendKissCommand([
this.CMD_FW_HASH,
...hash,
]);
}
async writeRom(address, value) {
// write to rom
await this.sendKissCommand([
this.CMD_ROM_WRITE,
address,
value,
]);
// wait a bit to allow device to write to rom
await Utils.sleepMillis(85);
}
async wipeRom() {
await this.sendKissCommand([
this.CMD_UNLOCK_ROM,
this.ROM_UNLOCK_BYTE,
]);
// wiping can take up to 30 seconds
await Utils.sleepMillis(30000);
}
async getRomAsObject() {
const rom = await this.getRom();
return new ROM(rom);
}
}
class ROM {
static PLATFORM_AVR = 0x90
static PLATFORM_ESP32 = 0x80
static PLATFORM_NRF52 = 0x70
static MCU_1284P = 0x91
static MCU_2560 = 0x92
static MCU_ESP32 = 0x81
static MCU_NRF52 = 0x71
static PRODUCT_RAK4631 = 0x10
static MODEL_11 = 0x11
static MODEL_12 = 0x12
static PRODUCT_RNODE = 0x03
static MODEL_A1 = 0xA1
static MODEL_A6 = 0xA6
static MODEL_A4 = 0xA4
static MODEL_A9 = 0xA9
static MODEL_A3 = 0xA3
static MODEL_A8 = 0xA8
static MODEL_A2 = 0xA2
static MODEL_A7 = 0xA7
static MODEL_A5 = 0xA5;
static MODEL_AA = 0xAA;
static PRODUCT_T32_10 = 0xB2
static MODEL_BA = 0xBA
static MODEL_BB = 0xBB
static PRODUCT_T32_20 = 0xB0
static MODEL_B3 = 0xB3
static MODEL_B8 = 0xB8
static PRODUCT_T32_21 = 0xB1
static MODEL_B4 = 0xB4
static MODEL_B9 = 0xB9
static MODEL_B4_TCXO = 0x04 // The TCXO model codes are only used here to select the
static MODEL_B9_TCXO = 0x09 // correct firmware, actual model codes in firmware is still 0xB4 and 0xB9.
static PRODUCT_H32_V2 = 0xC0
static MODEL_C4 = 0xC4
static MODEL_C9 = 0xC9
static PRODUCT_H32_V3 = 0xC1
static MODEL_C5 = 0xC5
static MODEL_CA = 0xCA
static PRODUCT_TBEAM = 0xE0
static MODEL_E4 = 0xE4
static MODEL_E9 = 0xE9
static MODEL_E3 = 0xE3
static MODEL_E8 = 0xE8
static PRODUCT_TBEAM_S_V1 = 0xEA;
static MODEL_DB = 0xDB
static MODEL_DC = 0xDC
static PRODUCT_TDECK = 0xD0;
static MODEL_D4 = 0xD4;
static MODEL_D9 = 0xD9;
static PRODUCT_TECHO = 0x15;
static MODEL_T4 = 0x16;
static MODEL_T9 = 0x17;
static PRODUCT_HMBRW = 0xF0
static MODEL_FF = 0xFF
static MODEL_FE = 0xFE
static ADDR_PRODUCT = 0x00
static ADDR_MODEL = 0x01
static ADDR_HW_REV = 0x02
static ADDR_SERIAL = 0x03
static ADDR_MADE = 0x07
static ADDR_CHKSUM = 0x0B
static ADDR_SIGNATURE = 0x1B
static ADDR_INFO_LOCK = 0x9B
static ADDR_CONF_SF = 0x9C
static ADDR_CONF_CR = 0x9D
static ADDR_CONF_TXP = 0x9E
static ADDR_CONF_BW = 0x9F
static ADDR_CONF_FREQ = 0xA3
static ADDR_CONF_OK = 0xA7
static INFO_LOCK_BYTE = 0x73
static CONF_OK_BYTE = 0x73
static BOARD_RNODE = 0x31
static BOARD_HMBRW = 0x32
static BOARD_TBEAM = 0x33
static BOARD_HUZZAH32 = 0x34
static BOARD_GENERIC_ESP32 = 0x35
static BOARD_LORA32_V2_0 = 0x36
static BOARD_LORA32_V2_1 = 0x37
static BOARD_RAK4631 = 0x51
static MANUAL_FLASH_MODELS = [ROM.MODEL_A1, ROM.MODEL_A6]
constructor(eeprom) {
this.eeprom = eeprom;
}
getProduct() {
return this.eeprom[ROM.ADDR_PRODUCT];
}
getModel() {
return this.eeprom[ROM.ADDR_MODEL];
}
getHardwareRevision() {
return this.eeprom[ROM.ADDR_HW_REV];
}
getSerialNumber() {
return [
this.eeprom[ROM.ADDR_SERIAL],
this.eeprom[ROM.ADDR_SERIAL + 1],
this.eeprom[ROM.ADDR_SERIAL + 2],
this.eeprom[ROM.ADDR_SERIAL + 3],
];
}
getMade() {
return [
this.eeprom[ROM.ADDR_MADE],
this.eeprom[ROM.ADDR_MADE + 1],
this.eeprom[ROM.ADDR_MADE + 2],
this.eeprom[ROM.ADDR_MADE + 3],
];
}
getChecksum() {
const checksum = [];
for(var i = 0; i < 16; i++){
checksum.push(this.eeprom[ROM.ADDR_CHKSUM + i]);
}
return checksum;
}
getSignature() {
const signature = [];
for(var i = 0; i < 128; i++){
signature.push(this.eeprom[ROM.ADDR_SIGNATURE + i]);
}
return signature;
}
getCalculatedChecksum() {
return Utils.md5([
this.getProduct(),
this.getModel(),
this.getHardwareRevision(),
...this.getSerialNumber(),
...this.getMade(),
]);
}
getConfiguredSpreadingFactor() {
return this.eeprom[ROM.ADDR_CONF_SF];
}
getConfiguredCodingRate() {
return this.eeprom[ROM.ADDR_CONF_CR];
}
getConfiguredTxPower() {
return this.eeprom[ROM.ADDR_CONF_TXP];
}
getConfiguredFrequency() {
return this.eeprom[ROM.ADDR_CONF_FREQ] << 24
| this.eeprom[ROM.ADDR_CONF_FREQ + 1] << 16
| this.eeprom[ROM.ADDR_CONF_FREQ + 2] << 8
| this.eeprom[ROM.ADDR_CONF_FREQ + 3];
}
getConfiguredBandwidth() {
return this.eeprom[ROM.ADDR_CONF_BW] << 24
| this.eeprom[ROM.ADDR_CONF_BW + 1] << 16
| this.eeprom[ROM.ADDR_CONF_BW + 2] << 8
| this.eeprom[ROM.ADDR_CONF_BW + 3];
}
isInfoLocked() {
return this.eeprom[ROM.ADDR_INFO_LOCK] === ROM.INFO_LOCK_BYTE;
}
isConfigured() {
return this.eeprom[ROM.ADDR_CONF_OK] === ROM.CONF_OK_BYTE;
}
parse() {
// ensure info lock byte is set
if(!this.isInfoLocked()){
return null;
}
// convert to hex
const checksumHex = Utils.bytesToHex(this.getChecksum());
const calculatedChecksumHex = Utils.bytesToHex(this.getCalculatedChecksum());
const signatureHex = Utils.bytesToHex(this.getSignature());
// add details
var details = {
is_provisioned: true,
is_configured: this.isConfigured(),
product: this.getProduct(),
model: this.getModel(),
hardware_revision: this.getHardwareRevision(),
serial_number: Utils.unpackUInt32BE(this.getSerialNumber()),
made: Utils.unpackUInt32BE(this.getMade()),
checksum: checksumHex,
calculated_checksum: calculatedChecksumHex,
signature: signatureHex,
}
// if configured, add configuration to details
if(details.is_configured){
details = {
...details,
configured_spreading_factor: this.getConfiguredSpreadingFactor(),
configured_coding_rate: this.getConfiguredCodingRate(),
configured_tx_power: this.getConfiguredTxPower(),
configured_frequency: this.getConfiguredFrequency(),
configured_bandwidth: this.getConfiguredBandwidth(),
};
}
// if checksum in eeprom does not match checksum calculated from info, it is not provisioned
if(details.checksum !== details.calculated_checksum){
details.is_provisioned = false;
}
return details;
}
}

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,491 @@
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of
* the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in
* writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
* OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing
* permissions and limitations under the License.
*/
'use strict';
export var SerialPolyfillProtocol;
(function (SerialPolyfillProtocol) {
SerialPolyfillProtocol[SerialPolyfillProtocol["UsbCdcAcm"] = 0] = "UsbCdcAcm";
})(SerialPolyfillProtocol || (SerialPolyfillProtocol = {}));
const kSetLineCoding = 0x20;
const kSetControlLineState = 0x22;
const kSendBreak = 0x23;
const kDefaultBufferSize = 255;
const kDefaultDataBits = 8;
const kDefaultParity = 'none';
const kDefaultStopBits = 1;
const kAcceptableDataBits = [16, 8, 7, 6, 5];
const kAcceptableStopBits = [1, 2];
const kAcceptableParity = ['none', 'even', 'odd'];
const kParityIndexMapping = ['none', 'odd', 'even'];
const kStopBitsIndexMapping = [1, 1.5, 2];
const kDefaultPolyfillOptions = {
protocol: SerialPolyfillProtocol.UsbCdcAcm,
usbControlInterfaceClass: 2,
usbTransferInterfaceClass: 10,
};
/**
* Utility function to get the interface implementing a desired class.
* @param {USBDevice} device The USB device.
* @param {number} classCode The desired interface class.
* @return {USBInterface} The first interface found that implements the desired
* class.
* @throws TypeError if no interface is found.
*/
function findInterface(device, classCode) {
const configuration = device.configurations[0];
for (const iface of configuration.interfaces) {
const alternate = iface.alternates[0];
if (alternate.interfaceClass === classCode) {
return iface;
}
}
throw new TypeError(`Unable to find interface with class ${classCode}.`);
}
/**
* Utility function to get an endpoint with a particular direction.
* @param {USBInterface} iface The interface to search.
* @param {USBDirection} direction The desired transfer direction.
* @return {USBEndpoint} The first endpoint with the desired transfer direction.
* @throws TypeError if no endpoint is found.
*/
function findEndpoint(iface, direction) {
const alternate = iface.alternates[0];
for (const endpoint of alternate.endpoints) {
if (endpoint.direction == direction) {
return endpoint;
}
}
throw new TypeError(`Interface ${iface.interfaceNumber} does not have an ` +
`${direction} endpoint.`);
}
/**
* Implementation of the underlying source API[1] which reads data from a USB
* endpoint. This can be used to construct a ReadableStream.
*
* [1]: https://streams.spec.whatwg.org/#underlying-source-api
*/
class UsbEndpointUnderlyingSource {
/**
* Constructs a new UnderlyingSource that will pull data from the specified
* endpoint on the given USB device.
*
* @param {USBDevice} device
* @param {USBEndpoint} endpoint
* @param {function} onError function to be called on error
*/
constructor(device, endpoint, onError) {
this.type = 'bytes';
this.device_ = device;
this.endpoint_ = endpoint;
this.onError_ = onError;
}
/**
* Reads a chunk of data from the device.
*
* @param {ReadableByteStreamController} controller
*/
pull(controller) {
(async () => {
var _a;
let chunkSize;
if (controller.desiredSize) {
const d = controller.desiredSize / this.endpoint_.packetSize;
chunkSize = Math.ceil(d) * this.endpoint_.packetSize;
}
else {
chunkSize = this.endpoint_.packetSize;
}
try {
const result = await this.device_.transferIn(this.endpoint_.endpointNumber, chunkSize);
if (result.status != 'ok') {
controller.error(`USB error: ${result.status}`);
this.onError_();
}
if ((_a = result.data) === null || _a === void 0 ? void 0 : _a.buffer) {
const chunk = new Uint8Array(result.data.buffer, result.data.byteOffset, result.data.byteLength);
controller.enqueue(chunk);
}
}
catch (error) {
controller.error(error.toString());
this.onError_();
}
})();
}
}
/**
* Implementation of the underlying sink API[2] which writes data to a USB
* endpoint. This can be used to construct a WritableStream.
*
* [2]: https://streams.spec.whatwg.org/#underlying-sink-api
*/
class UsbEndpointUnderlyingSink {
/**
* Constructs a new UnderlyingSink that will write data to the specified
* endpoint on the given USB device.
*
* @param {USBDevice} device
* @param {USBEndpoint} endpoint
* @param {function} onError function to be called on error
*/
constructor(device, endpoint, onError) {
this.device_ = device;
this.endpoint_ = endpoint;
this.onError_ = onError;
}
/**
* Writes a chunk to the device.
*
* @param {Uint8Array} chunk
* @param {WritableStreamDefaultController} controller
*/
async write(chunk, controller) {
try {
const result = await this.device_.transferOut(this.endpoint_.endpointNumber, chunk);
if (result.status != 'ok') {
controller.error(result.status);
this.onError_();
}
}
catch (error) {
controller.error(error.toString());
this.onError_();
}
}
}
/** a class used to control serial devices over WebUSB */
export class SerialPort {
/**
* constructor taking a WebUSB device that creates a SerialPort instance.
* @param {USBDevice} device A device acquired from the WebUSB API
* @param {SerialPolyfillOptions} polyfillOptions Optional options to
* configure the polyfill.
*/
constructor(device, polyfillOptions) {
this.polyfillOptions_ = Object.assign(Object.assign({}, kDefaultPolyfillOptions), polyfillOptions);
this.outputSignals_ = {
dataTerminalReady: false,
requestToSend: false,
break: false,
};
this.device_ = device;
this.controlInterface_ = findInterface(this.device_, this.polyfillOptions_.usbControlInterfaceClass);
this.transferInterface_ = findInterface(this.device_, this.polyfillOptions_.usbTransferInterfaceClass);
this.inEndpoint_ = findEndpoint(this.transferInterface_, 'in');
this.outEndpoint_ = findEndpoint(this.transferInterface_, 'out');
}
/**
* Getter for the readable attribute. Constructs a new ReadableStream as
* necessary.
* @return {ReadableStream} the current readable stream
*/
get readable() {
var _a;
if (!this.readable_ && this.device_.opened) {
this.readable_ = new ReadableStream(new UsbEndpointUnderlyingSource(this.device_, this.inEndpoint_, () => {
this.readable_ = null;
}), {
highWaterMark: (_a = this.serialOptions_.bufferSize) !== null && _a !== void 0 ? _a : kDefaultBufferSize,
});
}
return this.readable_;
}
/**
* Getter for the writable attribute. Constructs a new WritableStream as
* necessary.
* @return {WritableStream} the current writable stream
*/
get writable() {
var _a;
if (!this.writable_ && this.device_.opened) {
this.writable_ = new WritableStream(new UsbEndpointUnderlyingSink(this.device_, this.outEndpoint_, () => {
this.writable_ = null;
}), new ByteLengthQueuingStrategy({
highWaterMark: (_a = this.serialOptions_.bufferSize) !== null && _a !== void 0 ? _a : kDefaultBufferSize,
}));
}
return this.writable_;
}
/**
* a function that opens the device and claims all interfaces needed to
* control and communicate to and from the serial device
* @param {SerialOptions} options Object containing serial options
* @return {Promise<void>} A promise that will resolve when device is ready
* for communication
*/
async open(options) {
this.serialOptions_ = options;
this.validateOptions();
try {
await this.device_.open();
if (this.device_.configuration === null) {
await this.device_.selectConfiguration(1);
}
await this.device_.claimInterface(this.controlInterface_.interfaceNumber);
if (this.controlInterface_ !== this.transferInterface_) {
await this.device_.claimInterface(this.transferInterface_.interfaceNumber);
}
await this.setLineCoding();
await this.setSignals({ dataTerminalReady: true });
}
catch (error) {
if (this.device_.opened) {
await this.device_.close();
}
throw new Error('Error setting up device: ' + error.toString());
}
}
/**
* Closes the port.
*
* @return {Promise<void>} A promise that will resolve when the port is
* closed.
*/
async close() {
const promises = [];
if (this.readable_) {
promises.push(this.readable_.cancel());
}
if (this.writable_) {
promises.push(this.writable_.abort());
}
await Promise.all(promises);
this.readable_ = null;
this.writable_ = null;
if (this.device_.opened) {
await this.setSignals({ dataTerminalReady: false, requestToSend: false });
await this.device_.close();
}
}
/**
* Forgets the port.
*
* @return {Promise<void>} A promise that will resolve when the port is
* forgotten.
*/
async forget() {
return this.device_.forget();
}
/**
* A function that returns properties of the device.
* @return {SerialPortInfo} Device properties.
*/
getInfo() {
return {
usbVendorId: this.device_.vendorId,
usbProductId: this.device_.productId,
};
}
/**
* A function used to change the serial settings of the device
* @param {object} options the object which carries serial settings data
* @return {Promise<void>} A promise that will resolve when the options are
* set
*/
reconfigure(options) {
this.serialOptions_ = Object.assign(Object.assign({}, this.serialOptions_), options);
this.validateOptions();
return this.setLineCoding();
}
/**
* Sets control signal state for the port.
* @param {SerialOutputSignals} signals The signals to enable or disable.
* @return {Promise<void>} a promise that is resolved when the signal state
* has been changed.
*/
async setSignals(signals) {
this.outputSignals_ = Object.assign(Object.assign({}, this.outputSignals_), signals);
if (signals.dataTerminalReady !== undefined ||
signals.requestToSend !== undefined) {
// The Set_Control_Line_State command expects a bitmap containing the
// values of all output signals that should be enabled or disabled.
//
// Ref: USB CDC specification version 1.1 §6.2.14.
const value = (this.outputSignals_.dataTerminalReady ? 1 << 0 : 0) |
(this.outputSignals_.requestToSend ? 1 << 1 : 0);
await this.device_.controlTransferOut({
'requestType': 'class',
'recipient': 'interface',
'request': kSetControlLineState,
'value': value,
'index': this.controlInterface_.interfaceNumber,
});
}
if (signals.break !== undefined) {
// The SendBreak command expects to be given a duration for how long the
// break signal should be asserted. Passing 0xFFFF enables the signal
// until 0x0000 is send.
//
// Ref: USB CDC specification version 1.1 §6.2.15.
const value = this.outputSignals_.break ? 0xFFFF : 0x0000;
await this.device_.controlTransferOut({
'requestType': 'class',
'recipient': 'interface',
'request': kSendBreak,
'value': value,
'index': this.controlInterface_.interfaceNumber,
});
}
}
/**
* Checks the serial options for validity and throws an error if it is
* not valid
*/
validateOptions() {
if (!this.isValidBaudRate(this.serialOptions_.baudRate)) {
throw new RangeError('invalid Baud Rate ' + this.serialOptions_.baudRate);
}
if (!this.isValidDataBits(this.serialOptions_.dataBits)) {
throw new RangeError('invalid dataBits ' + this.serialOptions_.dataBits);
}
if (!this.isValidStopBits(this.serialOptions_.stopBits)) {
throw new RangeError('invalid stopBits ' + this.serialOptions_.stopBits);
}
if (!this.isValidParity(this.serialOptions_.parity)) {
throw new RangeError('invalid parity ' + this.serialOptions_.parity);
}
}
/**
* Checks the baud rate for validity
* @param {number} baudRate the baud rate to check
* @return {boolean} A boolean that reflects whether the baud rate is valid
*/
isValidBaudRate(baudRate) {
return baudRate % 1 === 0;
}
/**
* Checks the data bits for validity
* @param {number} dataBits the data bits to check
* @return {boolean} A boolean that reflects whether the data bits setting is
* valid
*/
isValidDataBits(dataBits) {
if (typeof dataBits === 'undefined') {
return true;
}
return kAcceptableDataBits.includes(dataBits);
}
/**
* Checks the stop bits for validity
* @param {number} stopBits the stop bits to check
* @return {boolean} A boolean that reflects whether the stop bits setting is
* valid
*/
isValidStopBits(stopBits) {
if (typeof stopBits === 'undefined') {
return true;
}
return kAcceptableStopBits.includes(stopBits);
}
/**
* Checks the parity for validity
* @param {string} parity the parity to check
* @return {boolean} A boolean that reflects whether the parity is valid
*/
isValidParity(parity) {
if (typeof parity === 'undefined') {
return true;
}
return kAcceptableParity.includes(parity);
}
/**
* sends the options alog the control interface to set them on the device
* @return {Promise} a promise that will resolve when the options are set
*/
async setLineCoding() {
var _a, _b, _c;
// Ref: USB CDC specification version 1.1 §6.2.12.
const buffer = new ArrayBuffer(7);
const view = new DataView(buffer);
view.setUint32(0, this.serialOptions_.baudRate, true);
view.setUint8(4, kStopBitsIndexMapping.indexOf((_a = this.serialOptions_.stopBits) !== null && _a !== void 0 ? _a : kDefaultStopBits));
view.setUint8(5, kParityIndexMapping.indexOf((_b = this.serialOptions_.parity) !== null && _b !== void 0 ? _b : kDefaultParity));
view.setUint8(6, (_c = this.serialOptions_.dataBits) !== null && _c !== void 0 ? _c : kDefaultDataBits);
const result = await this.device_.controlTransferOut({
'requestType': 'class',
'recipient': 'interface',
'request': kSetLineCoding,
'value': 0x00,
'index': this.controlInterface_.interfaceNumber,
}, buffer);
if (result.status != 'ok') {
throw new DOMException('NetworkError', 'Failed to set line coding.');
}
}
}
/** implementation of the global navigator.serial object */
class Serial {
/**
* Requests permission to access a new port.
*
* @param {SerialPortRequestOptions} options
* @param {SerialPolyfillOptions} polyfillOptions
* @return {Promise<SerialPort>}
*/
async requestPort(options, polyfillOptions) {
polyfillOptions = Object.assign(Object.assign({}, kDefaultPolyfillOptions), polyfillOptions);
const usbFilters = [];
if (options && options.filters) {
for (const filter of options.filters) {
const usbFilter = {
classCode: polyfillOptions.usbControlInterfaceClass,
};
if (filter.usbVendorId !== undefined) {
usbFilter.vendorId = filter.usbVendorId;
}
if (filter.usbProductId !== undefined) {
usbFilter.productId = filter.usbProductId;
}
usbFilters.push(usbFilter);
}
}
if (usbFilters.length === 0) {
usbFilters.push({
classCode: polyfillOptions.usbControlInterfaceClass,
});
}
const device = await navigator.usb.requestDevice({ 'filters': usbFilters });
const port = new SerialPort(device, polyfillOptions);
return port;
}
/**
* Get the set of currently available ports.
*
* @param {SerialPolyfillOptions} polyfillOptions Polyfill configuration that
* should be applied to these ports.
* @return {Promise<SerialPort[]>} a promise that is resolved with a list of
* ports.
*/
async getPorts(polyfillOptions) {
polyfillOptions = Object.assign(Object.assign({}, kDefaultPolyfillOptions), polyfillOptions);
const devices = await navigator.usb.getDevices();
const ports = [];
devices.forEach((device) => {
try {
const port = new SerialPort(device, polyfillOptions);
ports.push(port);
}
catch (e) {
// Skip unrecognized port.
}
});
return ports;
}
}
/* an object to be used for starting the serial workflow */
export const serial = new Serial();
//# sourceMappingURL=serial.js.map

View File

File diff suppressed because one or more lines are too long

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB