diff --git a/buildspec.yml b/buildspec.yml
index cd62a67..f9e9693 100644
--- a/buildspec.yml
+++ b/buildspec.yml
@@ -10,6 +10,7 @@ phases:
build:
commands:
- yarn build
+ - cp dist/servers-beta.json dist/servers.json
artifacts:
files:
- '**/*'
diff --git a/package.json b/package.json
index a7a2e51..f734292 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "duinoapp-client",
- "version": "3.2.3",
+ "version": "3.3.1",
"author": "Fraser Bullock",
"license": "GPL-3.0",
"private": true,
@@ -17,6 +17,7 @@
"avrgirl-arduino": "https://github.com/duinoapp/avrgirl-arduino.git#browser-port-forwarding",
"core-js": "^3.6.5",
"country-code-emoji": "^2.2.0",
+ "crypto-js": "^4.0.0",
"dayjs": "^1.8.28",
"debounce-promise": "^3.1.2",
"feathers-localstorage": "^5.1.1",
@@ -26,6 +27,7 @@
"intel-hex": "^0.1.2",
"lodash": "^4.17.15",
"monaco-editor-webpack-plugin": "<=1.8.2",
+ "pako": "^2.0.3",
"register-service-worker": "^1.7.1",
"roboto-fontface": "*",
"sass": "^1.26.7",
diff --git a/public/servers-beta.json b/public/servers-beta.json
new file mode 100644
index 0000000..5110ddd
--- /dev/null
+++ b/public/servers-beta.json
@@ -0,0 +1,3 @@
+[
+ "https://compile-beta.duino.app"
+]
\ No newline at end of file
diff --git a/public/stubs/esp32.json b/public/stubs/esp32.json
new file mode 100644
index 0000000..4e48a85
--- /dev/null
+++ b/public/stubs/esp32.json
@@ -0,0 +1 @@
+{"text": "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", "text_start": 1074520064, "entry": 1074521516, "data": "CMD8Pw==", "data_start": 1073605544}
\ No newline at end of file
diff --git a/public/stubs/esp32c3.json b/public/stubs/esp32c3.json
new file mode 100644
index 0000000..80dad8c
--- /dev/null
+++ b/public/stubs/esp32c3.json
@@ -0,0 +1 @@
+{"text": "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", "text_start": 1077411840, "entry": 1077413488, "data": "DEDIPw==", "data_start": 1070164904}
\ No newline at end of file
diff --git a/public/stubs/esp32h2.json b/public/stubs/esp32h2.json
new file mode 100644
index 0000000..67375e8
--- /dev/null
+++ b/public/stubs/esp32h2.json
@@ -0,0 +1 @@
+{"text": "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", "text_start": 1077411840, "entry": 1077413328, "data": "DEDIPw==", "data_start": 1070164904}
\ No newline at end of file
diff --git a/public/stubs/esp32s2.json b/public/stubs/esp32s2.json
new file mode 100644
index 0000000..74a8576
--- /dev/null
+++ b/public/stubs/esp32s2.json
@@ -0,0 +1 @@
+{"text": "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", "text_start": 1073905664, "entry": 1073907540, "data": "CAD9Pw==", "data_start": 1073622004}
\ No newline at end of file
diff --git a/public/stubs/esp32s3.json b/public/stubs/esp32s3.json
new file mode 100644
index 0000000..ba7d1ee
--- /dev/null
+++ b/public/stubs/esp32s3.json
@@ -0,0 +1 @@
+{"text": "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", "text_start": 1077379072, "entry": 1077381116, "data": "CADKPw==", "data_start": 1070279592}
\ No newline at end of file
diff --git a/public/stubs/esp8266.json b/public/stubs/esp8266.json
new file mode 100644
index 0000000..6ae7a2e
--- /dev/null
+++ b/public/stubs/esp8266.json
@@ -0,0 +1 @@
+{"text": "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", "text_start": 1074843648, "entry": 1074843652, "data": "CIH+PwUFBAACAwcAAwMLALnXEEDv1xBAHdgQQLrYEEBo5xBAHtkQQHTZEEDA2RBAaOcQQILaEED/2hBAwNsQQGjnEEBo5xBAWNwQQGjnEEA33xBAAOAQQDvgEEBo5xBAaOcQQNfgEEBo5xBAv+EQQGXiEECj4xBAY+QQQDTlEEBo5xBAaOcQQGjnEEBo5xBAYuYQQGjnEEBX5xBAkN0QQI/YEECm5RBAq9oQQPzZEEBo5xBA7OYQQDHnEEBo5xBAaOcQQGjnEEBo5xBAaOcQQGjnEEBo5xBAaOcQQCLaEEBf2hBAvuUQQAEAAAACAAAAAwAAAAQAAAAFAAAABwAAAAkAAAANAAAAEQAAABkAAAAhAAAAMQAAAEEAAABhAAAAgQAAAMEAAAABAQAAgQEAAAECAAABAwAAAQQAAAEGAAABCAAAAQwAAAEQAAABGAAAASAAAAEwAAABQAAAAWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAgAAAAIAAAADAAAAAwAAAAQAAAAEAAAABQAAAAUAAAAGAAAABgAAAAcAAAAHAAAACAAAAAgAAAAJAAAACQAAAAoAAAAKAAAACwAAAAsAAAAMAAAADAAAAA0AAAANAAAAAAAAAAAAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAANAAAADwAAABEAAAATAAAAFwAAABsAAAAfAAAAIwAAACsAAAAzAAAAOwAAAEMAAABTAAAAYwAAAHMAAACDAAAAowAAAMMAAADjAAAAAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAAEAAAABAAAAAQAAAAEAAAABQAAAAUAAAAFAAAABQAAAAAAAAAAAAAAAAAAABAREgAIBwkGCgULBAwDDQIOAQ8AAQEAAAEAAAAEAAAA", "data_start": 1073720488}
\ No newline at end of file
diff --git a/service-worker.js b/service-worker.js
index 0c5e1d3..6c70ce6 100644
--- a/service-worker.js
+++ b/service-worker.js
@@ -3,7 +3,7 @@
workbox.core.setCacheNameDetails({ prefix: 'd4' });
// Do not touch this line
-const LATEST_VERSION = '3.2.3';
+const LATEST_VERSION = '3.3.1';
self.addEventListener('activate', (event) => {
console.log(`%c ${LATEST_VERSION} `, 'background: #ddd; color: #0000ff');
diff --git a/src/components/files/editor.vue b/src/components/files/editor.vue
index f06239c..7e36ee2 100644
--- a/src/components/files/editor.vue
+++ b/src/components/files/editor.vue
@@ -56,6 +56,6 @@ export default {
diff --git a/src/components/libs/libs-table.vue b/src/components/libs/libs-table.vue
index 1c2db82..66327dd 100644
--- a/src/components/libs/libs-table.vue
+++ b/src/components/libs/libs-table.vue
@@ -133,9 +133,11 @@ export default {
const res = await this.$compiler.librariesSearch(
this.search,
itemsPerPage,
- page - 1,
- sortBy[0],
- sortDesc[0],
+ {
+ skip: page - 1,
+ sortBy: sortBy[0],
+ sortDesc: sortDesc[0],
+ },
);
this.items = res.data;
this.total = res.total;
diff --git a/src/components/settings/monitor.vue b/src/components/settings/monitor.vue
new file mode 100644
index 0000000..2a36fcb
--- /dev/null
+++ b/src/components/settings/monitor.vue
@@ -0,0 +1,65 @@
+
+
+
+ Serial Monitor
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/terminal/terminal.vue b/src/components/terminal/terminal.vue
index ec8e145..a5ce640 100644
--- a/src/components/terminal/terminal.vue
+++ b/src/components/terminal/terminal.vue
@@ -39,6 +39,7 @@ export default {
this.$terminal.write(val.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'));
},
clear() {
+ this.$terminal.write('\r\n');
this.$terminal.clear();
},
focus() {
@@ -50,6 +51,9 @@ export default {
fit() {
this.$terminal.fit();
},
+ cols() {
+ return this.$terminal.cols;
+ },
},
};
diff --git a/src/plugins/compile-server.js b/src/plugins/compile-server.js
index c6a7ef9..8f0395a 100644
--- a/src/plugins/compile-server.js
+++ b/src/plugins/compile-server.js
@@ -148,10 +148,14 @@ class CompileServer extends EventEmitter {
console.log('loading finished');
}
- async librariesSearch(search, limit = 10, skip = 0, sortBy = 'name', sortDesc = false) {
+ async librariesSearch(search, limit = 10, {
+ skip = 0, sortBy = 'name', sortDesc = false, exact = false,
+ } = {}) {
await this.initPromise;
const e = encodeURIComponent;
- const query = `?search=${e(search ?? '')}&limit=${limit}&skip=${skip}&sortBy=${e(sortBy)}&sortDesc=${sortDesc}`;
+ const query = `?search=${
+ e(search ?? '')
+ }&limit=${limit}&skip=${skip}&sortBy=${e(sortBy)}&sortDesc=${sortDesc}&exact=${exact}`;
const res = await this.serverReq(`info/libraries${query}`);
return res || {
limit, skip, total: 0, data: [],
@@ -163,7 +167,7 @@ class CompileServer extends EventEmitter {
const board = store.getters['boards/find']({ query: { uuid: store.getters.currentBoard } }).data[0];
if (!board) return 'arduino:avr:uno';
return Object.keys(board.config)
- .filter((i) => !board.config_options
+ .filter((i) => board.config[i] && !board.config_options
.find((c) => c.option === i).values
.find((v) => v.value === board.config[i]).isDefault)
.reduce((a, i) => `${a}:${i}=${board.config[i]}`, board.fqbn);
@@ -180,8 +184,8 @@ class CompileServer extends EventEmitter {
async _getLibs({ libraries }) {
if (!libraries?.length) return [];
- const search = libraries.map(({ name }) => name.replaceAll(' ', '.')).join(' ');
- const { data } = await this.librariesSearch(search, libraries.length);
+ const search = libraries.map(({ name }) => name.replaceAll(' ', '.')).join(',');
+ const { data } = await this.librariesSearch(search, libraries.length, { exact: true });
return libraries.map((lib) => ({
...lib,
url: data
@@ -198,13 +202,13 @@ class CompileServer extends EventEmitter {
.map((f) => ({ content: f.body, name: `${project.ref}/${f.name}` }));
this.emit('console.progress', { percent: 0, message: 'Initialising Libraries...' });
const libs = await this._getLibs(project);
- if (libs.length) {
- await this.serverReq('libraries/cache', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ libs }),
- });
- }
+ // if (libs.length) {
+ // await this.serverReq('libraries/cache', {
+ // method: 'POST',
+ // headers: { 'Content-Type': 'application/json' },
+ // body: JSON.stringify({ libs }),
+ // });
+ // }
this.emit('console.progress', { percent: 0.25 * mod, message: 'Compiling code...' });
const req = {
fqbn: this._getFqbn(),
@@ -236,8 +240,11 @@ class CompileServer extends EventEmitter {
throw new Error(res.error);
}
this.emit('console.log', res.log);
+ if (res.log.includes('In function \'spiTransferBytesNL\':')) {
+ this.emit('console.log', '> Please note that the above "-Wincompatible-pointer-types" warning is only a warning.\r\n');
+ }
this.emit('console.progress', { percent: 1, message: 'Done!' });
- return res.hex;
+ return res;
// this.disconnect();
// return res.hex;
}
@@ -252,11 +259,12 @@ class CompileServer extends EventEmitter {
}
const flags = this._getFlags();
try {
- const hex = await this.compile(1, false);
+ const res = await this.compile(1, false);
+ if (!res.hex && !res.files) throw new Error('Failed to compile code');
this.emit('console.progress', { percent: 0.5, message: 'Uploading code...' });
// eslint-disable-next-line no-console
// console.log(this.Vue.$serial);
- await this.Vue.$uploader.upload(hex, { ...flags });
+ await this.Vue.$uploader.upload(res, { ...flags });
this.emit('console.progress', { percent: 1.0, message: 'Done!' });
} catch (err) {
// eslint-disable-next-line no-console
diff --git a/src/plugins/serial/base-serial.js b/src/plugins/serial/base-serial.js
index a9f0c3f..9e69500 100644
--- a/src/plugins/serial/base-serial.js
+++ b/src/plugins/serial/base-serial.js
@@ -1,6 +1,7 @@
/* eslint-disable no-unused-vars */
/* eslint-disable class-methods-use-this */
import EventEmitter from 'events';
+import store from '@/store';
/*
Interface should emit the following:
@@ -28,7 +29,6 @@ class BaseSerial extends EventEmitter {
this.mute = false;
this.baud = Number(window.localStorage.currentBaudRate) || 115200;
this.lastBaud = 115200;
- this.encoding = 'ascii';
this.devices = [];
this.currentDevice = null;
this.connected = false;
@@ -39,6 +39,11 @@ class BaseSerial extends EventEmitter {
// console.log('debug', this.DEBUG);
}
+ get encoding() {
+ const [settings] = store.getters['settings/find']({ query: { key: 'monitor' } }).data;
+ return settings?.value?.encoding || 'ascii';
+ }
+
install(Vue) {
// eslint-disable-next-line no-param-reassign
Vue.$serial = this;
diff --git a/src/plugins/uploader/avrgirl.js b/src/plugins/uploader/avrgirl.js
index 0b45c01..cb0087d 100644
--- a/src/plugins/uploader/avrgirl.js
+++ b/src/plugins/uploader/avrgirl.js
@@ -11,7 +11,7 @@ const getConfig = (board) => {
const isValid = (board) => true || !!getConfig(board)?.protocol;
-const upload = (hex, board, serial, config) => new Promise((resolve, reject) => {
+const upload = ({ hex }, board, serial, config) => new Promise((resolve, reject) => {
const avrgirl = new Avrgirl({
board: getConfig(board),
serialPort: serial,
diff --git a/src/plugins/uploader/esptool/ESPLoader.js b/src/plugins/uploader/esptool/ESPLoader.js
new file mode 100644
index 0000000..518f98a
--- /dev/null
+++ b/src/plugins/uploader/esptool/ESPLoader.js
@@ -0,0 +1,1603 @@
+/* eslint-disable no-await-in-loop */
+/* eslint-disable no-throw-literal */
+/* eslint-disable prefer-destructuring */
+/* eslint-disable no-plusplus */
+/* eslint-disable class-methods-use-this */
+/* eslint-disable no-bitwise */
+/* eslint-disable camelcase */
+/* eslint-disable max-classes-per-file */
+import pako from 'pako';
+import CryptoJS from 'crypto-js';
+
+const stubCache = {};
+
+class ESP8266ROM {
+ static CHIP_NAME = 'ESP8266';
+
+ static IS_STUB = true;
+
+ static CHIP_DETECT_MAGIC_VALUE = 0xfff0c101;
+
+ static FLASH_WRITE_SIZE = 0x400;
+
+ // OTP ROM addresses
+ static ESP_OTP_MAC0 = 0x3ff00050
+
+ static ESP_OTP_MAC1 = 0x3ff00054
+
+ static ESP_OTP_MAC3 = 0x3ff0005c
+
+ static SPI_REG_BASE = 0x60000200
+
+ static SPI_USR_OFFS = 0x1c
+
+ static SPI_USR1_OFFS = 0x20
+
+ static SPI_USR2_OFFS = 0x24
+
+ static SPI_MOSI_DLEN_OFFS = null
+
+ static SPI_MISO_DLEN_OFFS = null
+
+ static SPI_W0_OFFS = 0x40
+
+ static UART_CLKDIV_REG = 0x60000014
+
+ static XTAL_CLK_DIVIDER = 2
+
+ static FLASH_SIZES = {
+ '512KB': 0x00,
+ '256KB': 0x10,
+ '1MB': 0x20,
+ '2MB': 0x30,
+ '4MB': 0x40,
+ '2MB-c1': 0x50,
+ '4MB-c1': 0x60,
+ '8MB': 0x80,
+ '16MB': 0x90,
+ }
+
+ static BOOTLOADER_FLASH_OFFSET = 0
+
+ static MEMORY_MAP = [[0x3FF00000, 0x3FF00010, 'DPORT'],
+ [0x3FFE8000, 0x40000000, 'DRAM'],
+ [0x40100000, 0x40108000, 'IRAM'],
+ [0x40201010, 0x402E1010, 'IROM']]
+
+ static get_efuses = async (loader) => {
+ // Return the 128 bits of ESP8266 efuse as a single integer
+ const result = (await loader.read_reg({ addr: 0x3ff0005c }) << 96)
+ | (await loader.read_reg({ addr: 0x3ff00058 }) << 64)
+ | (await loader.read_reg({ addr: 0x3ff00054 }) << 32)
+ | await loader.read_reg({ addr: 0x3ff00050 });
+ return result;
+ }
+
+ static _get_flash_size = (efuses) => {
+ // rX_Y = EFUSE_DATA_OUTX[Y]
+ const r0_4 = (efuses & (1 << 4)) !== 0;
+ const r3_25 = (efuses & (1 << 121)) !== 0;
+ const r3_26 = (efuses & (1 << 122)) !== 0;
+ const r3_27 = (efuses & (1 << 123)) !== 0;
+
+ if (r0_4 && !r3_25) {
+ if (!r3_27 && !r3_26) {
+ return 1;
+ } if (!r3_27 && r3_26) {
+ return 2;
+ }
+ }
+ if (!r0_4 && r3_25) {
+ if (!r3_27 && !r3_26) {
+ return 2;
+ } if (!r3_27 && r3_26) {
+ return 4;
+ }
+ }
+ return -1;
+ }
+
+ static get_chip_description = async (loader) => {
+ const efuses = await this.get_efuses(loader);
+ const is_8285 = (efuses & (((1 << 4) | 1) << 80)) !== 0; // One or the other efuse bit is set for ESP8285
+ if (is_8285) {
+ const flash_size = this._get_flash_size(efuses);
+ const max_temp = (efuses & (1 << 5)) !== 0; // This efuse bit identifies the max flash temperature
+ const chip_name = {
+ 1: max_temp ? 'ESP8285H08' : 'ESP8285N08',
+ 2: max_temp ? 'ESP8285H16' : 'ESP8285N16',
+ }[flash_size] || 'ESP8285';
+ return chip_name;
+ }
+ return 'ESP8266EX';
+ }
+
+ static get_chip_features = async (loader) => {
+ const features = ['WiFi'];
+ if (await this.get_chip_description(loader) === 'ESP8285') {
+ features.push('Embedded Flash');
+ }
+ return features;
+ }
+
+ static flash_spi_attach = async (loader, hspi_arg) => {
+ if (this.IS_STUB) {
+ await super.flash_spi_attach(loader, hspi_arg);
+ } else {
+ // ESP8266 ROM has no flash_spi_attach command in serial protocol,
+ // but flash_begin will do it
+ await loader.flash_begin(0, 0);
+ }
+ }
+
+ static flash_set_parameters = async (loader, size) => {
+ // not implemented in ROM, but OK to silently skip for ROM
+ if (this.IS_STUB) {
+ await super.flash_set_parameters(loader, size);
+ }
+ }
+
+ static chip_id = async (loader) => {
+ // Read Chip ID from efuse - the equivalent of the SDK system_get_chip_id() function
+ const id0 = await loader.read_reg({ addr: this.ESP_OTP_MAC0 });
+ const id1 = await loader.read_reg({ addr: this.ESP_OTP_MAC1 });
+ return (id0 >> 24) | ((id1 & 0xffffff) << 8);
+ }
+
+ static read_mac = async (loader) => {
+ // Read MAC from OTP ROM
+ const mac0 = await loader.read_reg({ addr: this.ESP_OTP_MAC0 });
+ const mac1 = await loader.read_reg({ addr: this.ESP_OTP_MAC1 });
+ const mac3 = await loader.read_reg({ addr: this.ESP_OTP_MAC3 });
+ let oui;
+ if (mac3 !== 0) {
+ oui = ((mac3 >> 16) & 0xff, (mac3 >> 8) & 0xff, mac3 & 0xff);
+ } else if (((mac1 >> 16) & 0xff) === 0) {
+ oui = (0x18, 0xfe, 0x34);
+ } else if (((mac1 >> 16) & 0xff) === 1) {
+ oui = (0xac, 0xd0, 0x74);
+ } else {
+ throw ('Unknown OUI');
+ }
+ return oui + ((mac1 >> 8) & 0xff, mac1 & 0xff, (mac0 >> 24) & 0xff);
+ }
+
+ static get_erase_size = (offset, size) => size
+
+ // eslint-disable-next-line no-unused-vars
+ static get_crystal_freq = async (loader) => 40
+}
+
+class ESP32ROM {
+ static CHIP_NAME = 'ESP32';
+
+ static IS_STUB = true;
+
+ static IMAGE_CHIP_ID = 0;
+
+ static CHIP_DETECT_MAGIC_VALUE = 0x00f01d83;
+
+ static EFUSE_RD_REG_BASE = 0x3ff5a000;
+
+ static DR_REG_SYSCON_BASE = 0x3ff66000;
+
+ static UART_CLKDIV_REG = 0x3ff40014;
+
+ static UART_CLKDIV_MASK = 0xFFFFF;
+
+ static UART_DATE_REG_ADDR = 0x60000078;
+
+ static XTAL_CLK_DIVIDER= 1;
+
+ static FLASH_WRITE_SIZE = 0x400;
+
+ static BOOTLOADER_FLASH_OFFSET = 0x1000;
+
+ static FLASH_SIZES = {
+ '1MB': 0x00, '2MB': 0x10, '4MB': 0x20, '8MB': 0x30, '16MB': 0x40,
+ };
+
+ static SPI_REG_BASE = 0x3ff42000;
+
+ static SPI_USR_OFFS = 0x1c;
+
+ static SPI_USR1_OFFS = 0x20;
+
+ static SPI_USR2_OFFS = 0x24;
+
+ static SPI_W0_OFFS = 0x80;
+
+ static SPI_MOSI_DLEN_OFFS = 0x28;
+
+ static SPI_MISO_DLEN_OFFS = 0x2c;
+
+ static read_efuse = async (loader, offset) => {
+ const addr = this.EFUSE_RD_REG_BASE + (4 * offset);
+ // console.log(`Read efuse ${addr}`);
+ return loader.read_reg({ addr });
+ }
+
+ static get_pkg_version = async (loader) => {
+ const word3 = await this.read_efuse(loader, 3);
+ let pkg_version = (word3 >> 9) & 0x07;
+ pkg_version += ((word3 >> 2) & 0x1) << 3;
+ return pkg_version;
+ }
+
+ static get_chip_revision = async (loader) => {
+ const word3 = await this.read_efuse(loader, 3);
+ const word5 = await this.read_efuse(loader, 5);
+ const apb_ctl_date = await loader.read_reg({ addr: this.DR_REG_SYSCON_BASE + 0x7C });
+
+ const rev_bit0 = (word3 >> 15) & 0x1;
+ const rev_bit1 = (word5 >> 20) & 0x1;
+ const rev_bit2 = (apb_ctl_date >> 31) & 0x1;
+ if (rev_bit0 !== 0) {
+ if (rev_bit1 !== 0) {
+ if (rev_bit2 !== 0) {
+ return 3;
+ }
+ return 2;
+ }
+ return 1;
+ }
+ return 0;
+ }
+
+ static get_chip_description = async (loader) => {
+ const chip_desc = ['ESP32-D0WDQ6', 'ESP32-D0WD', 'ESP32-D2WD', '', 'ESP32-U4WDH', 'ESP32-PICO-D4', 'ESP32-PICO-V3-02'];
+ let chip_name = '';
+ const pkg_version = await this.get_pkg_version(loader);
+ const chip_revision = await this.get_chip_revision(loader);
+ const rev3 = (chip_revision === 3);
+ const single_core = await this.read_efuse(loader, 3) & (1 << 0);
+
+ if (single_core !== 0) {
+ chip_desc[0] = 'ESP32-S0WDQ6';
+ chip_desc[1] = 'ESP32-S0WD';
+ }
+ if (rev3) {
+ chip_desc[5] = 'ESP32-PICO-V3';
+ }
+ if (pkg_version >= 0 && pkg_version <= 6) {
+ chip_name = chip_desc[pkg_version];
+ } else {
+ chip_name = 'Unknown ESP32';
+ }
+
+ if (rev3 && (pkg_version === 0 || pkg_version === 1)) {
+ chip_name += '-V3';
+ }
+ return `${chip_name} (revision ${chip_revision})`;
+ }
+
+ static get_chip_features = async (loader) => {
+ const features = ['Wi-Fi'];
+ const word3 = await this.read_efuse(loader, 3);
+
+ const chip_ver_dis_bt = word3 & (1 << 1);
+ if (chip_ver_dis_bt === 0) {
+ features.push(' BT');
+ }
+
+ const chip_ver_dis_app_cpu = word3 & (1 << 0);
+ if (chip_ver_dis_app_cpu !== 0) {
+ features.push(' Single Core');
+ } else {
+ features.push(' Dual Core');
+ }
+
+ const chip_cpu_freq_rated = word3 & (1 << 13);
+ if (chip_cpu_freq_rated !== 0) {
+ const chip_cpu_freq_low = word3 & (1 << 12);
+ if (chip_cpu_freq_low !== 0) {
+ features.push(' 160MHz');
+ } else {
+ features.push(' 240MHz');
+ }
+ }
+
+ const pkg_version = await this.get_pkg_version(loader);
+ if ([2, 4, 5, 6].includes(pkg_version)) {
+ features.push(' Embedded Flash');
+ }
+
+ if (pkg_version === 6) {
+ features.push(' Embedded PSRAM');
+ }
+
+ const word4 = await this.read_efuse(loader, 4);
+ const adc_vref = (word4 >> 8) & 0x1F;
+ if (adc_vref !== 0) {
+ features.push(' VRef calibration in efuse');
+ }
+
+ const blk3_part_res = (word3 >> 14) & 0x1;
+ if (blk3_part_res !== 0) {
+ features.push(' BLK3 partially reserved');
+ }
+
+ const word6 = await this.read_efuse(loader, 6);
+ const coding_scheme = word6 & 0x3;
+ const coding_scheme_arr = ['None', '3/4', 'Repeat (UNSUPPORTED)', 'Invalid'];
+ features.push(` Coding Scheme ${coding_scheme_arr[coding_scheme]}`);
+
+ return features;
+ }
+
+ static get_crystal_freq = async (loader) => {
+ const uart_div = await loader.read_reg({ addr: this.UART_CLKDIV_REG }) & this.UART_CLKDIV_MASK;
+ const ets_xtal = (loader.transport.baudrate * uart_div) / 1000000 / this.XTAL_CLK_DIVIDER;
+ let norm_xtal;
+ if (ets_xtal > 33) {
+ norm_xtal = 40;
+ } else {
+ norm_xtal = 26;
+ }
+ if (Math.abs(norm_xtal - ets_xtal) > 1) {
+ loader.log('WARNING: Unsupported crystal in use');
+ }
+ return norm_xtal;
+ }
+
+ static _d2h(d) {
+ const h = (+d).toString(16);
+ return h.length === 1 ? `0${h}` : h;
+ }
+
+ static read_mac = async (loader) => {
+ let mac0 = await this.read_efuse(loader, 1);
+ mac0 >>>= 0;
+ let mac1 = await this.read_efuse(loader, 2);
+ mac1 >>>= 0;
+ const mac = new Uint8Array(6);
+ mac[0] = (mac1 >> 8) & 0xff;
+ mac[1] = mac1 & 0xff;
+ mac[2] = (mac0 >> 24) & 0xff;
+ mac[3] = (mac0 >> 16) & 0xff;
+ mac[4] = (mac0 >> 8) & 0xff;
+ mac[5] = mac0 & 0xff;
+
+ return (`${
+ this._d2h(mac[0])
+ }:${
+ this._d2h(mac[1])
+ }:${
+ this._d2h(mac[2])
+ }:${
+ this._d2h(mac[3])
+ }:${
+ this._d2h(mac[4])
+ }:${
+ this._d2h(mac[5])
+ }`);
+ }
+
+ static get_erase_size = (offset, size) => size
+}
+
+class ESP32S2ROM {
+ static CHIP_NAME = 'ESP32-S2';
+
+ static IS_STUB = true;
+
+ static IMAGE_CHIP_ID = 2;
+
+ static CHIP_DETECT_MAGIC_VALUE = 0x000007c6;
+
+ static MAC_EFUSE_REG = 0x3f41A044;
+
+ static EFUSE_BASE = 0x3f41A000;
+
+ static UART_CLKDIV_REG = 0x3f400014;
+
+ static UART_CLKDIV_MASK = 0xFFFFF;
+
+ static UART_DATE_REG_ADDR = 0x60000078;
+
+ static FLASH_WRITE_SIZE = 0x400;
+
+ static BOOTLOADER_FLASH_OFFSET = 0x1000;
+
+ static FLASH_SIZES = {
+ '1MB': 0x00, '2MB': 0x10, '4MB': 0x20, '8MB': 0x30, '16MB': 0x40,
+ };
+
+ static SPI_REG_BASE = 0x3f402000;
+
+ static SPI_USR_OFFS = 0x18;
+
+ static SPI_USR1_OFFS = 0x1c;
+
+ static SPI_USR2_OFFS = 0x20;
+
+ static SPI_W0_OFFS = 0x58;
+
+ static SPI_MOSI_DLEN_OFFS = 0x24;
+
+ static SPI_MISO_DLEN_OFFS = 0x28;
+
+ static get_pkg_version = async (loader) => {
+ const num_word = 3;
+ const block1_addr = this.EFUSE_BASE + 0x044;
+ const addr = block1_addr + (4 * num_word);
+ const word3 = await loader.read_reg({ addr });
+ const pkg_version = (word3 >> 21) & 0x0F;
+ return pkg_version;
+ }
+
+ static get_chip_description = async (loader) => {
+ const chip_desc = ['ESP32-S2', 'ESP32-S2FH16', 'ESP32-S2FH32'];
+ const pkg_ver = await this.get_pkg_version(loader);
+ if (pkg_ver >= 0 && pkg_ver <= 2) {
+ return chip_desc[pkg_ver];
+ }
+ return 'unknown ESP32-S2';
+ }
+
+ static get_chip_features = async (loader) => {
+ const features = ['Wi-Fi'];
+ const pkg_ver = await this.get_pkg_version(loader);
+ if (pkg_ver === 1) {
+ features.push('Embedded 2MB Flash');
+ } else if (pkg_ver === 2) {
+ features.push('Embedded 4MB Flash');
+ }
+ const num_word = 4;
+ const block2_addr = this.EFUSE_BASE + 0x05C;
+ const addr = block2_addr + (4 * num_word);
+ const word4 = await loader.read_reg({ addr });
+ const block2_ver = (word4 >> 4) & 0x07;
+
+ if (block2_ver === 1) {
+ features.push('ADC and temperature sensor calibration in BLK2 of efuse');
+ }
+ return features;
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ static get_crystal_freq = async (loader) => 40
+
+ static _d2h(d) {
+ const h = (+d).toString(16);
+ return h.length === 1 ? `0${h}` : h;
+ }
+
+ static read_mac = async (loader) => {
+ let mac0 = await loader.read_reg({ addr: this.MAC_EFUSE_REG });
+ mac0 >>>= 0;
+ let mac1 = await loader.read_reg({ addr: this.MAC_EFUSE_REG + 4 });
+ mac1 = (mac1 >>> 0) & 0x0000ffff;
+ const mac = new Uint8Array(6);
+ mac[0] = (mac1 >> 8) & 0xff;
+ mac[1] = mac1 & 0xff;
+ mac[2] = (mac0 >> 24) & 0xff;
+ mac[3] = (mac0 >> 16) & 0xff;
+ mac[4] = (mac0 >> 8) & 0xff;
+ mac[5] = mac0 & 0xff;
+
+ return (`${
+ this._d2h(mac[0])
+ }:${
+ this._d2h(mac[1])
+ }:${
+ this._d2h(mac[2])
+ }:${
+ this._d2h(mac[3])
+ }:${
+ this._d2h(mac[4])
+ }:${
+ this._d2h(mac[5])
+ }`);
+ }
+
+ static get_erase_size = (offset, size) => size
+}
+
+class ESP32S3BETA2ROM {
+ static CHIP_NAME = 'ESP32-S3';
+
+ static IMAGE_CHIP_ID = 4;
+
+ static CHIP_DETECT_MAGIC_VALUE = 0xeb004136;
+
+ // eslint-disable-next-line no-unused-vars
+ static get_pkg_version = async (loader) => {
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ static get_chip_revision = async (loader) => {
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ static get_chip_description = async (loader) => {
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ static get_chip_features = async (loader) => {
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ static get_crystal_freq = async (loader) => {
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ static read_mac = async (loader) => {
+ }
+}
+
+class ESP32C3ROM {
+ static CHIP_NAME = 'ESP32-C3';
+
+ static IS_STUB = true;
+
+ static IMAGE_CHIP_ID = 5;
+
+ static CHIP_DETECT_MAGIC_VALUE = 0x6921506f;
+
+ static EFUSE_BASE = 0x60008800;
+
+ static MAC_EFUSE_REG = this.EFUSE_BASE + 0x044;
+
+ static UART_CLKDIV_REG = 0x3ff40014;
+
+ static UART_CLKDIV_MASK = 0xFFFFF;
+
+ static UART_DATE_REG_ADDR = 0x6000007C;
+
+ static FLASH_WRITE_SIZE = 0x400;
+
+ static BOOTLOADER_FLASH_OFFSET = 0x1000;
+
+ static FLASH_SIZES = {
+ '1MB': 0x00, '2MB': 0x10, '4MB': 0x20, '8MB': 0x30, '16MB': 0x40,
+ };
+
+ static SPI_REG_BASE = 0x60002000;
+
+ static SPI_USR_OFFS = 0x18;
+
+ static SPI_USR1_OFFS = 0x1C;
+
+ static SPI_USR2_OFFS = 0x20;
+
+ static SPI_MOSI_DLEN_OFFS = 0x24;
+
+ static SPI_MISO_DLEN_OFFS = 0x28;
+
+ static SPI_W0_OFFS = 0x58;
+
+ static get_pkg_version = async (loader) => {
+ const num_word = 3;
+ const block1_addr = this.EFUSE_BASE + 0x044;
+ const addr = block1_addr + (4 * num_word);
+ const word3 = await loader.read_reg({ addr });
+ const pkg_version = (word3 >> 21) & 0x0F;
+ return pkg_version;
+ }
+
+ static get_chip_revision = async (loader) => {
+ const block1_addr = this.EFUSE_BASE + 0x044;
+ const num_word = 3;
+ const pos = 18;
+ const addr = block1_addr + (4 * num_word);
+ const ret = (await loader.read_reg({ addr }) & (0x7 << pos)) >> pos;
+ return ret;
+ }
+
+ static get_chip_description = async (loader) => {
+ let desc;
+ const pkg_ver = await this.get_pkg_version(loader);
+ if (pkg_ver === 0) {
+ desc = 'ESP32-C3';
+ } else {
+ desc = 'unknown ESP32-C3';
+ }
+ const chip_rev = await this.get_chip_revision(loader);
+ desc += ` (revision ${chip_rev})`;
+ return desc;
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ static get_chip_features = async (loader) => ['Wi-Fi']
+
+ // eslint-disable-next-line no-unused-vars
+ static get_crystal_freq = async (loader) => 40
+
+ static _d2h(d) {
+ const h = (+d).toString(16);
+ return h.length === 1 ? `0${h}` : h;
+ }
+
+ static read_mac = async (loader) => {
+ let mac0 = await loader.read_reg({ addr: this.MAC_EFUSE_REG });
+ mac0 >>>= 0;
+ let mac1 = await loader.read_reg({ addr: this.MAC_EFUSE_REG + 4 });
+ mac1 = (mac1 >>> 0) & 0x0000ffff;
+ const mac = new Uint8Array(6);
+ mac[0] = (mac1 >> 8) & 0xff;
+ mac[1] = mac1 & 0xff;
+ mac[2] = (mac0 >> 24) & 0xff;
+ mac[3] = (mac0 >> 16) & 0xff;
+ mac[4] = (mac0 >> 8) & 0xff;
+ mac[5] = mac0 & 0xff;
+
+ return (`${
+ this._d2h(mac[0])
+ }:${
+ this._d2h(mac[1])
+ }:${
+ this._d2h(mac[2])
+ }:${
+ this._d2h(mac[3])
+ }:${
+ this._d2h(mac[4])
+ }:${
+ this._d2h(mac[5])
+ }`);
+ }
+
+ static get_erase_size = (offset, size) => size
+}
+
+export default class ESPLoader {
+ ESP_RAM_BLOCK = 0x1800;
+
+ ESP_FLASH_BEGIN = 0x02;
+
+ ESP_FLASH_DATA = 0x03;
+
+ ESP_FLASH_END = 0x04;
+
+ ESP_MEM_BEGIN = 0x05;
+
+ ESP_MEM_END = 0x06;
+
+ ESP_MEM_DATA = 0x07;
+
+ ESP_WRITE_REG = 0x09;
+
+ ESP_FLASH_DEFL_BEGIN = 0x10;
+
+ ESP_FLASH_DEFL_DATA = 0x11;
+
+ ESP_FLASH_DEFL_END = 0x12;
+
+ ESP_SPI_FLASH_MD5 = 0x13;
+
+ ESP_READ_REG = 0x0A;
+
+ ESP_SPI_ATTACH = 0x0D;
+
+ // Only Stub supported commands
+ ESP_ERASE_FLASH = 0xD0;
+
+ ESP_ERASE_REGION = 0xD1;
+
+ ESP_IMAGE_MAGIC = 0xe9;
+
+ ESP_CHECKSUM_MAGIC = 0xef;
+
+ ERASE_REGION_TIMEOUT_PER_MB = 30000;
+
+ ERASE_WRITE_TIMEOUT_PER_MB = 40000;
+
+ MD5_TIMEOUT_PER_MB = 8000;
+
+ CHIP_ERASE_TIMEOUT = 120000;
+
+ MAX_TIMEOUT = this.CHIP_ERASE_TIMEOUT * 2;
+
+ CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000;
+
+ DETECTED_FLASH_SIZES = {
+ 0x12: '256KB', 0x13: '512KB', 0x14: '1MB', 0x15: '2MB', 0x16: '4MB', 0x17: '8MB', 0x18: '16MB',
+ };
+
+ constructor(transport, terminal) {
+ this.transport = transport;
+ this.terminal = terminal;
+ this.IS_STUB = false;
+ this.chip = null;
+
+ // if (terminal) {
+ // this.terminal.clear();
+ // }
+
+ this.log('esptool.js v0.1-dev');
+ this.log(`Serial port ${this.transport.get_info()}`);
+ }
+
+ _sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+
+ log(str) {
+ if (this.transport) {
+ this.terminal.log(str);
+ } else {
+ // eslint-disable-next-line no-console
+ console.log(str);
+ }
+ }
+
+ write_char(str) {
+ if (this.transport) {
+ this.terminal.write(str);
+ } else {
+ // eslint-disable-next-line no-console
+ console.log(str);
+ }
+ }
+
+ _short_to_bytearray(i) {
+ return [i & 0xff, (i >> 8) & 0xff];
+ }
+
+ _int_to_bytearray(i) {
+ return [i & 0xff, (i >> 8) & 0xff, (i >> 16) & 0xff, (i >> 24) & 0xff];
+ }
+
+ _bytearray_to_short(i, j) {
+ return (new Uint16Array([(i | (j >> 8))]))[0];
+ }
+
+ _bytearray_to_int(i, j, k, l) {
+ return (new Uint32Array([(i | (j << 8) | (k << 16) | (l << 24))]))[0];
+ }
+
+ _appendBuffer(buffer1, buffer2) {
+ const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
+ tmp.set(new Uint8Array(buffer1), 0);
+ tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
+ return tmp.buffer;
+ }
+
+ _appendArray(arr1, arr2) {
+ const c = new Uint8Array(arr1.length + arr2.length);
+ c.set(arr1, 0);
+ c.set(arr2, arr1.length);
+ return c;
+ }
+
+ async _loadStub() {
+ const stubName = this.chip.CHIP_NAME.replaceAll('-', '').toLowerCase();
+ if (stubCache[stubName]) {
+ return stubCache[stubName];
+ }
+ const stub = await fetch(`/stubs/${stubName}.json`).then((res) => res.json());
+
+ stub.data = Buffer.from(stub.data, 'base64');
+ stub.text = Buffer.from(stub.text, 'base64');
+
+ stubCache[stubName] = stub;
+ return stub;
+ }
+
+ ui8ToBstr(u8Array) {
+ let i;
+ const len = u8Array.length;
+ let b_str = '';
+ for (i = 0; i < len; i++) {
+ b_str += String.fromCharCode(u8Array[i]);
+ }
+ return b_str;
+ }
+
+ bstrToUi8(bStr) {
+ const len = bStr.length;
+ const u8_array = new Uint8Array(len);
+ for (let i = 0; i < len; i++) {
+ u8_array[i] = bStr.charCodeAt(i);
+ }
+ return u8_array;
+ }
+
+ flush_input = async () => {
+ try {
+ await this.transport.read({ timeout: 200 });
+ } catch (e) {
+ Math.random(e);
+ }
+ }
+
+ command = async ({
+ op = null, data = [], chk = 0, wait_response = true, timeout = 3000, min_data = 12,
+ } = {}) => {
+ // console.log("command "+ op + " " + wait_response + " " + timeout);
+ if (op != null) {
+ const pkt = new Uint8Array(8 + data.length);
+ pkt[0] = 0x00;
+ pkt[1] = op;
+ pkt[2] = this._short_to_bytearray(data.length)[0];
+ pkt[3] = this._short_to_bytearray(data.length)[1];
+ pkt[4] = this._int_to_bytearray(chk)[0];
+ pkt[5] = this._int_to_bytearray(chk)[1];
+ pkt[6] = this._int_to_bytearray(chk)[2];
+ pkt[7] = this._int_to_bytearray(chk)[3];
+
+ let i;
+ for (i = 0; i < data.length; i++) {
+ pkt[8 + i] = data[i];
+ }
+ // console.log("Command " + pkt);
+ await this.transport.write(pkt);
+ }
+
+ if (wait_response) {
+ try {
+ const p = await this.transport.read({ timeout, min_data });
+ // console.log(this.transport.slip_reader_enabled, p);
+ // const resp = p[0];
+ const op_ret = p[1];
+ // const len_ret = this._bytearray_to_short(p[2], p[3]);
+ const val = this._bytearray_to_int(p[4], p[5], p[6], p[7]);
+ // eslint-disable-next-line no-console
+ // console.log(`Resp ${resp} ${op_ret} ${op} ${len_ret} ${val} ${p}`);
+ const datum = p.slice(8);
+ // eslint-disable-next-line eqeqeq
+ if (op == null || op_ret == op) {
+ return [val, datum];
+ }
+ throw ('invalid response');
+ } catch (e) {
+ if (e === 'timeout') {
+ throw (e);
+ }
+ }
+ }
+ return [];
+ }
+
+ read_reg = async ({ addr, timeout = 3000 } = {}) => {
+ // console.log(`read reg ${addr} ${timeout}`);
+ const pkt = this._int_to_bytearray(addr);
+ const val = await this.command({ op: this.ESP_READ_REG, data: pkt, timeout });
+ // console.log('Read reg resp', val);
+ return val[0];
+ }
+
+ write_reg = async ({
+ addr, value, mask = 0xFFFFFFFF, delay_us = 0, delay_after_us = 0,
+ } = {}) => {
+ let pkt = this._appendArray(this._int_to_bytearray(addr), this._int_to_bytearray(value));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(mask));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(delay_us));
+
+ if (delay_after_us > 0) {
+ pkt = this._appendArray(pkt, this._int_to_bytearray(this.chip.UART_DATE_REG_ADDR));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(delay_after_us));
+ }
+
+ await this.check_command({ op_description: 'write target memory', op: this.ESP_WRITE_REG, data: pkt });
+ }
+
+ sync = async () => {
+ // console.log('Sync');
+ const cmd = new Uint8Array(36);
+ let i;
+ cmd[0] = 0x07;
+ cmd[1] = 0x07;
+ cmd[2] = 0x12;
+ cmd[3] = 0x20;
+ for (i = 0; i < 32; i++) {
+ cmd[4 + i] = 0x55;
+ }
+
+ try {
+ const resp = await this.command({ op: 0x08, data: cmd, timeout: 100 });
+ this.syncStubDetected = resp[0] === 0;
+ return resp;
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.log(`Sync err ${e}`);
+ throw (e);
+ }
+ }
+
+ _connect_attempt = async ({ mode = 'default_reset', esp32r0_delay = false } = {}) => {
+ // console.log(`_connect_attempt ${esp32r0_delay}`);
+ if (mode !== 'no_reset') {
+ await this.transport.setDTR(false);
+ await this.transport.setRTS(true);
+ await this._sleep(100);
+ if (esp32r0_delay) {
+ // await this._sleep(1200);
+ await this._sleep(2000);
+ }
+ await this.transport.setDTR(true);
+ await this.transport.setRTS(false);
+ if (esp32r0_delay) {
+ // await this._sleep(400);
+ }
+ await this._sleep(50);
+ await this.transport.setDTR(false);
+ }
+ let i = 0;
+ // eslint-disable-next-line no-constant-condition
+ while (1) {
+ try {
+ const res = await this.transport.read({ timeout: 1000 });
+ i += res.length;
+ // console.log("Len = " + res.length);
+ // var str = new TextDecoder().decode(res);
+ // this.log(str);
+ } catch (e) {
+ if (e === 'timeout') {
+ break;
+ }
+ }
+ await this._sleep(50);
+ }
+ this.transport.slip_reader_enabled = true;
+ i = 7;
+ while (i--) {
+ try {
+ await this.sync();
+ return 'success';
+ } catch (error) {
+ if (error === 'timeout') {
+ if (esp32r0_delay) {
+ this.write_char('_');
+ } else {
+ this.write_char('.');
+ }
+ }
+ }
+ await this._sleep(50);
+ }
+ return 'error';
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ async connect({ mode = 'default_reset', attempts = 7, detecting = false } = {}) {
+ let i;
+ let resp;
+ this.write_char('Connecting...');
+ await this.transport.connect();
+ for (i = 0; i < attempts; i++) {
+ resp = await this._connect_attempt({ esp32r0_delay: false });
+ if (resp === 'success') {
+ break;
+ }
+ resp = await this._connect_attempt({ esp32r0_delay: true });
+ if (resp === 'success') {
+ break;
+ }
+ }
+ if (resp !== 'success') {
+ this.log('Failed to connect with the device');
+ return 'error';
+ }
+ this.write_char('\n');
+ this.write_char('\r');
+ await this._sleep(100);
+ await this.flush_input();
+
+ if (!detecting) {
+ const chip_magic_value = await this.read_reg({ addr: 0x40001000 });
+ // eslint-disable-next-line no-console
+ // console.log(`Chip Magic ${chip_magic_value}`);
+ const chips = [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32C3ROM];
+ this.chip = chips.find((cls) => chip_magic_value === cls.CHIP_DETECT_MAGIC_VALUE);
+ // console.log('chip', this.chip);
+ }
+ return null;
+ }
+
+ detect_chip = async () => {
+ await this.connect();
+ this.write_char('Detecting chip type... ');
+ if (this.chip != null) {
+ this.log(this.chip.CHIP_NAME);
+ }
+ }
+
+ check_command = async ({
+ // eslint-disable-next-line no-unused-vars
+ op_description = '', op = null, data = [], chk = 0, timeout = 3000, min_data,
+ } = {}) => {
+ // console.log(`check_command ${op}`);
+ const resp = await this.command({
+ op, data, chk, timeout, min_data,
+ });
+ if (resp[1].length > 4) {
+ return resp[1];
+ }
+ return resp[0];
+ }
+
+ mem_begin = async (size, blocks, blocksize, offset) => {
+ /* XXX: Add check to ensure that STUB is not getting overwritten */
+ // console.log(`mem_begin ${size} ${blocks} ${blocksize} ${offset}`);
+ let pkt = this._appendArray(this._int_to_bytearray(size), this._int_to_bytearray(blocks));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(blocksize));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(offset));
+ await this.check_command({ op_description: 'write to target RAM', op: this.ESP_MEM_BEGIN, data: pkt });
+ }
+
+ checksum = (data) => {
+ let i;
+ let chk = 0xEF;
+
+ for (i = 0; i < data.length; i++) {
+ chk ^= data[i];
+ }
+ return chk;
+ }
+
+ mem_block = async (buffer, seq) => {
+ let pkt = this._appendArray(this._int_to_bytearray(buffer.length), this._int_to_bytearray(seq));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0));
+ pkt = this._appendArray(pkt, buffer);
+ const checksum = this.checksum(buffer);
+ await this.check_command({
+ op_description: 'write to target RAM', op: this.ESP_MEM_DATA, data: pkt, chk: checksum,
+ });
+ }
+
+ mem_finish = async (entrypoint) => {
+ const is_entry = (entrypoint === 0) ? 1 : 0;
+ const pkt = this._appendArray(this._int_to_bytearray(is_entry), this._int_to_bytearray(entrypoint));
+ return this.check_command({
+ op_description: 'leave RAM download mode', op: this.ESP_MEM_END, data: pkt, timeout: 500, min_data: 12,
+ }); // XXX: handle non-stub with diff timeout
+ }
+
+ flash_spi_attach = async (hspi_arg) => {
+ const pkt = this._int_to_bytearray(hspi_arg);
+ await this.check_command({ op_description: 'configure SPI flash pins', op: this.ESP_SPI_ATTACH, data: pkt });
+ }
+
+ timeout_per_mb = (seconds_per_mb, size_bytes) => {
+ const result = seconds_per_mb * (size_bytes / 1000000);
+ if (result < 3000) {
+ return 3000;
+ }
+ return result;
+ }
+
+ flash_begin = async (size, offset) => {
+ const num_blocks = Math.floor((size + this.FLASH_WRITE_SIZE - 1) / this.FLASH_WRITE_SIZE);
+ const erase_size = this.chip.get_erase_size(offset, size);
+
+ const d = new Date();
+ const t1 = d.getTime();
+
+ let timeout = 3000;
+ if (this.IS_STUB === false) {
+ timeout = this.timeout_per_mb(this.ERASE_REGION_TIMEOUT_PER_MB, size);
+ }
+
+ // eslint-disable-next-line no-console
+ // console.log(`flash begin ${erase_size} ${num_blocks} ${this.FLASH_WRITE_SIZE} ${offset} ${size}`);
+ let pkt = this._appendArray(this._int_to_bytearray(erase_size), this._int_to_bytearray(num_blocks));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(this.FLASH_WRITE_SIZE));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(offset));
+ if (this.IS_STUB === false) {
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0)); // XXX: Support encrypted
+ }
+
+ await this.check_command({
+ op_description: 'enter Flash download mode', op: this.ESP_FLASH_BEGIN, data: pkt, timeout,
+ });
+
+ const t2 = d.getTime();
+ if (size !== 0 && this.IS_STUB === false) {
+ this.log(`Took ${(t2 - t1) / 1000}.${(t2 - t1) % 1000}s to erase flash block`);
+ }
+ return num_blocks;
+ }
+
+ flash_defl_begin = async (size, compsize, offset) => {
+ const num_blocks = Math.floor((compsize + this.FLASH_WRITE_SIZE - 1) / this.FLASH_WRITE_SIZE);
+ const erase_blocks = Math.floor((size + this.FLASH_WRITE_SIZE - 1) / this.FLASH_WRITE_SIZE);
+
+ const d = new Date();
+ const t1 = d.getTime();
+
+ let write_size; let
+ timeout;
+ if (this.IS_STUB) {
+ write_size = size;
+ timeout = 3000;
+ } else {
+ write_size = erase_blocks * this.FLASH_WRITE_SIZE;
+ timeout = this.timeout_per_mb(this.ERASE_REGION_TIMEOUT_PER_MB, write_size);
+ }
+ this.log(`Compressed ${size} bytes to ${compsize}...`);
+
+ let pkt = this._appendArray(this._int_to_bytearray(write_size), this._int_to_bytearray(num_blocks));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(this.FLASH_WRITE_SIZE));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(offset));
+
+ if (
+ (this.chip.CHIP_NAME === 'ESP32-S2' || this.chip.CHIP_NAME === 'ESP32-S3' || this.chip.CHIP_NAME === 'ESP32-C3')
+ && (this.IS_STUB === false)
+ ) {
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0));
+ }
+ if (this.chip.CHIP_NAME === 'ESP8266') {
+ await this.flush_input();
+ }
+ await this.check_command({
+ op_description: 'enter compressed flash mode', op: this.ESP_FLASH_DEFL_BEGIN, data: pkt, timeout,
+ });
+ const t2 = d.getTime();
+ if (size !== 0 && this.IS_STUB === false) {
+ this.log(`Took ${(t2 - t1) / 1000}.${(t2 - t1) % 1000}s to erase flash block`);
+ }
+ return num_blocks;
+ }
+
+ flash_block = async (data, seq, timeout) => {
+ let pkt = this._appendArray(this._int_to_bytearray(data.length), this._int_to_bytearray(seq));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0));
+ pkt = this._appendArray(pkt, data);
+
+ const checksum = this.checksum(data);
+
+ await this.check_command({
+ op_description: `write to target Flash after seq ${seq}`, op: this.ESP_FLASH_DATA, data: pkt, chk: checksum, timeout,
+ });
+ }
+
+ flash_defl_block = async (data, seq, timeout) => {
+ let pkt = this._appendArray(this._int_to_bytearray(data.length), this._int_to_bytearray(seq));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0));
+ pkt = this._appendArray(pkt, data);
+
+ const checksum = this.checksum(data);
+ // console.log(`flash_defl_block ${data[0].toString(16)}`, +' ' + data[1].toString(16));
+
+ await this.check_command({
+ op_description: `write compressed data to flash after seq ${seq}`,
+ op: this.ESP_FLASH_DEFL_DATA,
+ data: pkt,
+ chk: checksum,
+ timeout,
+ });
+ }
+
+ flash_finish = async ({ reboot = false } = {}) => {
+ const val = reboot ? 0 : 1;
+ const pkt = this._int_to_bytearray(val);
+
+ await this.check_command({ op_description: 'leave Flash mode', op: this.ESP_FLASH_END, data: pkt });
+ }
+
+ flash_defl_finish = async ({ reboot = false } = {}) => {
+ const val = reboot ? 0 : 1;
+ const pkt = this._int_to_bytearray(val);
+
+ await this.check_command({ op_description: 'leave compressed flash mode', op: this.ESP_FLASH_DEFL_END, data: pkt });
+ }
+
+ run_spiflash_command = async (spiflash_command, data, read_bits) => {
+ // SPI_USR register flags
+ const SPI_USR_COMMAND = (1 << 31);
+ const SPI_USR_MISO = (1 << 28);
+ const SPI_USR_MOSI = (1 << 27);
+
+ // SPI registers, base address differs ESP32* vs 8266
+ const base = this.chip.SPI_REG_BASE;
+ const SPI_CMD_REG = base + 0x00;
+ const SPI_USR_REG = base + this.chip.SPI_USR_OFFS;
+ const SPI_USR1_REG = base + this.chip.SPI_USR1_OFFS;
+ const SPI_USR2_REG = base + this.chip.SPI_USR2_OFFS;
+ const SPI_W0_REG = base + this.chip.SPI_W0_OFFS;
+
+ let set_data_lengths;
+ if (this.chip.SPI_MOSI_DLEN_OFFS != null) {
+ set_data_lengths = async (mosi_bits, miso_bits) => {
+ const SPI_MOSI_DLEN_REG = base + this.chip.SPI_MOSI_DLEN_OFFS;
+ const SPI_MISO_DLEN_REG = base + this.chip.SPI_MISO_DLEN_OFFS;
+ if (mosi_bits > 0) {
+ await this.write_reg({ addr: SPI_MOSI_DLEN_REG, value: (mosi_bits - 1) });
+ }
+ if (miso_bits > 0) {
+ await this.write_reg({ addr: SPI_MISO_DLEN_REG, value: (miso_bits - 1) });
+ }
+ };
+ } else {
+ set_data_lengths = async (mosi_bits, miso_bits) => {
+ const SPI_DATA_LEN_REG = SPI_USR1_REG;
+ const SPI_MOSI_BITLEN_S = 17;
+ const SPI_MISO_BITLEN_S = 8;
+ const mosi_mask = (mosi_bits === 0) ? 0 : (mosi_bits - 1);
+ const miso_mask = (miso_bits === 0) ? 0 : (miso_bits - 1);
+ const val = (miso_mask << SPI_MISO_BITLEN_S) | (mosi_mask << SPI_MOSI_BITLEN_S);
+ await this.write_reg({ addr: SPI_DATA_LEN_REG, value: val });
+ };
+ }
+
+ const SPI_CMD_USR = (1 << 18);
+ const SPI_USR2_COMMAND_LEN_SHIFT = 28;
+ if (read_bits > 32) {
+ throw 'Reading more than 32 bits back from a SPI flash operation is unsupported';
+ }
+ if (data.length > 64) {
+ throw 'Writing more than 64 bytes of data with one SPI command is unsupported';
+ }
+
+ const data_bits = data.length * 8;
+ const old_spi_usr = await this.read_reg({ addr: SPI_USR_REG });
+ const old_spi_usr2 = await this.read_reg({ addr: SPI_USR2_REG });
+ let flags = SPI_USR_COMMAND;
+ let i;
+ if (read_bits > 0) {
+ flags |= SPI_USR_MISO;
+ }
+ if (data_bits > 0) {
+ flags |= SPI_USR_MOSI;
+ }
+ await set_data_lengths(data_bits, read_bits);
+ await this.write_reg({ addr: SPI_USR_REG, value: flags });
+ let val = (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflash_command;
+ await this.write_reg({ addr: SPI_USR2_REG, value: val });
+ if (data_bits === 0) {
+ await this.write_reg({ addr: SPI_W0_REG, value: 0 });
+ } else {
+ if (data.length % 4 !== 0) {
+ const padding = new Uint8Array(data.length % 4);
+ // eslint-disable-next-line no-param-reassign
+ data = this._appendArray(data, padding);
+ }
+ let next_reg = SPI_W0_REG;
+ for (i = 0; i < data.length - 4; i += 4) {
+ val = this._bytearray_to_int(data[i], data[i + 1], data[i + 2], data[i + 3]);
+ await this.write_reg({ addr: next_reg, value: val });
+ next_reg += 4;
+ }
+ }
+ await this.write_reg({ addr: SPI_CMD_REG, value: SPI_CMD_USR });
+ for (i = 0; i < 10; i++) {
+ val = await this.read_reg({ addr: SPI_CMD_REG }) & SPI_CMD_USR;
+ if (val === 0) {
+ break;
+ }
+ }
+ if (i === 10) {
+ throw 'SPI command did not complete in time';
+ }
+ const stat = await this.read_reg({ addr: SPI_W0_REG });
+ await this.write_reg({ addr: SPI_USR_REG, value: old_spi_usr });
+ await this.write_reg({ addr: SPI_USR2_REG, value: old_spi_usr2 });
+ return stat;
+ }
+
+ read_flash_id = async () => {
+ const SPIFLASH_RDID = 0x9F;
+ const pkt = new Uint8Array(0);
+ return this.run_spiflash_command(SPIFLASH_RDID, pkt, 24);
+ }
+
+ erase_flash = async () => {
+ this.log('Erasing flash (this may take a while)...');
+ let d = new Date();
+ const t1 = d.getTime();
+ const ret = await this.check_command({
+ op_description: 'erase flash',
+ op: this.ESP_ERASE_FLASH,
+ timeout: this.CHIP_ERASE_TIMEOUT,
+ });
+ d = new Date();
+ const t2 = d.getTime();
+ this.log(`Chip erase completed successfully in ${(t2 - t1) / 1000}s`);
+ return ret;
+ }
+
+ toHex(buffer) {
+ return Array.prototype.map.call(buffer, (x) => (`00${x.toString(16)}`).slice(-2)).join('');
+ }
+
+ flash_md5sum = async (addr, size) => {
+ const timeout = this.timeout_per_mb(this.MD5_TIMEOUT_PER_MB, size);
+ let pkt = this._appendArray(this._int_to_bytearray(addr), this._int_to_bytearray(size));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0));
+ pkt = this._appendArray(pkt, this._int_to_bytearray(0));
+
+ let res = await this.check_command({
+ op_description: 'calculate md5sum', op: this.ESP_SPI_FLASH_MD5, data: pkt, timeout, min_data: 26,
+ });
+ if (res.length > 16) {
+ res = res.slice(0, 16);
+ }
+ const strmd5 = this.toHex(res);
+ return strmd5;
+ }
+
+ run_stub = async () => {
+ this.log('Fetching stub...');
+
+ const stub = await this._loadStub();
+ // console.log(stub);
+ const {
+ data, text, data_start, text_start, entry,
+ } = stub;
+
+ this.log('Uploading stub...');
+
+ let blocks = Math.floor((text.length + this.ESP_RAM_BLOCK - 1) / this.ESP_RAM_BLOCK);
+ let i;
+
+ await this.mem_begin(text.length, blocks, this.ESP_RAM_BLOCK, text_start);
+ for (i = 0; i < blocks; i++) {
+ const from_offs = i * this.ESP_RAM_BLOCK;
+ let to_offs = from_offs + this.ESP_RAM_BLOCK;
+ if (to_offs > text.length) to_offs = text.length;
+ await this.mem_block(text.slice(from_offs, to_offs), i);
+ }
+
+ blocks = Math.floor((data.length + this.ESP_RAM_BLOCK - 1) / this.ESP_RAM_BLOCK);
+ await this.mem_begin(data.length, blocks, this.ESP_RAM_BLOCK, data_start);
+ for (i = 0; i < blocks; i++) {
+ const from_offs = i * this.ESP_RAM_BLOCK;
+ let to_offs = from_offs + this.ESP_RAM_BLOCK;
+ if (to_offs > data.length) to_offs = data.length;
+ await this.mem_block(data.slice(from_offs, to_offs), i);
+ }
+
+ this.log('Running stub...');
+ let valid = false;
+ await this.mem_finish(entry);
+
+ if (this.chip.CHIP_NAME === 'ESP8266') {
+ const [reply] = await this.sync();
+ if (reply === 0) valid = true;
+ } else {
+ const res = await this.transport.read({ timeout: 1000, min_data: 6 });
+ if (res[0] === 79 && res[1] === 72 && res[2] === 65 && res[3] === 73) {
+ valid = true;
+ }
+ }
+
+ if (valid) {
+ this.log('Stub running...');
+ this.IS_STUB = true;
+ this.FLASH_WRITE_SIZE = 0x4000;
+ return this.chip;
+ }
+ this.log('Failed to start stub. Unexpected response');
+ return null;
+ }
+
+ main_fn = async () => {
+ await this.detect_chip();
+ if (this.chip == null) {
+ this.log('Error in connecting to board');
+ return;
+ }
+
+ const chip = await this.chip.get_chip_description(this);
+ this.log(`Chip is ${chip}`);
+ this.log(`Features: ${await this.chip.get_chip_features(this)}`);
+ this.log(`Crystal is ${await this.chip.get_crystal_freq(this)}MHz`);
+ this.log(`MAC: ${await this.chip.read_mac(this)}`);
+ await this.chip.read_mac(this);
+
+ if (this.chip.IS_STUB) await this.run_stub();
+ else this.FLASH_WRITE_SIZE = this.chip.FLASH_WRITE_SIZE || 0x4000;
+ }
+
+ flash_size_bytes = (flash_size) => {
+ let flash_size_b = -1;
+ if (flash_size.indexOf('KB') !== -1) {
+ flash_size_b = parseInt(flash_size.slice(0, flash_size.indexOf('KB')), 10) * 1024;
+ } else if (flash_size.indexOf('MB') !== -1) {
+ flash_size_b = parseInt(flash_size.slice(0, flash_size.indexOf('MB')), 10) * 1024 * 1024;
+ }
+ return flash_size_b;
+ }
+
+ pad_array = (arr, len, fillValue) => Object.assign(new Array(len).fill(fillValue), arr)
+
+ parse_flash_size_arg = (flsz) => {
+ if (typeof this.chip.FLASH_SIZES[flsz] === 'undefined') {
+ this.log(`Flash size ${flsz} is not supported by this chip type. Supported sizes: ${this.chip.FLASH_SIZES}`);
+ throw 'Invalid flash size';
+ }
+ return this.chip.FLASH_SIZES[flsz];
+ }
+
+ _update_image_flash_params = (image, address, flash_size, flash_mode, flash_freq) => {
+ // console.log(`_update_image_flash_params ${flash_size} ${flash_mode} ${flash_freq}`);
+ if (image.length < 8) {
+ return image;
+ }
+ if (address !== this.chip.BOOTLOADER_FLASH_OFFSET) {
+ return image;
+ }
+ if (flash_size === 'keep' && flash_mode === 'keep' && flash_freq === 'keep') {
+ // console.log('Not changing the image');
+ return image;
+ }
+
+ const magic = image[0];
+ let a_flash_mode = image[2];
+ const flash_size_freq = image[3];
+ if (magic !== this.ESP_IMAGE_MAGIC) {
+ this.log(`Warning: Image file at 0x${
+ address.toString(16)
+ } doesn't look like an image file, so not changing any flash settings.`);
+ return image;
+ }
+
+ /* XXX: Yet to implement actual image verification */
+
+ if (flash_mode !== 'keep') {
+ const flash_modes = {
+ qio: 0, qout: 1, dio: 2, dout: 3,
+ };
+ a_flash_mode = flash_modes[flash_mode];
+ }
+ let a_flash_freq = flash_size_freq & 0x0F;
+ if (flash_freq !== 'keep') {
+ const flash_freqs = {
+ '40m': 0, '26m': 1, '20m': 2, '80m': 0xf,
+ };
+ a_flash_freq = flash_freqs[flash_freq];
+ }
+ let a_flash_size = flash_size_freq & 0xF0;
+ if (flash_size !== 'keep') {
+ a_flash_size = this.parse_flash_size_arg(flash_size);
+ }
+
+ const flash_params = (a_flash_mode << 8) | (a_flash_freq + a_flash_size);
+ this.log(`Flash params set to ${flash_params.toString(16)}`);
+ if (image[2] !== (a_flash_mode << 8)) {
+ // eslint-disable-next-line no-param-reassign
+ image[2] = (a_flash_mode << 8);
+ }
+ if (image[3] !== (a_flash_freq + a_flash_size)) {
+ // eslint-disable-next-line no-param-reassign
+ image[3] = (a_flash_freq + a_flash_size);
+ }
+ return image;
+ }
+
+ write_flash = async ({
+ fileArray = [], flash_size = 'keep', flash_mode = 'keep', flash_freq = 'keep', erase_all = false, compress = true,
+ } = {}) => {
+ // console.log('EspLoader program');
+ if (flash_size !== 'keep') {
+ const flash_end = this.flash_size_bytes(flash_size);
+ for (let i = 0; i < fileArray.length; i++) {
+ if ((fileArray[i].data.length + fileArray[i].address) > flash_end) {
+ this.log("Specified file doesn't fit in the available flash");
+ return;
+ }
+ }
+ }
+
+ if (this.IS_STUB === true && erase_all === true) {
+ this.erase_flash();
+ }
+ let image;
+ let address;
+ for (let i = 0; i < fileArray.length; i++) {
+ // console.log(`Data Length ${fileArray[i].data.length}`);
+ // image = this.pad_array(fileArray[i].data, Math.floor((fileArray[i].data.length + 3)/4) * 4, 0xff);
+ // XXX : handle padding
+ image = fileArray[i].data;
+ address = fileArray[i].address;
+ // console.log(`Image Length ${image.length}`);
+ if (image.length === 0) {
+ this.log('Warning: File is empty');
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+ image = this._update_image_flash_params(image, address, flash_size, flash_mode, flash_freq);
+ const calcmd5 = CryptoJS.MD5(CryptoJS.enc.Base64.parse(image.toString('base64')));
+ // console.log(`Image MD5 ${calcmd5}`);
+ const uncsize = image.length;
+ let blocks;
+ // console.log(image);
+ if (compress) {
+ // const uncimage = this.bstrToUi8(image);
+ image = pako.deflate(image, { level: 9 });
+ // console.log('Compressed image ');
+ // console.log(image);
+ blocks = await this.flash_defl_begin(uncsize, image.length, address);
+ } else {
+ blocks = await this.flash_begin(uncsize, address);
+ }
+ let seq = 0;
+ let bytes_sent = 0;
+ // const bytes_written = 0;
+
+ let d = new Date();
+ const t1 = d.getTime();
+
+ let timeout = 5000;
+ while (image.length > 0) {
+ // console.log(`Write loop ${address} ${seq} ${blocks}`);
+ this.write_char(`\rWriting at 0x${
+ (address + (seq * this.FLASH_WRITE_SIZE)).toString(16)
+ }... (${
+ Math.floor(100 * ((seq + 1) / blocks))
+ }%)`);
+ let block = image.slice(0, this.FLASH_WRITE_SIZE);
+ if (compress) {
+ /*
+ let block_uncompressed = pako.inflate(block).length;
+ //let len_uncompressed = block_uncompressed.length;
+ bytes_written += block_uncompressed;
+ if (this.timeout_per_mb(this.ERASE_WRITE_TIMEOUT_PER_MB, block_uncompressed) > 3000) {
+ block_timeout = this.timeout_per_mb(this.ERASE_WRITE_TIMEOUT_PER_MB, block_uncompressed);
+ } else {
+ block_timeout = 3000;
+ } */ // XXX: Partial block inflate seems to be unsupported in Pako. Hardcoding timeout
+ const block_timeout = 5000;
+ if (this.IS_STUB === false) {
+ timeout = block_timeout;
+ }
+ await this.flash_defl_block(block, seq, timeout);
+ if (this.IS_STUB) {
+ timeout = block_timeout;
+ }
+ } else {
+ // this.log('Yet to handle Non Compressed writes');
+ // block = block + b'\xff' * (esp.FLASH_WRITE_SIZE - len(block))
+ if (block.length < this.FLASH_WRITE_SIZE) {
+ const existingBlock = block.toString('base64');
+ block = Buffer.alloc(this.FLASH_WRITE_SIZE, 0xff);
+ block.write(existingBlock, 'base64');
+ }
+ // if encrypted:
+ // esp.flash_encrypt_block(block, seq)
+ // else:
+ // esp.flash_block(block, seq)
+ // bytes_written += len(block)
+ await this.flash_block(block, seq, timeout);
+ }
+ bytes_sent += block.length;
+ image = image.slice(this.FLASH_WRITE_SIZE, image.length);
+ seq++;
+ }
+ if (this.IS_STUB) {
+ await this.read_reg({ addr: this.CHIP_DETECT_MAGIC_REG_ADDR, timeout });
+ }
+ d = new Date();
+ const t = d.getTime() - t1;
+ this.log('');
+ this.log(`Wrote ${uncsize} bytes${
+ compress ? ` (${bytes_sent} compressed)` : ''
+ } at 0x${address.toString(16)} in ${t / 1000} seconds.`);
+ this._sleep(100);
+ if (this.IS_STUB || this.chip.CHIP_NAME !== 'ESP8266') {
+ const res = await this.flash_md5sum(address, uncsize);
+ if (`${res}` !== `${calcmd5}`) {
+ this.log(`File md5: ${calcmd5}`);
+ this.log(`Flash md5: ${res}`);
+ } else {
+ this.log('Hash of data verified.');
+ }
+ }
+ }
+ this.log('Leaving...');
+
+ if (this.IS_STUB) {
+ await this.flash_begin(0, 0);
+ if (compress) {
+ await this.flash_defl_finish();
+ } else {
+ await this.flash_finish();
+ }
+ }
+ }
+
+ flash_id = async () => {
+ // console.log('flash_id');
+ const flashid = await this.read_flash_id();
+ this.log(`Manufacturer: ${(flashid & 0xff).toString(16)}`);
+ const flid_lowbyte = (flashid >> 16) & 0xff;
+ this.log(`Device: ${((flashid >> 8) & 0xff).toString(16)}${flid_lowbyte.toString(16)}`);
+ this.log(`Detected flash size: ${this.DETECTED_FLASH_SIZES[flid_lowbyte] || 'Unknown'}`);
+ }
+}
diff --git a/src/plugins/uploader/esptool/index.js b/src/plugins/uploader/esptool/index.js
new file mode 100644
index 0000000..8fed3f6
--- /dev/null
+++ b/src/plugins/uploader/esptool/index.js
@@ -0,0 +1,71 @@
+/* eslint-disable camelcase */
+import ESPLoader from './ESPLoader';
+import Transport from './webserial';
+
+const asyncTimeout = (timeout) => new Promise((resolve) => setTimeout(() => resolve(timeout), timeout));
+const isValid = (board) => ['esp8266', 'esp32'].includes(board.props?.build?.mcu);
+
+// eslint-disable-next-line no-unused-vars
+const upload = async ({ files, flash_mode, flash_freq }, board, serial, config) => {
+ const log = (...args) => config.debug(`${args.join(' ')}\r\n`);
+ const term = { log, debug: log, write: config.debug };
+
+ const { port } = serial;
+ const transport = new Transport(port, term);
+ let espLoader;
+
+ try {
+ log('> Connecting...');
+ espLoader = new ESPLoader(transport, term);
+ await espLoader.main_fn();
+ // await espLoader.flash_id();
+ log('> Connected');
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ log('Failed to connect:', typeof err === 'string' ? err : err.message);
+ try {
+ await transport.disconnect();
+ } catch (err2) {
+ // eslint-disable-next-line no-console
+ console.error(err2);
+ }
+ return;
+ }
+
+ try {
+ if (board.config?.wipe && board.config.wipe !== 'none') {
+ log('> Erasing device flash...');
+ await espLoader.erase_flash();
+ log('> Successfully erased device flash');
+ }
+ log('> Writing main data partition, this may take a while...');
+ await espLoader.write_flash({
+ fileArray: files.map((file) => ({ ...file, data: Buffer.from(file.data, 'base64') })),
+ flash_size: 'keep',
+ // flash_freq,
+ // flash_mode,
+ // compress: board.props?.build?.mcu !== 'esp8266',
+ });
+ await espLoader.flash_defl_finish({ reboot: true });
+ await asyncTimeout(100);
+ log('> Successfully written data partition');
+ log('> Flashing succeeded! Have a nice day! :)');
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ log('Failed to upload:', typeof err === 'string' ? err : err.message);
+ }
+
+ try {
+ await transport.disconnect();
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ }
+};
+
+export default {
+ isValid,
+ default: upload,
+};
diff --git a/src/plugins/uploader/esptool/webserial.js b/src/plugins/uploader/esptool/webserial.js
new file mode 100644
index 0000000..a5ea78d
--- /dev/null
+++ b/src/plugins/uploader/esptool/webserial.js
@@ -0,0 +1,191 @@
+/* eslint-disable no-plusplus */
+/* eslint-disable class-methods-use-this */
+/* eslint-disable no-continue */
+/* eslint-disable camelcase */
+
+export default class Transport {
+ constructor(device, logger = console) {
+ this.device = device;
+ this.slip_reader_enabled = false;
+ this.logger = logger;
+ }
+
+ get_info() {
+ const info = this.device.getInfo();
+ return `WebSerial VendorID 0x${info.usbVendorId.toString(16)} ProductID 0x${info.usbProductId.toString(16)}`;
+ }
+
+ slip_writer(data) {
+ let count_esc = 0;
+ let i = 0;
+ let j = 0;
+
+ for (i = 0; i < data.length; i++) {
+ if (data[i] === 0xC0 || data[i] === 0xDB) {
+ count_esc++;
+ }
+ }
+ const out_data = new Uint8Array(2 + count_esc + data.length);
+ out_data[0] = 0xC0;
+ j = 1;
+ for (i = 0; i < data.length; i++, j++) {
+ if (data[i] === 0xC0) {
+ out_data[j++] = 0xDB;
+ out_data[j] = 0xDC;
+ continue;
+ }
+ if (data[i] === 0xDB) {
+ out_data[j++] = 0xDB;
+ out_data[j] = 0xDD;
+ continue;
+ }
+
+ out_data[j] = data[i];
+ }
+ out_data[j] = 0xC0;
+ return out_data;
+ }
+
+ write = async (data) => {
+ const writer = this.device.writable.getWriter();
+ const out_data = this.slip_writer(data);
+ await writer.write(out_data.buffer);
+ writer.releaseLock();
+ }
+
+ _appendBuffer(buffer1, buffer2) {
+ const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
+ tmp.set(new Uint8Array(buffer1), 0);
+ tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
+ return tmp.buffer;
+ }
+
+ /* this function expects complete packet (hence reader reads for atleast 8 bytes. This function is
+ * stateless and returns the first wellformed packet only after replacing escape sequence */
+ slip_reader(data) {
+ let i = 0;
+ let data_start = 0;
+ let data_end = 0;
+ let state = 'init';
+ while (i < data.length) {
+ if (state === 'init' && data[i] === 0xC0 && data[i + 1] !== 0xC0) {
+ data_start = i + 1;
+ state = 'valid_data';
+ i++;
+ continue;
+ }
+ if (state === 'valid_data' && data[i] === 0xC0) {
+ data_end = i - 1;
+ state = 'packet_complete';
+ break;
+ }
+ i++;
+ }
+ // console.log(state, data, data_start, data_end);
+ if (state === 'valid_data' && data[data.length - 1] === 0) {
+ data_end = data.length - 1;
+ state = 'packet_complete';
+ }
+ if (state !== 'packet_complete') {
+ return new Uint8Array(0);
+ }
+
+ const temp_pkt = new Uint8Array(data_end - data_start + 1);
+ let j = 0;
+ for (i = data_start; i <= data_end; i++, j++) {
+ if (data[i] === 0xDB && data[i + 1] === 0xDC) {
+ temp_pkt[j] = 0xC0;
+ i++;
+ continue;
+ }
+ if (data[i] === 0xDB && data[i + 1] === 0xDD) {
+ temp_pkt[j] = 0xDB;
+ i++;
+ continue;
+ }
+ temp_pkt[j] = data[i];
+ }
+ const packet = temp_pkt.slice(0, j); /* Remove unused bytes due to escape seq */
+ return packet;
+ }
+
+ read = async ({ timeout = 0, min_data = 12 } = {}) => {
+ let t;
+ let packet = null;
+ let value;
+ let done;
+ // this.logger.log(`Read with timeout ${timeout}`);
+ const reader = this.device.readable.getReader();
+ if (timeout > 0) {
+ t = setTimeout(() => {
+ reader.cancel();
+ reader.releaseLock();
+ }, timeout);
+ }
+
+ do {
+ this.reader = reader;
+ // eslint-disable-next-line no-await-in-loop
+ const o = await reader.read();
+ this.reader = null;
+ value = o.value;
+ done = o.done;
+ if (packet == null) {
+ if (value) packet = value;
+ } else {
+ const p = new Uint8Array(this._appendBuffer(packet.buffer, value.buffer));
+ packet = p;
+ }
+ if (done) {
+ break;
+ }
+ } while (packet.length < min_data);
+
+ if (done) {
+ // console.log('timed out', packet);
+ // eslint-disable-next-line no-throw-literal
+ throw ('timeout');
+ } else {
+ if (timeout > 0) {
+ clearTimeout(t);
+ }
+ reader.releaseLock();
+ if (this.slip_reader_enabled) {
+ const val_final = this.slip_reader(packet);
+ return val_final;
+ }
+ return packet;
+ }
+ }
+
+ rawRead = async () => {
+ const reader = this.device.readable.getReader();
+
+ this.reader = reader;
+ const o = await reader.read();
+ this.reader = null;
+ reader.releaseLock();
+ return o.value;
+ }
+
+ setRTS = async (state) => {
+ await this.device.setSignals({ requestToSend: state });
+ }
+
+ setDTR = async (state) => {
+ await this.device.setSignals({ dataTerminalReady: state });
+ }
+
+ connect = async () => {
+ await this.device.open({ baudRate: 115200 });
+ this.baudrate = 115200;
+ }
+
+ disconnect = async () => {
+ if (this.reader !== null) {
+ this.reader.cancel();
+ this.reader.releaseLock();
+ }
+ await this.device.close();
+ }
+}
diff --git a/src/plugins/uploader/index.js b/src/plugins/uploader/index.js
index b899154..6505c9f 100644
--- a/src/plugins/uploader/index.js
+++ b/src/plugins/uploader/index.js
@@ -3,6 +3,7 @@ import get from 'lodash/get';
import store from '../../store';
// import avrdude from './stk500';
import avrdude from './avrgirl';
+import esptool from './esptool';
const asyncTimeout = (timeout) => new Promise((resolve) => setTimeout(() => resolve(timeout), timeout));
@@ -15,13 +16,15 @@ class Uploader extends EventEmitter {
this.Vue = Vue;
this.toolMap = {
avrdude,
+ esptool,
+ esptool_py: esptool,
};
}
// eslint-disable-next-line class-methods-use-this
isSupported(board) { // eslint-disable-line no-unused-vars
const tool = get(board, 'props.upload.tool', '').split(':').pop();
- const toolProt = `${tool}.${get(board, 'props.upload.protocol')}`;
+ const toolProt = `${tool}.${get(board, 'props.upload.protocol', 'default')}`;
return !!get(this.toolMap, toolProt) && get(this.toolMap, tool).isValid(board);
}
@@ -31,11 +34,11 @@ class Uploader extends EventEmitter {
return this.waitForClose(serial, count + 1);
}
- async upload(hex, config) {
+ async upload(res, config) {
const serial = this.Vue.$serial;
const existBaud = serial.baud;
const [board] = store.getters['boards/find']({ query: { uuid: store.getters.currentBoard } }).data;
- const toolProt = `${board?.props?.upload?.tool?.split(':').pop()}.${board?.props?.upload?.protocol}`;
+ const toolProt = `${board?.props?.upload?.tool?.split(':').pop()}.${board?.props?.upload?.protocol || 'default'}`;
const uploader = get(this.toolMap, toolProt);
if (!uploader) throw new Error('Board not currently supported');
@@ -43,9 +46,10 @@ class Uploader extends EventEmitter {
await serial.setMute(true);
await serial.disconnect();
- await uploader(hex, board, serial.serial, {
+ await uploader(res, board, serial.serial, {
...config,
debug: (message) => this.Vue.$compiler.emit('console.log', message),
+ progress: (message, percent) => this.Vue.$compiler.emit('console.progress', { message, percent: (percent * 0.5) + 0.5 }),
});
await this.waitForClose(serial);
diff --git a/src/store/services/boards.js b/src/store/services/boards.js
index f7b956a..c05d2f2 100644
--- a/src/store/services/boards.js
+++ b/src/store/services/boards.js
@@ -16,7 +16,11 @@ class Board extends BaseModel {
if (!this.config_options) return {};
const config = {};
this.config_options.forEach((con) => {
- config[con.option] = (con.values.find((val) => val.selected) || {}).value;
+ config[con.option] = (
+ con.values.find((val) => val.selected)
+ || con.values.find((val) => val.isDefault)
+ || {}
+ ).value;
});
return config;
},
diff --git a/src/store/tools.js b/src/store/tools.js
index 5b40d8b..36a8653 100644
--- a/src/store/tools.js
+++ b/src/store/tools.js
@@ -41,4 +41,7 @@ export const settingsDefaults = {
compiler: {
verbose: false,
},
+ monitor: {
+ encoding: 'ascii',
+ },
};
diff --git a/src/views/tools/Settings.vue b/src/views/tools/Settings.vue
index 80fd0af..2ec027b 100644
--- a/src/views/tools/Settings.vue
+++ b/src/views/tools/Settings.vue
@@ -12,17 +12,22 @@
+
+
+
diff --git a/yarn.lock b/yarn.lock
index 979688e..ae29a10 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3203,6 +3203,11 @@ crypto-browserify@^3.11.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
+crypto-js@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.0.0.tgz#2904ab2677a9d042856a2ea2ef80de92e4a36dcc"
+ integrity sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==
+
css-color-names@0.0.4, css-color-names@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@@ -6959,6 +6964,11 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+pako@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.3.tgz#cdf475e31b678565251406de9e759196a0ea7a43"
+ integrity sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw==
+
pako@~1.0.5:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"