esp-controller
Version:
Typescript package for connecting and flashing images to your ESP device.
1,660 lines (1,637 loc) • 57.2 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
AppPartitionSubType: () => AppPartitionSubType,
BinFilePartition: () => BinFilePartition,
DataPartitionSubType: () => DataPartitionSubType,
ESPImage: () => ESPImage,
NVSPartition: () => NVSPartition,
PartitionTable: () => PartitionTable,
PartitionType: () => PartitionType,
SerialController: () => SerialController
});
module.exports = __toCommonJS(index_exports);
// src/utils/common.ts
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function toHex(bytes) {
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
}
function slipEncode(buffer) {
const encoded = [192 /* END */];
for (const byte of buffer) {
if (byte === 192 /* END */) {
encoded.push(219 /* ESC */, 220 /* ESC_END */);
} else if (byte === 219 /* ESC */) {
encoded.push(219 /* ESC */, 221 /* ESC_ESC */);
} else {
encoded.push(byte);
}
}
encoded.push(192 /* END */);
return new Uint8Array(encoded);
}
function base64ToUint8Array(base64) {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
// src/esp/stream-transformers.ts
var LineBreakTransformer = class {
buffer = "";
transform(chunk, controller) {
this.buffer += chunk;
const lines = this.buffer?.split("\r\n");
this.buffer = lines?.pop();
lines?.forEach((line) => controller.enqueue(line));
}
};
var SlipStreamTransformer = class {
// Buffer to accumulate bytes for the current frame.
/**
* Constructs a new SlipStreamTransformer.
* @param mode Specifies whether the transformer should operate in "encoding" or "decoding" mode.
*/
constructor(mode) {
this.mode = mode;
if (this.mode === "encoding") {
this.decoding = false;
}
}
decoding = false;
// Flag to indicate if the initial END byte for a packet has been received in decoding mode.
escape = false;
// Flag to indicate if the current byte is an escape character in decoding mode.
frame = [];
transform(chunk, controller) {
if (this.mode === "decoding") {
for (const byte of chunk) {
if (this.decoding) {
if (this.escape) {
if (byte === 220 /* ESC_END */) {
this.frame.push(192 /* END */);
} else if (byte === 221 /* ESC_ESC */) {
this.frame.push(219 /* ESC */);
} else {
this.frame.push(byte);
}
this.escape = false;
} else if (byte === 219 /* ESC */) {
this.escape = true;
} else if (byte === 192 /* END */) {
if (this.frame.length > 0) {
controller.enqueue(new Uint8Array(this.frame));
}
this.frame = [];
} else {
this.frame.push(byte);
}
} else if (byte === 192 /* END */) {
this.decoding = true;
this.frame = [];
this.escape = false;
}
}
} else {
for (const byte of chunk) {
if (byte === 192 /* END */) {
this.frame.push(219 /* ESC */, 220 /* ESC_END */);
} else if (byte === 219 /* ESC */) {
this.frame.push(219 /* ESC */, 221 /* ESC_ESC */);
} else {
this.frame.push(byte);
}
}
}
}
flush(controller) {
if (this.mode === "encoding") {
if (this.frame.length > 0) {
const finalPacket = new Uint8Array([
192 /* END */,
...this.frame,
192 /* END */
]);
controller.enqueue(finalPacket);
this.frame = [];
}
}
}
};
function createLineBreakTransformer() {
return new TransformStream(new LineBreakTransformer());
}
var SlipStreamEncoder = class extends TransformStream {
/**
* Constructs a new SlipStreamEncoder.
* This sets up the underlying SlipStreamTransformer in "encoding" mode.
*/
constructor() {
super(new SlipStreamTransformer("encoding"));
}
};
var SlipStreamDecoder = class extends TransformStream {
/**
* Constructs a new SlipStreamDecoder.
* This sets up the underlying SlipStreamTransformer in "decoding" mode.
*/
constructor() {
super(new SlipStreamTransformer("decoding"));
}
};
// src/esp/command.ts
var EspCommand = /* @__PURE__ */ ((EspCommand2) => {
EspCommand2[EspCommand2["FLASH_BEGIN"] = 2] = "FLASH_BEGIN";
EspCommand2[EspCommand2["FLASH_DATA"] = 3] = "FLASH_DATA";
EspCommand2[EspCommand2["FLASH_END"] = 4] = "FLASH_END";
EspCommand2[EspCommand2["MEM_BEGIN"] = 5] = "MEM_BEGIN";
EspCommand2[EspCommand2["MEM_END"] = 6] = "MEM_END";
EspCommand2[EspCommand2["MEM_DATA"] = 7] = "MEM_DATA";
EspCommand2[EspCommand2["SYNC"] = 8] = "SYNC";
EspCommand2[EspCommand2["WRITE_REG"] = 9] = "WRITE_REG";
EspCommand2[EspCommand2["READ_REG"] = 10] = "READ_REG";
EspCommand2[EspCommand2["SPI_SET_PARAMS"] = 11] = "SPI_SET_PARAMS";
EspCommand2[EspCommand2["SPI_ATTACH"] = 13] = "SPI_ATTACH";
EspCommand2[EspCommand2["CHANGE_BAUDRATE"] = 15] = "CHANGE_BAUDRATE";
EspCommand2[EspCommand2["FLASH_DEFL_BEGIN"] = 16] = "FLASH_DEFL_BEGIN";
EspCommand2[EspCommand2["FLASH_DEFL_DATA"] = 17] = "FLASH_DEFL_DATA";
EspCommand2[EspCommand2["FLASH_DEFL_END"] = 18] = "FLASH_DEFL_END";
EspCommand2[EspCommand2["SPI_FLASH_MD5"] = 19] = "SPI_FLASH_MD5";
EspCommand2[EspCommand2["ERASE_FLASH"] = 208] = "ERASE_FLASH";
EspCommand2[EspCommand2["ERASE_REGION"] = 209] = "ERASE_REGION";
EspCommand2[EspCommand2["READ_FLASH"] = 210] = "READ_FLASH";
EspCommand2[EspCommand2["RUN_USER_CODE"] = 211] = "RUN_USER_CODE";
return EspCommand2;
})(EspCommand || {});
var EspCommandPacket = class {
packetHeader = new Uint8Array(8);
packetData = new Uint8Array(0);
set direction(direction) {
new DataView(this.packetHeader.buffer, 0, 1).setUint8(0, direction);
}
get direction() {
return new DataView(this.packetHeader.buffer, 0, 1).getUint8(0);
}
set command(command) {
new DataView(this.packetHeader.buffer, 1, 1).setUint8(0, command);
}
get command() {
return new DataView(this.packetHeader.buffer, 1, 1).getUint8(0);
}
set size(size) {
new DataView(this.packetHeader.buffer, 2, 2).setUint16(0, size, true);
}
get size() {
return new DataView(this.packetHeader.buffer, 2, 2).getUint16(0, true);
}
set checksum(checksum) {
new DataView(this.packetHeader.buffer, 4, 4).setUint32(0, checksum, true);
}
get checksum() {
return new DataView(this.packetHeader.buffer, 4, 4).getUint32(0, true);
}
set value(value) {
new DataView(this.packetHeader.buffer, 4, 4).setUint32(0, value, true);
}
get value() {
return new DataView(this.packetHeader.buffer, 4, 4).getUint32(0, true);
}
get status() {
return new DataView(this.packetData.buffer, 0, 1).getUint8(0);
}
get error() {
return new DataView(this.packetData.buffer, 1, 1).getUint8(0);
}
generateChecksum(data) {
let cs = 239;
for (const byte of data) {
cs ^= byte;
}
return cs;
}
set data(packetData) {
this.size = packetData.length;
this.packetData = packetData;
}
get data() {
return this.packetData;
}
parseResponse(responsePacket) {
const responseDataView = new DataView(responsePacket.buffer);
this.direction = responseDataView.getUint8(0);
this.command = responseDataView.getUint8(1);
this.size = responseDataView.getUint16(2, true);
this.value = responseDataView.getUint32(4, true);
this.packetData = responsePacket.slice(8);
if (this.status === 1) {
console.log(this.getErrorMessage(this.error));
}
}
getErrorMessage(error) {
switch (error) {
case 5:
return "Status Error: Received message is invalid. (parameters or length field is invalid)";
case 6:
return "Failed to act on received message";
case 7:
return "Invalid CRC in message";
case 8:
return "flash write error - after writing a block of data to flash, the ROM loader reads the value back and the 8-bit CRC is compared to the data read from flash. If they don't match, this error is returned.";
case 9:
return "flash read error - SPI read failed";
case 10:
return "flash read length error - SPI read request length is too long";
case 11:
return "Deflate error (compressed uploads only)";
default:
return "No error status for response";
}
}
getPacketData() {
const header = new Uint8Array(8);
const view = new DataView(header.buffer);
view.setUint8(0, this.direction);
view.setUint8(1, this.command);
view.setUint16(2, this.data.length, true);
view.setUint32(4, this.checksum, true);
return new Uint8Array([...header, ...this.data]);
}
getSlipStreamEncodedPacketData() {
return slipEncode(this.getPacketData());
}
};
// src/esp/command.sync.ts
var EspCommandSync = class extends EspCommandPacket {
constructor() {
super();
this.direction = 0 /* REQUEST */;
this.command = 8 /* SYNC */;
this.data = new Uint8Array([
7,
7,
18,
32,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85,
85
]);
this.checksum = 0;
}
};
// src/esp/command.spi-attach.ts
var EspCommandSpiAttach = class extends EspCommandPacket {
spiAttachData = new ArrayBuffer(8);
view1 = new DataView(this.spiAttachData, 0, 4);
view2 = new DataView(this.spiAttachData, 4, 4);
constructor() {
super();
this.direction = 0 /* REQUEST */;
this.command = 13 /* SPI_ATTACH */;
this.view1.setUint32(0, 0, true);
this.view2.setUint32(0, 0, true);
this.data = new Uint8Array(this.spiAttachData);
}
};
// src/esp/command.spi-set-params.ts
var EspCommandSpiSetParams = class extends EspCommandPacket {
paramsData = new ArrayBuffer(24);
id = new DataView(this.paramsData, 0, 4);
totalSize = new DataView(this.paramsData, 4, 4);
blockSize = new DataView(this.paramsData, 8, 4);
sectorSize = new DataView(this.paramsData, 12, 4);
pageSize = new DataView(this.paramsData, 16, 4);
statusMask = new DataView(this.paramsData, 20, 4);
constructor() {
super();
this.direction = 0 /* REQUEST */;
this.command = 11 /* SPI_SET_PARAMS */;
this.id.setUint32(0, 0, true);
this.totalSize.setUint32(0, 4 * 1024 * 1024, true);
this.blockSize.setUint32(0, 65536, true);
this.sectorSize.setUint32(0, 4096, true);
this.pageSize.setUint32(0, 256, true);
this.statusMask.setUint32(0, 4294967295, true);
this.data = new Uint8Array(this.paramsData);
}
};
// src/esp/command.flash-begin.ts
var EspCommandFlashBegin = class extends EspCommandPacket {
flashBeginData = new ArrayBuffer(16);
eraseSizeView = new DataView(this.flashBeginData, 0, 4);
numDataPacketsView = new DataView(this.flashBeginData, 4, 4);
dataSizeView = new DataView(this.flashBeginData, 8, 4);
offsetView = new DataView(this.flashBeginData, 12, 4);
constructor(image, offset, packetSize, numPackets) {
super();
this.direction = 0 /* REQUEST */;
this.command = 2 /* FLASH_BEGIN */;
this.eraseSizeView.setUint32(0, image.length, true);
this.numDataPacketsView.setUint32(0, numPackets, true);
this.dataSizeView.setUint32(0, packetSize, true);
this.offsetView.setUint32(0, offset, true);
this.data = new Uint8Array(this.flashBeginData);
}
};
// src/esp/command.flash-data.ts
var EspCommandFlashData = class extends EspCommandPacket {
constructor(image, sequenceNumber, blockSize) {
super();
this.direction = 0 /* REQUEST */;
this.command = 3 /* FLASH_DATA */;
const flashDownloadData = new Uint8Array(16 + blockSize);
const blockSizeView = new DataView(flashDownloadData.buffer, 0, 4);
const sequenceView = new DataView(flashDownloadData.buffer, 4, 4);
const paddingView = new DataView(flashDownloadData.buffer, 8, 8);
blockSizeView.setUint32(0, blockSize, true);
sequenceView.setUint32(0, sequenceNumber, true);
paddingView.setUint32(0, 0, true);
paddingView.setUint32(4, 0, true);
const block = image.slice(
sequenceNumber * blockSize,
sequenceNumber * blockSize + blockSize
);
const blockData = new Uint8Array(blockSize);
blockData.fill(255);
blockData.set(block, 0);
flashDownloadData.set(blockData, 16);
this.data = flashDownloadData;
this.checksum = this.generateChecksum(blockData);
}
};
// src/esp/command.read-reg.ts
var EspCommandReadReg = class extends EspCommandPacket {
readRegData = new ArrayBuffer(4);
constructor(address) {
super();
this.command = 10 /* READ_REG */;
this.direction = 0 /* REQUEST */;
new DataView(this.readRegData).setUint32(0, address, true);
this.data = new Uint8Array(this.readRegData);
}
};
// src/esp/command.mem-begin.ts
var EspCommandMemBegin = class extends EspCommandPacket {
constructor(totalSize, numPackets, packetSize, offset) {
super();
this.totalSize = totalSize;
this.numPackets = numPackets;
this.packetSize = packetSize;
this.offset = offset;
this.direction = 0 /* REQUEST */;
this.command = 5 /* MEM_BEGIN */;
this.checksum = 0;
const dataPayload = new Uint8Array(16);
const view = new DataView(dataPayload.buffer);
view.setUint32(0, this.totalSize, true);
view.setUint32(4, this.numPackets, true);
view.setUint32(8, this.packetSize, true);
view.setUint32(12, this.offset, true);
this.data = dataPayload;
}
};
// src/esp/command.mem-data.ts
var EspCommandMemData = class extends EspCommandPacket {
constructor(binary, sequence, packetSize) {
super();
this.binary = binary;
this.sequence = sequence;
this.packetSize = packetSize;
const chunk = binary.slice(
sequence * packetSize,
(sequence + 1) * packetSize
);
const header = new Uint8Array(16);
const view = new DataView(header.buffer);
view.setUint32(0, chunk.length, true);
view.setUint32(4, sequence, true);
view.setUint32(8, 0, true);
view.setUint32(12, 0, true);
this.direction = 0 /* REQUEST */;
this.command = 7 /* MEM_DATA */;
this.data = new Uint8Array([...header, ...chunk]);
this.checksum = this.generateChecksum(chunk);
}
};
// src/esp/command.mem-end.ts
var EspCommandMemEnd = class extends EspCommandPacket {
constructor(executeFlag, entryPoint) {
super();
this.executeFlag = executeFlag;
this.entryPoint = entryPoint;
this.direction = 0 /* REQUEST */;
this.command = 6 /* MEM_END */;
this.checksum = 0;
const dataPayload = new Uint8Array(8);
const view = new DataView(dataPayload.buffer);
view.setUint32(0, this.executeFlag, true);
view.setUint32(4, this.entryPoint, true);
this.data = dataPayload;
}
};
// src/esp/serial-controller.ts
var DEFAULT_ESP32_SERIAL_OPTIONS = {
baudRate: 115200,
dataBits: 8,
stopBits: 1,
bufferSize: 255,
parity: "none",
flowControl: "none"
};
var ChipFamily = /* @__PURE__ */ ((ChipFamily2) => {
ChipFamily2[ChipFamily2["ESP32"] = 15736195] = "ESP32";
ChipFamily2[ChipFamily2["ESP32S2"] = 1990] = "ESP32S2";
ChipFamily2[ChipFamily2["ESP32S3"] = 9] = "ESP32S3";
ChipFamily2[ChipFamily2["ESP32C3"] = 1763790959] = "ESP32C3";
ChipFamily2[ChipFamily2["ESP32C6"] = 752910447] = "ESP32C6";
ChipFamily2[ChipFamily2["ESP32H2"] = 3389177967] = "ESP32H2";
ChipFamily2[ChipFamily2["ESP8266"] = 4293968129] = "ESP8266";
ChipFamily2[ChipFamily2["UNKNOWN"] = 4294967295] = "UNKNOWN";
return ChipFamily2;
})(ChipFamily || {});
var SerialController = class extends EventTarget {
connection;
constructor() {
super();
this.connection = this.createSerialConnection();
}
createSerialConnection() {
return {
port: void 0,
connected: false,
synced: false,
chip: null,
readable: null,
writable: null,
abortStreamController: void 0,
commandResponseStream: void 0
};
}
async requestPort() {
this.connection.port = await navigator.serial.requestPort();
this.connection.synced = false;
this.connection.chip = null;
}
createLogStreamReader() {
if (!this.connection.connected || !this.connection.readable || !this.connection.abortStreamController)
return async function* logStream() {
};
const streamPipeOptions = {
signal: this.connection.abortStreamController.signal,
preventCancel: false,
preventClose: false,
preventAbort: false
};
const [newReadable, logReadable] = this.connection.readable.tee();
this.connection.readable = newReadable;
const reader = logReadable.pipeThrough(new TextDecoderStream(), streamPipeOptions).pipeThrough(createLineBreakTransformer(), streamPipeOptions).getReader();
const connection = this.connection;
return async function* logStream() {
try {
while (connection.connected) {
const result = await reader?.read();
if (result?.done) return;
yield result?.value;
}
} finally {
reader?.releaseLock();
}
};
}
async openPort(options = DEFAULT_ESP32_SERIAL_OPTIONS) {
if (!this.connection.port) return;
await this.connection?.port.open(options);
if (!this.connection.port?.readable) return;
this.connection.abortStreamController = new AbortController();
const [commandTee, logTee] = this.connection.port.readable.tee();
this.connection.connected = true;
this.connection.readable = logTee;
this.connection.writable = this.connection.port.writable;
this.connection.commandResponseStream = commandTee.pipeThrough(
new SlipStreamDecoder(),
{ signal: this.connection.abortStreamController.signal }
);
}
async disconnect() {
if (!this.connection.connected || !this.connection.port) {
return;
}
this.connection.abortStreamController?.abort();
try {
await this.connection.port.close();
} catch (error) {
console.error("Failed to close the serial port:", error);
}
const port = this.connection.port;
this.connection = this.createSerialConnection();
this.connection.port = port;
}
async sendResetPulse() {
if (!this.connection.port) return;
this.connection.port.setSignals({
dataTerminalReady: false,
requestToSend: true
});
await sleep(100);
this.connection.port.setSignals({
dataTerminalReady: true,
requestToSend: false
});
await sleep(100);
}
async writeToConnection(data) {
if (this.connection.writable) {
const writer = this.connection.writable.getWriter();
await writer.write(data);
writer.releaseLock();
}
}
async sync() {
await this.sendResetPulse();
const maxAttempts = 10;
const timeoutPerAttempt = 500;
const syncCommand = new EspCommandSync();
for (let i = 0; i < maxAttempts; i++) {
this.dispatchEvent(
new CustomEvent("sync-progress", {
detail: { progress: i / maxAttempts * 100 }
})
);
console.log(`Sync attempt ${i + 1} of ${maxAttempts}`);
await this.writeToConnection(
syncCommand.getSlipStreamEncodedPacketData()
);
let responseReader;
try {
if (!this.connection.commandResponseStream) {
throw new Error(`No command response stream available.`);
}
responseReader = this.connection.commandResponseStream.getReader();
const timeoutPromise = sleep(timeoutPerAttempt).then(() => {
throw new Error(`Timeout after ${timeoutPerAttempt}ms`);
});
while (true) {
const { value, done } = await Promise.race([
responseReader.read(),
timeoutPromise
]);
if (done) {
throw new Error("Stream closed unexpectedly while syncing.");
}
if (value) {
try {
const responsePacket = new EspCommandPacket();
responsePacket.parseResponse(value);
if (responsePacket.command === 8 /* SYNC */) {
console.log("SYNCED successfully.", responsePacket);
this.connection.synced = true;
this.dispatchEvent(
new CustomEvent("sync-progress", {
detail: { progress: 100 }
})
);
return true;
}
} catch {
}
}
}
} catch (e) {
console.log(`Sync attempt ${i + 1} failed.`, e);
} finally {
if (responseReader) {
responseReader.releaseLock();
}
}
await sleep(100);
}
console.log("Failed to sync with the device.");
this.connection.synced = false;
return false;
}
async detectChip() {
if (!this.connection.synced) {
throw new Error("Device must be synced to detect chip type.");
}
const CHIP_DETECT_MAGIC_REG_ADDR = 1073745920;
const readRegCmd = new EspCommandReadReg(CHIP_DETECT_MAGIC_REG_ADDR);
await this.writeToConnection(readRegCmd.getSlipStreamEncodedPacketData());
const response = await this.readResponse(10 /* READ_REG */);
const magicValue = response.value;
const numericChipValues = Object.values(ChipFamily).filter(
(v) => typeof v === "number"
);
const chip = numericChipValues.find((c) => c === magicValue) || 4294967295 /* UNKNOWN */;
this.connection.chip = chip;
console.log(
`Detected chip: ${ChipFamily[chip]} (Magic value: ${toHex(new Uint8Array(new Uint32Array([magicValue]).buffer))})`
);
if (chip === 4294967295 /* UNKNOWN */) {
throw new Error("Could not detect a supported chip family.");
}
return chip;
}
async loadToRam(binary, offset, execute = false, entryPoint = 0) {
console.log(
`Loading binary to RAM at offset ${toHex(new Uint8Array(new Uint32Array([offset]).buffer))}`
);
const packetSize = 1460;
const numPackets = Math.ceil(binary.length / packetSize);
const memBeginCmd = new EspCommandMemBegin(
binary.length,
numPackets,
packetSize,
offset
);
await this.writeToConnection(memBeginCmd.getSlipStreamEncodedPacketData());
await this.readResponse(5 /* MEM_BEGIN */);
for (let i = 0; i < numPackets; i++) {
const memDataCmd = new EspCommandMemData(binary, i, packetSize);
await this.writeToConnection(memDataCmd.getSlipStreamEncodedPacketData());
await this.readResponse(7 /* MEM_DATA */, 1e3);
}
if (execute) {
console.log(`Executing from entry point ${entryPoint}`);
const memEndCmd = new EspCommandMemEnd(1, entryPoint);
await this.writeToConnection(memEndCmd.getSlipStreamEncodedPacketData());
await this.readResponse(6 /* MEM_END */);
}
}
/**
* Fetches the stub for the given chip family from the local file system.
* @param chip The chip family to fetch the stub for.
* @returns A promise that resolves to the Stub object.
*/
async getStubForChip(chip) {
const chipNameMap = {
[15736195 /* ESP32 */]: "32",
[1990 /* ESP32S2 */]: "32s2",
[9 /* ESP32S3 */]: "32s3",
[1763790959 /* ESP32C3 */]: "32c3",
[752910447 /* ESP32C6 */]: "32c6",
[3389177967 /* ESP32H2 */]: "32h2",
[4293968129 /* ESP8266 */]: "8266"
};
const chipName = chipNameMap[chip];
if (!chipName) {
throw new Error(`No stub file mapping for chip: ${ChipFamily[chip]}`);
}
const stubUrl = `./stub-flasher/stub_flasher_${chipName}.json`;
console.log(`Fetching stub from ${stubUrl}`);
try {
const response = await fetch(stubUrl);
if (!response.ok) {
throw new Error(`Failed to fetch stub file: ${response.statusText}`);
}
return await response.json();
} catch (e) {
console.error(`Error loading stub for ${ChipFamily[chip]}:`, e);
throw e;
}
}
async uploadStub(stub) {
const text = base64ToUint8Array(stub.text);
const data = base64ToUint8Array(stub.data);
await this.loadToRam(text, stub.text_start, false);
await this.loadToRam(data, stub.data_start, false);
console.log(`Starting stub at entry point 0x${stub.entry.toString(16)}...`);
const memEndCmd = new EspCommandMemEnd(1, stub.entry);
await this.writeToConnection(memEndCmd.getSlipStreamEncodedPacketData());
await this.readResponse(6 /* MEM_END */);
console.log("Stub started successfully.");
await this.awaitOhaiResponse();
}
async awaitOhaiResponse(timeout = 2e3) {
let responseReader;
const ohaiPacket = new Uint8Array([79, 72, 65, 73]);
try {
if (!this.connection.commandResponseStream) {
throw new Error("No command response stream available.");
}
responseReader = this.connection.commandResponseStream.getReader();
const timeoutPromise = sleep(timeout).then(() => {
throw new Error(
`Timeout: Did not receive "OHAI" from stub within ${timeout}ms.`
);
});
console.log("Waiting for 'OHAI' packet from stub...");
while (true) {
const { value, done } = await Promise.race([
responseReader.read(),
timeoutPromise
]);
if (done) {
throw new Error(
"Stream closed unexpectedly while waiting for 'OHAI'."
);
}
if (value && value.length === ohaiPacket.length) {
if (value.every((byte, index) => byte === ohaiPacket[index])) {
console.log("'OHAI' packet received, stub confirmed.");
return;
}
}
}
} finally {
if (responseReader) {
responseReader.releaseLock();
}
}
}
async readResponse(expectedCommand, timeout = 2e3) {
let responseReader;
try {
if (!this.connection.commandResponseStream) {
throw new Error(`No command response stream available.`);
}
responseReader = this.connection.commandResponseStream.getReader();
const timeoutPromise = sleep(timeout).then(() => {
throw new Error(
`Timeout: No response received for command ${EspCommand[expectedCommand]} within ${timeout}ms.`
);
});
while (true) {
const { value, done } = await Promise.race([
responseReader.read(),
timeoutPromise
]);
if (done) {
throw new Error(
"Stream closed unexpectedly while awaiting response."
);
}
if (value) {
try {
const responsePacket = new EspCommandPacket();
responsePacket.parseResponse(value);
if (responsePacket.direction === 1 /* RESPONSE */ && responsePacket.command === expectedCommand) {
if (responsePacket.error > 0) {
throw new Error(
`Device returned error for ${EspCommand[expectedCommand]}: ${responsePacket.getErrorMessage(responsePacket.error)}`
);
}
return responsePacket;
}
} catch {
}
}
}
} finally {
if (responseReader) {
responseReader.releaseLock();
}
}
}
async flashPartition(partition) {
console.log(
`Flashing partition: ${partition.filename}, offset: ${toHex(
new Uint8Array(new Uint32Array([partition.offset]).buffer)
)}`
);
const packetSize = 4096;
const numPackets = Math.ceil(partition.binary.length / packetSize);
const flashBeginCmd = new EspCommandFlashBegin(
partition.binary,
partition.offset,
packetSize,
numPackets
);
await this.writeToConnection(
flashBeginCmd.getSlipStreamEncodedPacketData()
);
await this.readResponse(2 /* FLASH_BEGIN */);
console.log("FLASH_BEGIN successful.");
for (let i = 0; i < numPackets; i++) {
const flashDataCmd = new EspCommandFlashData(
partition.binary,
i,
packetSize
);
await this.writeToConnection(
flashDataCmd.getSlipStreamEncodedPacketData()
);
this.dispatchEvent(
new CustomEvent("flash-progress", {
detail: {
progress: (i + 1) / numPackets * 100,
partition
}
})
);
console.log(
`[${partition.filename}] Writing block ${i + 1}/${numPackets}`
);
await this.readResponse(3 /* FLASH_DATA */, 5e3);
}
console.log(`Flash data for ${partition.filename} sent successfully.`);
}
/**
* Main method to flash a complete image.
* @param image The ESPImage to flash.
*/
async flashImage(image) {
if (!this.connection.connected) {
throw new Error("Device is not connected.");
}
if (!this.connection.synced) {
const synced = await this.sync();
if (!synced) {
throw new Error(
"ESP32 Needs to Sync before flashing. Hold the `boot` button on the device during sync attempts."
);
}
}
if (!this.connection.chip) {
await this.detectChip();
}
const stub = await this.getStubForChip(this.connection.chip);
await this.uploadStub(stub);
const attachCmd = new EspCommandSpiAttach();
await this.writeToConnection(attachCmd.getSlipStreamEncodedPacketData());
await this.readResponse(13 /* SPI_ATTACH */);
console.log("SPI_ATTACH successful.");
const setParamsCmd = new EspCommandSpiSetParams();
await this.writeToConnection(setParamsCmd.getSlipStreamEncodedPacketData());
await this.readResponse(11 /* SPI_SET_PARAMS */);
console.log("SPI_SET_PARAMS successful.");
const totalSize = image.partitions.reduce(
(acc, part) => acc + part.binary.length,
0
);
let flashedSize = 0;
for (const partition of image.partitions) {
const originalDispatchEvent = this.dispatchEvent;
this.dispatchEvent = (event) => {
if (event.type === "flash-progress" && "detail" in event) {
const partitionFlashed = partition.binary.length * event.detail.progress / 100;
originalDispatchEvent.call(
this,
new CustomEvent("flash-image-progress", {
detail: {
progress: (flashedSize + partitionFlashed) / totalSize * 100,
partition
}
})
);
}
return originalDispatchEvent.call(this, event);
};
await this.flashPartition(partition);
flashedSize += partition.binary.length;
this.dispatchEvent = originalDispatchEvent;
}
this.dispatchEvent(
new CustomEvent("flash-image-progress", {
detail: { progress: 100 }
})
);
console.log("Flashing complete. Resetting device...");
await this.sendResetPulse();
console.log("Device has been reset.");
}
};
// src/image/bin-file-partition.ts
var BinFilePartition = class {
constructor(offset, filename) {
this.offset = offset;
this.filename = filename;
}
binary = new Uint8Array(0);
async load() {
try {
const response = await fetch(this.filename);
if (!response.ok) {
console.error(
`Failed to fetch ${this.filename}: ${response.statusText}`
);
return false;
}
this.binary = new Uint8Array(await response.arrayBuffer());
return true;
} catch (e) {
console.error(`Error loading file ${this.filename}:`, e);
return false;
}
}
};
// src/image/image.ts
var ESPImage = class {
partitions = [];
addBootloader(fileName) {
this.partitions.push(new BinFilePartition(4096, fileName));
}
addPartitionTable(fileName) {
this.partitions.push(new BinFilePartition(32768, fileName));
}
addApp(fileName) {
this.partitions.push(new BinFilePartition(65536, fileName));
}
addPartition(partition) {
this.partitions.push(partition);
}
};
// src/utils/crc32.ts
var crc32Table = [
0,
1996959894,
3993919788,
2567524794,
124634137,
1886057615,
3915621685,
2657392035,
249268274,
2044508324,
3772115230,
2547177864,
162941995,
2125561021,
3887607047,
2428444049,
498536548,
1789927666,
4089016648,
2227061214,
450548861,
1843258603,
4107580753,
2211677639,
325883990,
1684777152,
4251122042,
2321926636,
335633487,
1661365465,
4195302755,
2366115317,
997073096,
1281953886,
3579855332,
2724688242,
1006888145,
1258607687,
3524101629,
2768942443,
901097722,
1119000684,
3686517206,
2898065728,
853044451,
1172266101,
3705015759,
2882616665,
651767980,
1373503546,
3369554304,
3218104598,
565507253,
1454621731,
3485111705,
3099436303,
671266974,
1594198024,
3322730930,
2970347812,
795835527,
1483230225,
3244367275,
3060149565,
1994146192,
31158534,
2563907772,
4023717930,
1907459465,
112637215,
2680153253,
3904427059,
2013776290,
251722036,
2517215374,
3775830040,
2137656763,
141376813,
2439277719,
3865271297,
1802195444,
476864866,
2238001368,
4066508878,
1812370925,
453092731,
2181625025,
4111451223,
1706088902,
314042704,
2344532202,
4240017532,
1658658271,
366619977,
2362670323,
4224994405,
1303535960,
984961486,
2747007092,
3569037538,
1256170817,
1037604311,
2765210733,
3554079995,
1131014506,
879679996,
2909243462,
3663771856,
1141124467,
855842277,
2852801631,
3708648649,
1342533948,
654459306,
3188396048,
3373015174,
1466479909,
544179635,
3110523913,
3462522015,
1591671054,
702138776,
2966460450,
3352799412,
1504918807,
783551873,
3082640443,
3233442989,
3988292384,
2596254646,
62317068,
1957810842,
3939845945,
2647816111,
81470997,
1943803523,
3814918930,
2489596804,
225274430,
2053790376,
3826175755,
2466906013,
167816743,
2097651377,
4027552580,
2265490386,
503444072,
1762050814,
4150417245,
2154129355,
426522225,
1852507879,
4275313526,
2312317920,
282753626,
1742555852,
4189708143,
2394877945,
397917763,
1622183637,
3604390888,
2714866558,
953729732,
1340076626,
3518719985,
2797360999,
1068828381,
1219638859,
3624741850,
2936675148,
906185462,
1090812512,
3747672003,
2825379669,
829329135,
1181335161,
3412177804,
3160834842,
628085408,
1382605366,
3423369109,
3138078467,
570562233,
1426400815,
3317316542,
2998733608,
733239954,
1555261956,
3268935591,
3050360625,
752459403,
1541320221,
2607071920,
3965973030,
1969922972,
40735498,
2617837225,
3943577151,
1913087877,
83908371,
2512341634,
3803740692,
2075208622,
213261112,
2463272603,
3855990285,
2094854071,
198958881,
2262029012,
4057260610,
1759359992,
534414190,
2176718541,
4139329115,
1873836001,
414664567,
2282248934,
4279200368,
1711684554,
285281116,
2405801727,
4167216745,
1634467795,
376229701,
2685067896,
3608007406,
1308918612,
956543938,
2808555105,
3495958263,
1231636301,
1047427035,
2932959818,
3654703836,
1088359270,
936918e3,
2847714899,
3736837829,
1202900863,
817233897,
3183342108,
3401237130,
1404277552,
615818150,
3134207493,
3453421203,
1423857449,
601450431,
3009837614,
3294710456,
1567103746,
711928724,
3020668471,
3272380065,
1510334235,
755167117
];
function crc32(buf, seed = 4294967295) {
let C = seed ^ -1;
const L = buf.length - 7;
let i = 0;
for (i = 0; i < L; ) {
C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255];
C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255];
C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255];
C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255];
C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255];
C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255];
C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255];
C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255];
}
while (i < L + 7) C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255];
C = C ^ -1;
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, C, true);
return new Uint8Array(buffer);
}
// src/nvs/nvs-settings.ts
var NVSSettings = class {
static BLOCK_SIZE = 32;
static PAGE_SIZE = 4096;
static PAGE_MAX_ENTRIES = 126;
static PAGE_ACTIVE = 4294967294;
static PAGE_FULL = 4294967292;
static NVS_VERSION = 254;
// version 2
static DEFAULT_NAMESPACE = "storage";
};
// src/nvs/nvs-entry.ts
var NVS_BLOCK_SIZE = NVSSettings.BLOCK_SIZE;
var NvsEntry = class {
namespaceIndex;
type;
key;
data;
chunkIndex;
headerNamespace;
headerType;
headerSpan;
headerChunkIndex;
headerCRC32;
headerKey;
headerData;
headerDataSize;
headerDataCRC32;
headerBuffer;
dataBuffer;
entriesNeeded = 0;
constructor(entry) {
this.namespaceIndex = entry.namespaceIndex;
this.type = entry.type;
this.data = entry.data;
this.chunkIndex = 255;
if (entry.key.length > 15) {
throw Error(
`NVS max key length is 15, received '${entry.key}' of length ${entry.key.length}`
);
}
this.key = entry.key + "\0";
this.headerBuffer = new Uint8Array(NVS_BLOCK_SIZE);
this.headerNamespace = new Uint8Array(this.headerBuffer.buffer, 0, 1);
this.headerType = new Uint8Array(this.headerBuffer.buffer, 1, 1);
this.headerSpan = new Uint8Array(this.headerBuffer.buffer, 2, 1);
this.headerChunkIndex = new Uint8Array(this.headerBuffer.buffer, 3, 1);
this.headerCRC32 = new Uint8Array(this.headerBuffer.buffer, 4, 4);
this.headerKey = new Uint8Array(this.headerBuffer.buffer, 8, 16);
this.headerData = new Uint8Array(this.headerBuffer.buffer, 24, 8);
this.headerDataSize = new Uint8Array(this.headerBuffer.buffer, 24, 4);
this.headerDataCRC32 = new Uint8Array(this.headerBuffer.buffer, 28, 4);
this.dataBuffer = new Uint8Array(0);
this.setEntryData();
this.setEntryHeader();
this.setEntryHeaderCRC();
}
setEntryHeader() {
const encoder = new TextEncoder();
this.headerNamespace.set([this.namespaceIndex]);
this.headerType.set([this.type]);
this.headerSpan.set([this.entriesNeeded]);
this.headerChunkIndex.set([this.chunkIndex]);
this.headerKey.set(encoder.encode(this.key));
}
setEntryData() {
if (this.type === 33 /* STR */) {
this.setStringEntry();
} else if (typeof this.data === "number") {
this.setPrimitiveEntry();
} else {
throw new Error("Unsupported data type for NVS entry.");
}
}
// In src/nvs/nvs-entry.ts
setStringEntry() {
if (typeof this.data === "string") {
this.headerData.fill(255);
const valueWithTerminator = this.data + "\0";
const encoder = new TextEncoder();
const data = encoder.encode(valueWithTerminator);
if (data.length > 4e3) {
throw new Error("String values are limited to 4000 bytes.");
}
this.entriesNeeded = 1 + Math.ceil(data.length / NVSSettings.BLOCK_SIZE);
this.dataBuffer = new Uint8Array(
(this.entriesNeeded - 1) * NVSSettings.BLOCK_SIZE
).fill(255);
this.dataBuffer.set(data);
const dataSizeBuffer = new ArrayBuffer(2);
const dataSizeView = new DataView(dataSizeBuffer);
dataSizeView.setUint16(0, data.length, true);
this.headerData.set(new Uint8Array(dataSizeBuffer), 0);
this.headerDataCRC32.set(crc32(data));
}
}
setPrimitiveEntry() {
if (typeof this.data === "number") {
this.entriesNeeded = 1;
this.headerData.fill(255);
const dataView = new DataView(
this.headerData.buffer,
this.headerData.byteOffset,
8
);
switch (this.type) {
case 1 /* U8 */:
dataView.setUint8(0, this.data);
break;
case 17 /* I8 */:
dataView.setInt8(0, this.data);
break;
case 2 /* U16 */:
dataView.setUint16(0, this.data, true);
break;
case 18 /* I16 */:
dataView.setInt16(0, this.data, true);
break;
case 4 /* U32 */:
dataView.setUint32(0, this.data, true);
break;
case 20 /* I32 */:
dataView.setInt32(0, this.data, true);
break;
case 8 /* U64 */:
dataView.setBigUint64(0, BigInt(this.data), true);
break;
case 24 /* I64 */:
dataView.setBigInt64(0, BigInt(this.data), true);
break;
default:
throw new Error(`Unsupported primitive type: ${this.type}`);
}
}
}
setEntryHeaderCRC() {
const crcData = new Uint8Array(28);
crcData.set(this.headerBuffer.slice(0, 4), 0);
crcData.set(this.headerBuffer.slice(8, 32), 4);
this.headerCRC32.set(crc32(crcData));
}
};
// src/nvs/state-bitmap.ts
var EntryStateBitmap = {
/**
* Updates the state for a given entry in the bitmap.
* @param currentBitmap The current state bitmap as a BigInt.
* @param entryIndex The index of the entry to update (0-125).
* @param newState The new state for the entry.
* @returns The updated bitmap as a BigInt.
*/
setState(currentBitmap, entryIndex, newState) {
if (entryIndex < 0 || entryIndex >= 126) {
throw new Error("Entry index is out of bounds.");
}
const bitPosition = BigInt(entryIndex * 2);
const clearMask = ~(0b11n << bitPosition);
const clearedBitmap = currentBitmap & clearMask;
const newValue = BigInt(newState) << bitPosition;
return clearedBitmap | newValue;
}
};
// src/nvs/nvs-page.ts
var NVSPage = class {
constructor(pageNumber, version) {
this.pageNumber = pageNumber;
this.version = version;
this.pageBuffer = new Uint8Array(NVSSettings.PAGE_SIZE).fill(255);
this.pageHeader = new Uint8Array(this.pageBuffer.buffer, 0, 32);
this.headerPageState = new Uint8Array(this.pageHeader.buffer, 0, 4);
this.headerPageNumber = new Uint8Array(this.pageHeader.buffer, 4, 4);
this.headerVersion = new Uint8Array(this.pageHeader.buffer, 8, 1);
this.headerCRC32 = new Uint8Array(this.pageHeader.buffer, 28, 4);
this.setPageHeader();
}
entryNumber = 0;
pageBuffer;
pageHeader;
stateBitmap = BigInt(
"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
);
entries = [];
itemHashMap = /* @__PURE__ */ new Map();
headerPageState;
headerPageNumber;
headerVersion;
headerCRC32;
isStateLocked = false;
setPageHeader() {
this.setPageState("ACTIVE");
const pageNumView = new DataView(
this.headerPageNumber.buffer,
this.headerPageNumber.byteOffset,
4
);
pageNumView.setUint32(0, this.pageNumber, true);
this.headerVersion.set([this.version]);
this.updateHeaderCrc();
}
updateHeaderCrc() {
const crcData = this.pageHeader.slice(4, 28);
this.headerCRC32.set(crc32(crcData));
}
_calculateItemHash(namespaceIndex, key, chunkIndex) {
const hashData = `${namespaceIndex}:${key}:${chunkIndex}`;
const fullCrc = crc32(new TextEncoder().encode(hashData));
return new DataView(fullCrc.buffer).getUint32(0, true) & 16777215;
}
getNVSEncoding(value) {
if (typeof value === "string") {
return 33 /* STR */;
}
const isNegative = value < 0;
const absValue = Math.abs(value);
if (isNegative) {
if (absValue <= 128) return 17 /* I8 */;
if (absValue <= 32768) return 18 /* I16 */;
if (absValue <= 2147483648) return 20 /* I32 */;
return 24 /* I64 */;
} else {
if (absValue <= 255) return 1 /* U8 */;
if (absValue <= 65535) return 2 /* U16 */;
if (absValue <= 4294967295) return 4 /* U32 */;
return 8 /* U64 */;
}
}
writeEntry(key, data, namespaceIndex) {
if (this.isStateLocked) {
throw new Error("Page is full and locked. Cannot write new entries.");
}
const entryKv = {
namespaceIndex,
key,
data,
type: this.getNVSEncoding(data)
};
const entry = new NvsEntry(entryKv);
if (entry.entriesNeeded + this.entryNumber > NVSSettings.PAGE_MAX_ENTRIES) {
this.setPageState("FULL");
throw new Error("Entry doesn't fit on the page");
}
const hash = this._calculateItemHash(namespaceIndex, key, entry.chunkIndex);
this.itemHashMap.set(hash, this.entryNumber);
this.entries.push(entry);
for (let i = 0; i < entry.entriesNeeded; i++) {
this.stateBitmap = EntryStateBitmap.setState(
this.stateBitmap,
this.entryNumber + i,
2 /* Written */
);
}
this.entryNumber += entry.entriesNeeded;
return entry;
}
findEntry(key, namespaceIndex, chunkIndex = 255) {
const hash = this._calculateItemHash(namespaceIndex, key, chunkIndex);
const potentialIndex = this.itemHashMap.get(hash);
if (potentialIndex !== void 0) {
const entry = this.entries[potentialIndex];
if (entry && entry.key === key && entry.namespaceIndex === namespaceIndex && entry.chunkIndex === chunkIndex) {
return entry;
}
}
return this.entries.find(
(e) => e.key === key && e.namespaceIndex === namespaceIndex && e.chunkIndex === chunkIndex
);
}
setPageState(state) {
if (state === "FULL") {
this.headerPageState.set(
new Uint8Array(new Uint32Array([NVSSettings.PAGE_FULL]).buffer)
);
this.isStateLocked = true;
} else if (state === "ACTIVE") {
this.headerPageState.set(
new Uint8Array(new Uint32Array([NVSSettings.PAGE_ACTIVE]).buffer)
);
} else {
throw Error("Invalid page state requested");
}
this.updateHeaderCrc();
}
getData() {
const sbm = new Uint8Array(NVSSettings.BLOCK_SIZE).fill(255);
new DataView(sbm.buffer).setBigUint64(0, this.stateBitmap, true);
this.pageBuffer.set(this.pageHeader, 0);
this.pageBuffer.set(sbm, NVSSettings.BLOCK_SIZE);
let currentEntrySlot = 0;
for (const entry of this.entries) {
const headerOffset = (2 + currentEntrySlot) * NVSSettings.BLOCK_SIZE;
this.pageBuffer.set(entry.headerBuffer, headerOffset);
if (entry.dataBuffer.length > 0) {
const dataOffset = (2 + currentEntrySlot + 1) * NVSSettings.BLOCK_SIZE;
this.pageBuffer.set(entry.dataBuffer, dataOffset);
}
currentEntrySlot += entry.entriesNeeded;
}
return this.pageBuffer;
}
};
// src/nvs/nvs-partition.ts
var NVSPartition = class {
constructor(offset, filename, size = 12288) {
this.offset = offset;
this.filename = filename;
this.size = size;
this.namespaces.push("RESERVED_NS_0");
this.newPage();
}
namespaces = [];
pages = [];
newPage() {
const lastPage = this.getLastPage();
if (lastPage) {
lastPage.setPageState("FULL");
}
const index = this.pages.length;
const nvsPage = new NVSPage(index, NVSSettings.NVS_VERSION);
this.pages.push(nvsPage);
return nvsPage;
}
getLastPage() {
if (this.pages.length === 0) {
return null;
}
return this.pages[this.pages.length - 1];
}
getNameSpaceIndex(namespace) {
const existingIndex = this.namespaces.indexOf(namespace);
if (existingIndex !== -1) {
return existingIndex;
}
if (this.namespaces.length >= 254) {
throw new Error("Maximum number of namespaces (254) reached.");
}
const newIndex = this.namespaces.length;
this.namespaces.push(namespace);
try {
this.write(namespace, newIndex, 0);
} catch (e) {
console.log("Page is full, creating new", e);
this.newPage();
this.write(namespace, newIndex, 0);
}
return newIndex;
}
get binary() {
const buffer = new Uint8Array(this.size).fill(255);
let offset = 0;
for (const page of this.pages) {
const pageBuffer = page.getData();
buffer.set(pageBuffer, offset);
offset += pageBuffer.length;
}
return buffer;
}
writeEntry(namespace, key, data) {
const namespaceIndex = this.getNameSpaceIndex(namespace);
this.write(key, data, namespaceIndex);
}
// Private write helper to avoid recursive loop in namespace creation
write(key, data, namespaceIndex) {
try {
const page = this.getLastPage();