feat(frontend): add PWA support with manifest, service worker, and audio processing features

This commit is contained in:
2026-01-02 18:20:18 -06:00
parent 22bacfd944
commit eeae2a9821
36 changed files with 17924 additions and 0 deletions

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

View File

@@ -0,0 +1,127 @@
class Codec2Lib {
static arrayBufferToBase64(buffer) {
let binary = "";
let bytes = new Uint8Array(buffer);
for (let byte of bytes) {
binary += String.fromCharCode(byte);
}
return window.btoa(binary);
}
static base64ToArrayBuffer(base64) {
let binary = window.atob(base64);
let bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
static readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsArrayBuffer(file);
});
}
static runDecode(mode, data) {
return new Promise((resolve, reject) => {
const module = {
arguments: [mode, "input.bit", "output.raw"],
preRun: () => {
module.FS.writeFile("input.bit", new Uint8Array(data));
},
postRun: () => {
let buffer = module.FS.readFile("output.raw", {
encoding: "binary",
});
resolve(buffer);
},
};
createC2Dec(module);
});
}
static runEncode(mode, data) {
return new Promise((resolve, reject) => {
const module = {
arguments: [mode, "input.raw", "output.bit"],
preRun: () => {
module.FS.writeFile("input.raw", new Uint8Array(data));
},
postRun: () => {
let buffer = module.FS.readFile("output.bit", {
encoding: "binary",
});
resolve(buffer);
},
};
createC2Enc(module);
});
}
static rawToWav(buffer) {
return new Promise((resolve, reject) => {
const module = {
arguments: [
"-r",
"8000",
"-L",
"-e",
"signed-integer",
"-b",
"16",
"-c",
"1",
"input.raw",
"output.wav",
],
preRun: () => {
module.FS.writeFile("input.raw", new Uint8Array(buffer));
},
postRun: () => {
let output = module.FS.readFile("output.wav", {
encoding: "binary",
});
resolve(output);
},
};
SOXModule(module);
});
}
static audioFileToRaw(buffer, filename) {
return new Promise((resolve, reject) => {
const module = {
arguments: [
filename,
"-r",
"8000",
"-L",
"-e",
"signed-integer",
"-b",
"16",
"-c",
"1",
"output.raw",
],
preRun: () => {
module.FS.writeFile(filename, new Uint8Array(buffer));
},
postRun: () => {
let output = module.FS.readFile("output.raw", {
encoding: "binary",
});
resolve(output);
},
};
SOXModule(module);
});
}
}

View File

@@ -0,0 +1,92 @@
/**
* A simple class for recording microphone input and returning the audio encoded in codec2
*/
class Codec2MicrophoneRecorder {
constructor() {
this.sampleRate = 8000;
this.codec2Mode = "1200";
this.audioChunks = [];
this.audioContext = null;
this.audioWorkletNode = null;
this.microphoneMediaStream = null;
this.mediaStreamSource = null;
}
async start() {
try {
// load audio worklet module
this.audioContext = new AudioContext({ sampleRate: this.sampleRate });
await this.audioContext.audioWorklet.addModule('assets/js/codec2-emscripten/processor.js');
this.audioWorkletNode = new AudioWorkletNode(this.audioContext, 'audio-processor');
// handle audio received from audio worklet
this.audioWorkletNode.port.onmessage = async (event) => {
this.audioChunks.push(event.data);
};
// request access to the microphone
this.microphoneMediaStream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
// send mic audio to audio worklet
this.mediaStreamSource = this.audioContext.createMediaStreamSource(this.microphoneMediaStream);
this.mediaStreamSource.connect(this.audioWorkletNode);
// successfully started recording
return true;
} catch(e) {
console.log(e);
return false;
}
}
async stop() {
// disconnect media stream source
if(this.mediaStreamSource){
this.mediaStreamSource.disconnect();
}
// stop using microphone
if(this.microphoneMediaStream){
this.microphoneMediaStream.getTracks().forEach(track => track.stop());
}
// disconnect the audio worklet node
if(this.audioWorkletNode){
this.audioWorkletNode.disconnect();
}
// close audio context
if(this.audioContext && this.audioContext.state !== "closed"){
this.audioContext.close();
}
// concatenate all audio chunks into a single array
var fullAudio = [];
for(const chunk of this.audioChunks){
fullAudio = [
...fullAudio,
...chunk,
]
}
// convert audio to wav
const buffer = WavEncoder.encodeWAV(fullAudio, this.sampleRate);
// convert wav audio to codec2
const rawBuffer = await Codec2Lib.audioFileToRaw(buffer, "audio.wav");
const encoded = await Codec2Lib.runEncode(this.codec2Mode, rawBuffer);
return encoded;
}
}

View File

@@ -0,0 +1,127 @@
<html>
<body>
<div>
<div style="margin-bottom:1rem;">
<div>Select a *.wav audio file.</div>
<input id="file-input" type="file" accept="audio/wav"/>
</div>
<div style="margin-bottom:1rem;">
<span>Select Codec2 Mode:</span>
<select id="codec-mode">
<option value="3200">3200</option>
<option value="2400">2400</option>
<option value="1600">1600</option>
<option value="1400">1400</option>
<option value="1300">1300</option>
<option value="1200">1200</option>
<option value="700C" selected>700C</option>
<option value="450">450</option>
<option value="450PWB">450PWB</option>
</select>
</div>
<div style="margin-bottom:1rem;">
<div>Click to encode audio file as Codec2</div>
<button type="submit" onclick="encode()">Encode</button>
</div>
<div style="margin-bottom:1rem;">
<div>Codec2 audio represented as Base64</div>
<textarea id="encoded-output" style="width:500px" rows="8"></textarea>
</div>
<div style="margin-bottom:1rem;">
<div>Click to decode Codec2 audio back to WAVE audio</div>
<button type="submit" onclick="decode()">Decode</button>
</div>
<div style="margin-bottom:1rem;">
<div>Decoded audio available to listen to</div>
<audio id="decoded-audio" controls></audio>
</div>
<div style="margin-bottom:1rem;">
<div>Input File Size: <span id="input-size">0 Bytes</span></div>
<div>Encoded Data Size: <span id="encoded-size">0 Bytes</span></div>
<div>Decoded Data Size: <span id="decoded-size">0 Bytes</span></div>
</div>
</div>
<script src="c2enc.js"></script>
<script src="c2dec.js"></script>
<script src="sox.js"></script>
<script src="codec2-lib.js"></script>
<script>
// find elements
const codecModeElement = document.getElementById("codec-mode");
const encodedOutputElement = document.getElementById("encoded-output");
const fileInputElement = document.getElementById("file-input");
const decodedAudioElement = document.getElementById("decoded-audio");
const inputSizeElement = document.getElementById("input-size");
const encodedSizeElement = document.getElementById("encoded-size");
const decodedSizeElement = document.getElementById("decoded-size");
// update file size stats on change
fileInputElement.onchange = function() {
if(fileInputElement.files.length > 0){
const file = fileInputElement.files[0];
inputSizeElement.innerText = formatBytes(file.size);
}
}
async function encode() {
const file = fileInputElement.files[0];
if(!file){
alert("select a file first");
return;
}
const mode = codecModeElement.value;
const buffer = await Codec2Lib.readFileAsArrayBuffer(file);
const rawBuffer = await Codec2Lib.audioFileToRaw(buffer, file.name || "input.wav");
const encoded = await Codec2Lib.runEncode(mode, rawBuffer);
encodedOutputElement.value = Codec2Lib.arrayBufferToBase64(encoded);
inputSizeElement.innerText = formatBytes(file.size);
encodedSizeElement.innerText = formatBytes(encoded.length);
}
async function decode() {
const mode = codecModeElement.value;
const input = encodedOutputElement.value;
const encoded = Codec2Lib.base64ToArrayBuffer(input);
const decodedRaw = await Codec2Lib.runDecode(mode, encoded);
const decodedWav = await Codec2Lib.rawToWav(decodedRaw);
decodedAudioElement.src = URL.createObjectURL(new Blob([decodedWav], { type: "audio/wav" }));
decodedSizeElement.innerText = formatBytes(decodedWav.length);
}
function formatBytes(bytes) {
if(bytes === 0){
return '0 Bytes';
}
const k = 1024;
const decimals = 0;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
}
</script>
</body>
</html>

View File

@@ -0,0 +1,55 @@
class AudioProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.bufferSize = 4096; // Adjust the buffer size as needed
this.sampleRate = 8000; // Target sample rate
this.inputBuffer = new Float32Array(this.bufferSize);
this.bufferIndex = 0;
}
process(inputs, outputs, parameters) {
const input = inputs[0];
if (input.length > 0) {
const inputData = input[0];
for (let i = 0; i < inputData.length; i++) {
if (this.bufferIndex < this.bufferSize) {
this.inputBuffer[this.bufferIndex++] = inputData[i];
}
if (this.bufferIndex === this.bufferSize) {
// Downsample the buffer and send to the main thread
const downsampledBuffer = this.downsampleBuffer(this.inputBuffer, this.sampleRate);
this.port.postMessage(downsampledBuffer);
this.bufferIndex = 0;
}
}
}
return true;
}
downsampleBuffer(buffer, targetSampleRate) {
if (targetSampleRate === this.sampleRate) {
return buffer;
}
const sampleRateRatio = this.sampleRate / targetSampleRate;
const newLength = Math.round(buffer.length / sampleRateRatio);
const result = new Float32Array(newLength);
let offsetResult = 0;
let offsetBuffer = 0;
while (offsetResult < result.length) {
const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
let accum = 0;
let count = 0;
for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
accum += buffer[i];
count++;
}
result[offsetResult] = accum / count;
offsetResult++;
offsetBuffer = nextOffsetBuffer;
}
return result;
}
}
registerProcessor('audio-processor', AudioProcessor);

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

View File

@@ -0,0 +1,47 @@
class WavEncoder {
static encodeWAV(samples, sampleRate = 8000, numChannels = 1) {
const buffer = new ArrayBuffer(44 + samples.length * 2);
const view = new DataView(buffer);
// RIFF chunk descriptor
this.writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + samples.length * 2, true); // file length
this.writeString(view, 8, 'WAVE');
// fmt sub-chunk
this.writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true); // sub-chunk size
view.setUint16(20, 1, true); // audio format (1 = PCM)
view.setUint16(22, numChannels, true); // number of channels
view.setUint32(24, sampleRate, true); // sample rate
view.setUint32(28, sampleRate * numChannels * 2, true); // byte rate
view.setUint16(32, numChannels * 2, true); // block align
view.setUint16(34, 16, true); // bits per sample
// data sub-chunk
this.writeString(view, 36, 'data');
view.setUint32(40, samples.length * 2, true); // data chunk length
// write the PCM samples
this.floatTo16BitPCM(view, 44, samples);
return buffer;
}
static writeString(view, offset, string) {
for(let i = 0; i < string.length; i++){
view.setUint8(offset + i, string.charCodeAt(i));
}
}
static floatTo16BitPCM(output, offset, input) {
for(let i = 0; i < input.length; i++, offset += 2){
const s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
syntax = "proto2";
// raw payload sent over the websocket
message AudioCallPayload {
optional AudioData audioData = 1;
}
// a message containing some sort of audio data
message AudioData {
optional Codec2Audio codec2Audio = 1;
}
// audio encoded with codec2
message Codec2Audio {
required Mode mode = 1; // codec2 mode used for encoding
required bytes encoded = 2; // audio encoded as codec2
enum Mode {
MODE_3200 = 0;
MODE_2400 = 1;
MODE_1600 = 2;
MODE_1400 = 3;
MODE_1300 = 4;
MODE_1200 = 5;
MODE_700C = 6;
MODE_450 = 7;
MODE_450PWB = 8;
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,17 @@
{
"name": "MeshChat",
"short_name": "MeshChat",
"description": "A simple mesh network communications app powered by the Reticulum Network Stack.",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "/favicons/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"display": "standalone",
"theme_color": "#FFFFFF",
"background_color": "#FFFFFF"
}

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

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because one or more lines are too long

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

View File

@@ -0,0 +1,2 @@
// Service worker required for PWA installability.
// A fetch handler is not required - only the service worker registration is needed.