esp-controller
Version:
Typescript package for connecting and flashing images to your ESP device.
1 lines • 106 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/utils/common.ts","../src/esp/stream-transformers.ts","../src/esp/command.ts","../src/esp/command.sync.ts","../src/esp/command.spi-attach.ts","../src/esp/command.spi-set-params.ts","../src/esp/command.flash-data.ts","../src/esp/command.flash-begin.ts","../src/esp/serial-controller.ts","../src/image/bin-file-partition.ts","../src/image/image.ts","../src/utils/crc32.ts","../src/nvs/nvs-settings.ts","../src/nvs/nvs-entry.ts","../src/nvs/state-bitmap.ts","../src/nvs/nvs-page.ts","../src/nvs/nvs-partition.ts","../src/partition/partition-entry.ts","../src/partition/partition-types.ts","../src/partition/partition-table.ts"],"sourcesContent":["/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// --- Core Controller ---\nexport { SerialController } from \"./esp/serial-controller\";\nexport type { SerialConnection } from \"./esp/serial-controller\";\n\n// --- Image Creation ---\nexport { ESPImage } from \"./image/image\";\n\n// --- Partition Implementations ---\nexport { BinFilePartition } from \"./image/bin-file-partition\";\nexport { NVSPartition } from \"./nvs/nvs-partition\";\nexport { PartitionTable } from \"./partition/partition-table\";\n\n// --- Partition Interfaces and Types ---\nexport type { Partition } from \"./partition/partition\";\nexport type {\n PartitionDefinition,\n PartitionFlags,\n} from \"./partition/partition-types\";\nexport {\n PartitionType,\n AppPartitionSubType,\n DataPartitionSubType,\n} from \"./partition/partition-types\";\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Returns a promise with a timeout for set milliseconds.\n * @param ms milliseconds to wait\n * @returns Promise that resolves after set ms.\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// Helper function to format a byte array into a hex string\nexport function toHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/* SLIP special character codes */\nexport enum SlipStreamBytes {\n END = 0xc0, // Indicates end of packet\n ESC = 0xdb, // Indicates byte stuffing\n ESC_END = 0xdc, // ESC ESC_END means END data byte\n ESC_ESC = 0xdd, // ESC ESC_ESC means ESC data byte\n}\n\n/**\n * Encode buffer using RFC 1055 (SLIP) standard\n * @param buffer\n * @returns Uint8Array buffer encoded.\n */\nexport function slipEncode(buffer: Uint8Array): Uint8Array {\n const encoded = [SlipStreamBytes.END];\n for (const byte of buffer) {\n if (byte === SlipStreamBytes.END) {\n encoded.push(SlipStreamBytes.ESC, SlipStreamBytes.ESC_END);\n } else if (byte === SlipStreamBytes.ESC) {\n encoded.push(SlipStreamBytes.ESC, SlipStreamBytes.ESC_ESC);\n } else {\n encoded.push(byte);\n }\n }\n encoded.push(SlipStreamBytes.END);\n return new Uint8Array(encoded);\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { SlipStreamBytes } from \"../utils/common\";\n\n/**\n * A generic string logging transformer.\n * It logs each chunk to the console and passes it through unmodified.\n */\nclass LoggingTransformer implements Transformer<string, string> {\n /**\n * Constructs a new LoggingTransformer.\n * @param logPrefix A prefix string to prepend to each log message.\n */\n constructor(public logPrefix: string = \"STREAM LOG: \") {}\n /**\n * Logs the incoming chunk to the console with the configured prefix\n * and then enqueues it to be passed to the next stage in the stream.\n * @param chunk The string chunk to process.\n * @param controller The TransformStreamDefaultController to enqueue the chunk.\n */\n transform(\n chunk: string,\n controller: TransformStreamDefaultController<string>,\n ) {\n console.log(this.logPrefix, chunk);\n controller.enqueue(chunk);\n }\n}\n\n/**\n * A generic Uint8Array logging transformer.\n * It logs each chunk to the console and passes it through unmodified.\n */\nclass Uint8LoggingTransformer implements Transformer<Uint8Array, Uint8Array> {\n /**\n * Constructs a new Uint8LoggingTransformer.\n * @param logPrefix A prefix string to prepend to each log message.\n */\n constructor(public logPrefix: string = \"UINT8 STREAM LOG: \") {}\n /**\n * Logs the incoming chunk to the console with the configured prefix\n * and then enqueues it to be passed to the next stage in the stream.\n * @param chunk The Uint8Array chunk to process.\n * @param controller The TransformStreamDefaultController to enqueue the chunk.\n */\n transform(\n chunk: Uint8Array,\n controller: TransformStreamDefaultController<Uint8Array>,\n ) {\n console.log(this.logPrefix, chunk);\n controller.enqueue(chunk);\n }\n}\n\n/**\n * A transformer that buffers incoming string chunks and splits them into lines based on `\\r\\n` separators.\n * It ensures that only complete lines are enqueued. Any partial line at the end of a chunk is buffered\n * and prepended to the next chunk.\n */\nclass LineBreakTransformer implements Transformer<string, string> {\n buffer: string | undefined = \"\";\n transform(\n chunk: string,\n controller: TransformStreamDefaultController<string>,\n ) {\n this.buffer += chunk;\n const lines = this.buffer?.split(\"\\r\\n\");\n this.buffer = lines?.pop();\n lines?.forEach((line) => controller.enqueue(line));\n }\n}\n\n/**\n * Implements the SLIP (Serial Line Internet Protocol) encoding and decoding logic.\n * This transformer can be configured to operate in either encoding or decoding mode.\n */\nexport class SlipStreamTransformer\n implements Transformer<Uint8Array, Uint8Array>\n{\n private decoding = false; // Flag to indicate if the initial END byte for a packet has been received in decoding mode.\n private escape = false; // Flag to indicate if the current byte is an escape character in decoding mode.\n private frame: number[] = []; // Buffer to accumulate bytes for the current frame.\n\n /**\n * Constructs a new SlipStreamTransformer.\n * @param mode Specifies whether the transformer should operate in \"encoding\" or \"decoding\" mode.\n */\n constructor(private mode: \"encoding\" | \"decoding\") {\n if (this.mode === \"encoding\") {\n this.decoding = false; // In encoding mode, 'decoding' state is not used.\n }\n }\n\n transform(\n chunk: Uint8Array,\n controller: TransformStreamDefaultController<Uint8Array>,\n ) {\n if (this.mode === \"decoding\") {\n for (const byte of chunk) {\n // State machine for decoding SLIP frames\n if (this.decoding) {\n // Currently inside a frame\n if (this.escape) {\n // Previous byte was ESC\n if (byte === SlipStreamBytes.ESC_END) {\n this.frame.push(SlipStreamBytes.END);\n } else if (byte === SlipStreamBytes.ESC_ESC) {\n this.frame.push(SlipStreamBytes.ESC);\n } else {\n // This case should ideally not happen in a valid SLIP stream,\n // but we'll add the byte as is to be robust.\n this.frame.push(byte);\n }\n this.escape = false;\n } else if (byte === SlipStreamBytes.ESC) {\n // Start of an escape sequence\n this.escape = true;\n } else if (byte === SlipStreamBytes.END) {\n // End of the current frame\n if (this.frame.length > 0) {\n controller.enqueue(new Uint8Array(this.frame));\n }\n this.frame = []; // Reset frame buffer for the next packet\n // this.decoding remains true as we might receive multiple packets\n } else {\n // Regular data byte\n this.frame.push(byte);\n }\n } else if (byte === SlipStreamBytes.END) {\n // Start of a new frame (or end of a previous one, signaling start of a new one)\n this.decoding = true;\n this.frame = []; // Clear any previous partial frame data\n this.escape = false;\n }\n // Bytes received before the first END in decoding mode are ignored.\n }\n } else {\n // Encoding mode: Escapes special SLIP bytes (END, ESC) in the input chunk\n // and accumulates them in an internal buffer. The actual packet framing\n // with END bytes is handled in the flush method.\n for (const byte of chunk) {\n if (byte === SlipStreamBytes.END) {\n this.frame.push(SlipStreamBytes.ESC, SlipStreamBytes.ESC_END);\n } else if (byte === SlipStreamBytes.ESC) {\n this.frame.push(SlipStreamBytes.ESC, SlipStreamBytes.ESC_ESC);\n } else {\n this.frame.push(byte);\n }\n }\n }\n }\n\n flush(controller: TransformStreamDefaultController<Uint8Array>) {\n if (this.mode === \"encoding\") {\n // For encoding mode, wraps the accumulated frame buffer with SLIP END bytes\n // to form a complete packet, then enqueues it.\n // Only enqueue if there's data to send to avoid empty packets.\n if (this.frame.length > 0) {\n const finalPacket = new Uint8Array([\n SlipStreamBytes.END,\n ...this.frame,\n SlipStreamBytes.END,\n ]);\n controller.enqueue(finalPacket);\n this.frame = []; // Clear the frame buffer after flushing\n }\n }\n // The decoder does not need any specific flush logic, as partial frames\n // are handled by the transform method. Any remaining data in `this.frame`\n // when the stream closes is considered an incomplete packet and is discarded.\n }\n}\n\n/**\n * Creates a TransformStream that logs string chunks to the console.\n * @returns A new TransformStream instance with a LoggingTransformer.\n */\nexport function createLoggingTransformer() {\n return new TransformStream<string, string>(new LoggingTransformer());\n}\n\n/**\n * Creates a TransformStream that logs Uint8Array chunks to the console.\n * @returns A new TransformStream instance with a Uint8LoggingTransformer.\n */\nexport function createUint8LoggingTransformer() {\n return new TransformStream<Uint8Array, Uint8Array>(\n new Uint8LoggingTransformer(),\n );\n}\n\n/**\n * Creates a TransformStream that returns full lines only.\n * Chunks are saved to buffer until `\\r\\n` is send.\n * @returns TransformStream\n */\nexport function createLineBreakTransformer() {\n return new TransformStream<string, string>(new LineBreakTransformer());\n}\n\n/**\n * TranformStream that encodes data according to the RFC 1055 (SLIP) standard.\n * It takes a stream of Uint8Array chunks and outputs a stream of Uint8Array chunks\n * where each output chunk represents a SLIP-encoded packet.\n * @example\n * const rawDataStream = getSomeUint8ArrayStream();\n * const slipEncodedStream = rawDataStream.pipeThrough(new SlipStreamEncoder());\n */\nexport class SlipStreamEncoder extends TransformStream<Uint8Array, Uint8Array> {\n /**\n * Constructs a new SlipStreamEncoder.\n * This sets up the underlying SlipStreamTransformer in \"encoding\" mode.\n */\n constructor() {\n super(new SlipStreamTransformer(\"encoding\"));\n }\n}\n\n/**\n * TranformStream that decodes data according to the RFC 1055 (SLIP) standard.\n * It takes a stream of Uint8Array chunks (potentially partial SLIP packets) and\n * outputs a stream of Uint8Array chunks where each output chunk represents a\n * decoded SLIP frame (the original data without SLIP framing and escaping).\n * @example\n * const slipEncodedStream = getSomeSlipEncodedStream();\n * const decodedDataStream = slipEncodedStream.pipeThrough(new SlipStreamDecoder());\n */\nexport class SlipStreamDecoder extends TransformStream<Uint8Array, Uint8Array> {\n /**\n * Constructs a new SlipStreamDecoder.\n * This sets up the underlying SlipStreamTransformer in \"decoding\" mode.\n */\n constructor() {\n super(new SlipStreamTransformer(\"decoding\"));\n }\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { slipEncode } from \"../utils/common\";\n\nexport enum EspPacketDirection {\n REQUEST = 0x00,\n RESPONSE = 0x01,\n}\n\nexport enum EspCommand {\n FLASH_BEGIN = 0x02,\n FLASH_DATA = 0x03,\n FLASH_END = 0x04,\n MEM_BEGIN = 0x05,\n MEM_END = 0x06,\n MEM_DATA = 0x07,\n SYNC = 0x08,\n WRITE_REG = 0x09,\n READ_REG = 0x0a,\n SPI_SET_PARAMS = 0x0b,\n SPI_ATTACH = 0x0d,\n CHANGE_BAUDRATE = 0x0f,\n FLASH_DEFL_BEGIN = 0x10,\n FLASH_DEFL_DATA = 0x11,\n FLASH_DEFL_END = 0x12,\n SPI_FLASH_MD5 = 0x13,\n}\n\nexport class EspCommandPacket {\n private packetHeader: Uint8Array = new Uint8Array(8);\n private packetData: Uint8Array = new Uint8Array(0);\n\n set direction(direction: EspPacketDirection) {\n new DataView(this.packetHeader.buffer, 0, 1).setUint8(0, direction);\n }\n\n get direction(): EspPacketDirection {\n return new DataView(this.packetHeader.buffer, 0, 1).getUint8(0);\n }\n\n set command(command: EspCommand) {\n new DataView(this.packetHeader.buffer, 1, 1).setUint8(0, command);\n }\n\n get command(): EspCommand {\n return new DataView(this.packetHeader.buffer, 1, 1).getUint8(0);\n }\n\n set size(size: number) {\n new DataView(this.packetHeader.buffer, 2, 2).setUint16(0, size, true);\n }\n\n get size(): number {\n return new DataView(this.packetHeader.buffer, 2, 2).getUint16(0, true);\n }\n\n set checksum(checksum: number) {\n new DataView(this.packetHeader.buffer, 4, 4).setUint32(0, checksum, true);\n }\n\n get checksum(): number {\n return new DataView(this.packetHeader.buffer, 4, 4).getUint32(0, true);\n }\n\n set value(value: number) {\n new DataView(this.packetHeader.buffer, 4, 4).setUint32(0, value, true);\n }\n\n get value(): number {\n return new DataView(this.packetHeader.buffer, 4, 4).getUint32(0, true);\n }\n\n get status(): number {\n return new DataView(this.packetData.buffer, 0, 1).getUint8(0);\n }\n\n get error(): number {\n return new DataView(this.packetData.buffer, 1, 1).getUint8(0);\n }\n\n generateChecksum(data: Uint8Array): number {\n let cs = 0xef;\n for (const byte of data) {\n cs ^= byte;\n }\n return cs;\n }\n\n set data(packetData: Uint8Array) {\n this.size = packetData.length;\n this.packetData = packetData;\n }\n\n get data(): Uint8Array {\n return this.packetData;\n }\n\n parseResponse(responsePacket: Uint8Array) {\n const responseDataView = new DataView(responsePacket.buffer);\n this.direction = responseDataView.getUint8(0) as EspPacketDirection;\n this.command = responseDataView.getUint8(1) as EspCommand;\n this.size = responseDataView.getUint16(2, true);\n this.value = responseDataView.getUint32(4, true);\n this.packetData = responsePacket.slice(8);\n\n if (this.status === 1) {\n console.log(this.getErrorMessage(this.error));\n }\n }\n\n getErrorMessage(error: number): string {\n switch (error) {\n case 0x05:\n return \"Status Error: Received message is invalid. (parameters or length field is invalid)\";\n case 0x06:\n return \"Failed to act on received message\";\n case 0x07:\n return \"Invalid CRC in message\";\n case 0x08:\n 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.\";\n case 0x09:\n return \"flash read error - SPI read failed\";\n case 0x0a:\n return \"flash read length error - SPI read request length is too long\";\n case 0x0b:\n return \"Deflate error (compressed uploads only)\";\n default:\n return \"No error status for response\";\n }\n }\n\n getPacketData(): Uint8Array {\n const header = new Uint8Array(8);\n const view = new DataView(header.buffer);\n view.setUint8(0, this.direction);\n view.setUint8(1, this.command);\n view.setUint16(2, this.data.length, true);\n view.setUint32(4, this.checksum, true);\n return new Uint8Array([...header, ...this.data]);\n }\n\n getSlipStreamEncodedPacketData(): Uint8Array {\n return slipEncode(this.getPacketData());\n }\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { EspCommand, EspCommandPacket, EspPacketDirection } from \"./command\";\n\nexport class EspCommandSync extends EspCommandPacket {\n constructor() {\n super();\n\n this.direction = EspPacketDirection.REQUEST;\n this.command = EspCommand.SYNC;\n this.data = new Uint8Array([\n 0x07, 0x07, 0x12, 0x20, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,\n 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,\n 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,\n ]);\n this.checksum = 0;\n }\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { EspCommand, EspCommandPacket, EspPacketDirection } from \"./command\";\n\nexport class EspCommandSpiAttach extends EspCommandPacket {\n private spiAttachData = new ArrayBuffer(8);\n private view1 = new DataView(this.spiAttachData, 0, 4);\n private view2 = new DataView(this.spiAttachData, 4, 4);\n\n constructor() {\n super();\n this.direction = EspPacketDirection.REQUEST;\n this.command = EspCommand.SPI_ATTACH;\n\n this.view1.setUint32(0, 0, true);\n this.view2.setUint32(0, 0, true);\n this.data = new Uint8Array(this.spiAttachData);\n }\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { EspCommand, EspCommandPacket, EspPacketDirection } from \"./command\";\n\nexport class EspCommandSpiSetParams extends EspCommandPacket {\n private paramsData = new ArrayBuffer(24);\n private id = new DataView(this.paramsData, 0, 4);\n private totalSize = new DataView(this.paramsData, 4, 4);\n private blockSize = new DataView(this.paramsData, 8, 4);\n private sectorSize = new DataView(this.paramsData, 12, 4);\n private pageSize = new DataView(this.paramsData, 16, 4);\n private statusMask = new DataView(this.paramsData, 20, 4);\n\n constructor() {\n super();\n this.direction = EspPacketDirection.REQUEST;\n this.command = EspCommand.SPI_SET_PARAMS;\n this.id.setUint32(0, 0, true);\n this.totalSize.setUint32(0, 4 * 1024 * 1024, true);\n this.blockSize.setUint32(0, 0x10000, true);\n this.sectorSize.setUint32(0, 0x1000, true);\n this.pageSize.setUint32(0, 0x100, true);\n this.statusMask.setUint32(0, 0xffffffff, true);\n this.data = new Uint8Array(this.paramsData);\n }\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { EspCommand, EspCommandPacket, EspPacketDirection } from \"./command\";\n\nexport class EspCommandFlashData extends EspCommandPacket {\n constructor(image: Uint8Array, sequenceNumber: number, blockSize: number) {\n super();\n\n this.direction = EspPacketDirection.REQUEST;\n this.command = EspCommand.FLASH_DATA;\n\n const flashDownloadData = new Uint8Array(16 + blockSize);\n const blockSizeView = new DataView(flashDownloadData.buffer, 0, 4);\n const sequenceView = new DataView(flashDownloadData.buffer, 4, 4);\n const paddingView = new DataView(flashDownloadData.buffer, 8, 8);\n\n blockSizeView.setUint32(0, blockSize, true);\n sequenceView.setUint32(0, sequenceNumber, true);\n\n paddingView.setUint32(0, 0, true);\n paddingView.setUint32(4, 0, true);\n\n const block = image.slice(\n sequenceNumber * blockSize,\n sequenceNumber * blockSize + blockSize,\n );\n\n const blockData = new Uint8Array(blockSize);\n blockData.fill(0xff);\n blockData.set(block, 0);\n\n flashDownloadData.set(blockData, 16);\n this.data = flashDownloadData;\n this.checksum = this.generateChecksum(blockData);\n }\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { EspCommand, EspCommandPacket, EspPacketDirection } from \"./command\";\n\nexport class EspCommandFlashBegin extends EspCommandPacket {\n private flashBeginData = new ArrayBuffer(16);\n private eraseSizeView = new DataView(this.flashBeginData, 0, 4);\n private numDataPacketsView = new DataView(this.flashBeginData, 4, 4);\n private dataSizeView = new DataView(this.flashBeginData, 8, 4);\n private offsetView = new DataView(this.flashBeginData, 12, 4);\n\n constructor(\n image: Uint8Array,\n offset: number,\n packetSize: number,\n numPackets: number,\n ) {\n super();\n\n this.direction = EspPacketDirection.REQUEST;\n this.command = EspCommand.FLASH_BEGIN;\n this.eraseSizeView.setUint32(0, image.length, true);\n this.numDataPacketsView.setUint32(0, numPackets, true);\n this.dataSizeView.setUint32(0, packetSize, true);\n this.offsetView.setUint32(0, offset, true);\n this.data = new Uint8Array(this.flashBeginData);\n }\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n createLineBreakTransformer,\n SlipStreamDecoder,\n} from \"./stream-transformers\";\nimport { sleep, toHex } from \"../utils/common\";\nimport { EspCommand, EspCommandPacket, EspPacketDirection } from \"./command\";\nimport { EspCommandSync } from \"./command.sync\";\nimport { EspCommandSpiAttach } from \"./command.spi-attach\";\nimport { EspCommandSpiSetParams } from \"./command.spi-set-params\";\nimport { ESPImage } from \"../image/image\";\nimport { EspCommandFlashData } from \"./command.flash-data\";\nimport { EspCommandFlashBegin } from \"./command.flash-begin\";\nimport { Partition } from \"../partition/partition\";\n\n/**\n * Default serial options when connecting to an ESP32.\n */\nconst DEFAULT_ESP32_SERIAL_OPTIONS: SerialOptions = {\n baudRate: 115200,\n dataBits: 8,\n stopBits: 1,\n bufferSize: 255,\n parity: \"none\",\n flowControl: \"none\",\n};\n\n/**\n * Interface defining the properties of a serial connection.\n */\nexport interface SerialConnection {\n /** The underlying SerialPort object. Undefined if no port is selected. */\n port: SerialPort | undefined;\n /** Indicates if the serial port is currently open and connected. */\n connected: boolean;\n /** Indicates if the connection has been synchronized with the device. */\n synced: boolean;\n /** The readable stream for receiving data from the serial port. Null if not connected. */\n readable: ReadableStream<Uint8Array> | null;\n /** The writable stream for sending data to the serial port. Null if not connected. */\n writable: WritableStream<Uint8Array> | null;\n /** An AbortController to signal termination of stream operations. Undefined if not connected. */\n abortStreamController: AbortController | undefined;\n /** A readable stream that contains the slipstream decoded responses from the esp. */\n commandResponseStream: ReadableStream<Uint8Array> | undefined;\n}\n\nexport class SerialController extends EventTarget {\n public connection: SerialConnection;\n\n constructor() {\n super();\n this.connection = this.createSerialConnection();\n }\n\n private createSerialConnection(): SerialConnection {\n return {\n port: undefined,\n connected: false,\n synced: false,\n readable: null,\n writable: null,\n abortStreamController: undefined,\n commandResponseStream: undefined,\n };\n }\n\n public async requestPort(): Promise<void> {\n this.connection.port = await navigator.serial.requestPort();\n this.connection.synced = false;\n }\n\n public createLogStreamReader(): () => AsyncGenerator<\n string | undefined,\n void,\n unknown\n > {\n if (\n !this.connection.connected ||\n !this.connection.readable ||\n !this.connection.abortStreamController\n )\n return async function* logStream() {};\n\n const streamPipeOptions = {\n signal: this.connection.abortStreamController.signal,\n preventCancel: false,\n preventClose: false,\n preventAbort: false,\n };\n\n const [newReadable, logReadable] = this.connection.readable.tee();\n this.connection.readable = newReadable;\n\n const reader = logReadable\n .pipeThrough(new TextDecoderStream(), streamPipeOptions)\n .pipeThrough(createLineBreakTransformer(), streamPipeOptions)\n .getReader();\n\n const connection = this.connection;\n return async function* logStream() {\n try {\n while (connection.connected) {\n const result = await reader?.read();\n if (result?.done) return;\n yield result?.value;\n }\n } finally {\n reader?.releaseLock();\n }\n };\n }\n\n public async openPort(\n options: SerialOptions = DEFAULT_ESP32_SERIAL_OPTIONS,\n ): Promise<void> {\n if (!this.connection.port) return;\n await this.connection?.port.open(options);\n\n if (!this.connection.port?.readable) return;\n\n this.connection.abortStreamController = new AbortController();\n const [commandTee, logTee] = this.connection.port.readable.tee();\n\n this.connection.connected = true;\n this.connection.readable = logTee;\n this.connection.writable = this.connection.port.writable;\n this.connection.commandResponseStream = commandTee.pipeThrough(\n new SlipStreamDecoder(),\n { signal: this.connection.abortStreamController.signal },\n );\n }\n\n public async disconnect(): Promise<void> {\n if (!this.connection.connected || !this.connection.port) {\n return;\n }\n\n // Abort any ongoing stream operations\n this.connection.abortStreamController?.abort();\n\n try {\n await this.connection.port.close();\n } catch (error) {\n // The port might already be closed or disconnected by the device.\n console.error(\"Failed to close the serial port:\", error);\n }\n\n // Reset the connection state, but keep the port reference\n const port = this.connection.port;\n this.connection = this.createSerialConnection();\n this.connection.port = port;\n }\n\n public async sendResetPulse(): Promise<void> {\n if (!this.connection.port) return;\n this.connection.port.setSignals({\n dataTerminalReady: false,\n requestToSend: true,\n });\n await sleep(100);\n this.connection.port.setSignals({\n dataTerminalReady: true,\n requestToSend: false,\n });\n await sleep(100);\n }\n\n public async writeToConnection(data: Uint8Array) {\n if (this.connection.writable) {\n const writer = this.connection.writable.getWriter();\n await writer.write(data);\n writer.releaseLock();\n }\n }\n\n public async sync(): Promise<boolean> {\n await this.sendResetPulse();\n const maxAttempts = 10;\n const timeoutPerAttempt = 500; // ms\n\n const syncCommand = new EspCommandSync();\n\n for (let i = 0; i < maxAttempts; i++) {\n this.dispatchEvent(\n new CustomEvent(\"sync-progress\", {\n detail: { progress: (i / maxAttempts) * 100 },\n }),\n );\n console.log(`Sync attempt ${i + 1} of ${maxAttempts}`);\n await this.writeToConnection(\n syncCommand.getSlipStreamEncodedPacketData(),\n );\n\n let responseReader: ReadableStreamDefaultReader<Uint8Array> | undefined;\n\n try {\n if (!this.connection.commandResponseStream) {\n throw new Error(`No command response stream available.`);\n }\n responseReader = this.connection.commandResponseStream.getReader();\n\n const timeoutPromise = sleep(timeoutPerAttempt).then(() => {\n throw new Error(`Timeout after ${timeoutPerAttempt}ms`);\n });\n\n while (true) {\n const result = await Promise.race([\n responseReader.read(),\n timeoutPromise,\n ]);\n\n const { value, done } = result;\n\n if (done) {\n break;\n }\n\n if (value) {\n const responsePacket = new EspCommandPacket();\n responsePacket.parseResponse(value);\n\n if (responsePacket.command === EspCommand.SYNC) {\n console.log(\"SYNCED successfully.\", responsePacket);\n this.connection.synced = true;\n this.dispatchEvent(\n new CustomEvent(\"sync-progress\", {\n detail: { progress: 100 },\n }),\n );\n return true;\n }\n }\n }\n } catch (e) {\n console.log(`Sync attempt ${i + 1} timed out.`, e);\n } finally {\n if (responseReader) {\n responseReader.releaseLock();\n }\n }\n\n await sleep(100);\n }\n\n console.log(\"Failed to sync with the device.\");\n this.connection.synced = false;\n return false;\n }\n\n private async readResponse(\n expectedCommand: EspCommand,\n timeout = 2000,\n ): Promise<EspCommandPacket> {\n let responseReader: ReadableStreamDefaultReader<Uint8Array> | undefined;\n try {\n if (!this.connection.commandResponseStream) {\n throw new Error(`No command response stream available.`);\n }\n responseReader = this.connection.commandResponseStream.getReader();\n const timeoutPromise = sleep(timeout).then(() => {\n throw new Error(\n `Timeout: No response received for command ${EspCommand[expectedCommand]} within ${timeout}ms.`,\n );\n });\n\n while (true) {\n const { value, done } = await Promise.race([\n responseReader.read(),\n timeoutPromise,\n ]);\n\n if (done) {\n throw new Error(\n \"Stream closed unexpectedly while awaiting response.\",\n );\n }\n\n if (value) {\n const responsePacket = new EspCommandPacket();\n responsePacket.parseResponse(value);\n\n if (\n responsePacket.direction === EspPacketDirection.RESPONSE &&\n responsePacket.command === expectedCommand\n ) {\n if (responsePacket.error > 0) {\n throw new Error(\n `Device returned error for ${\n EspCommand[expectedCommand]\n }: ${responsePacket.getErrorMessage(responsePacket.error)}`,\n );\n }\n return responsePacket;\n }\n }\n }\n } finally {\n if (responseReader) {\n responseReader.releaseLock();\n }\n }\n }\n\n public async flashPartition(partition: Partition) {\n console.log(\n `Flashing partition: ${partition.filename}, offset: ${toHex(\n new Uint8Array(new Uint32Array([partition.offset]).buffer),\n )}`,\n );\n const packetSize = 4096;\n const numPackets = Math.ceil(partition.binary.length / packetSize);\n\n const flashBeginCmd = new EspCommandFlashBegin(\n partition.binary,\n partition.offset,\n packetSize,\n numPackets,\n );\n await this.writeToConnection(\n flashBeginCmd.getSlipStreamEncodedPacketData(),\n );\n await this.readResponse(EspCommand.FLASH_BEGIN);\n console.log(\"FLASH_BEGIN successful.\");\n\n for (let i = 0; i < numPackets; i++) {\n const flashDataCmd = new EspCommandFlashData(\n partition.binary,\n i,\n packetSize,\n );\n await this.writeToConnection(\n flashDataCmd.getSlipStreamEncodedPacketData(),\n );\n\n this.dispatchEvent(\n new CustomEvent(\"flash-progress\", {\n detail: {\n progress: ((i + 1) / numPackets) * 100,\n partition: partition,\n },\n }),\n );\n\n console.log(\n `[${partition.filename}] Writing block ${i + 1}/${numPackets}`,\n );\n await this.readResponse(EspCommand.FLASH_DATA, 5000);\n }\n console.log(`Flash data for ${partition.filename} sent successfully.`);\n }\n\n public async flashImage(image: ESPImage) {\n if (!this.connection.connected) {\n throw new Error(\"Device is not connected.\");\n }\n\n if (!this.connection.synced) {\n const synced = await this.sync();\n if (!synced) {\n throw new Error(\n \"ESP32 Needs to Sync before flashing. Hold the `boot` button on the device during sync attempts.\",\n );\n }\n }\n\n const attachCmd = new EspCommandSpiAttach();\n await this.writeToConnection(attachCmd.getSlipStreamEncodedPacketData());\n await this.readResponse(EspCommand.SPI_ATTACH);\n console.log(\"SPI_ATTACH successful.\");\n\n const setParamsCmd = new EspCommandSpiSetParams();\n await this.writeToConnection(setParamsCmd.getSlipStreamEncodedPacketData());\n await this.readResponse(EspCommand.SPI_SET_PARAMS);\n console.log(\"SPI_SET_PARAMS successful.\");\n\n const totalSize = image.partitions.reduce(\n (acc, part) => acc + part.binary.length,\n 0,\n );\n let flashedSize = 0;\n\n for (const partition of image.partitions) {\n const originalDispatchEvent = this.dispatchEvent;\n this.dispatchEvent = (event: Event) => {\n if (event.type === \"flash-progress\" && \"detail\" in event) {\n const partitionFlashed =\n (partition.binary.length * (event as CustomEvent).detail.progress) /\n 100;\n originalDispatchEvent.call(\n this,\n new CustomEvent(\"flash-image-progress\", {\n detail: {\n progress: ((flashedSize + partitionFlashed) / totalSize) * 100,\n partition: partition,\n },\n }),\n );\n }\n return originalDispatchEvent.call(this, event);\n };\n\n await this.flashPartition(partition);\n flashedSize += partition.binary.length;\n this.dispatchEvent = originalDispatchEvent;\n }\n\n this.dispatchEvent(\n new CustomEvent(\"flash-image-progress\", {\n detail: { progress: 100 },\n }),\n );\n\n console.log(\"Flashing complete. Resetting device...\");\n await this.sendResetPulse();\n console.log(\"Device has been reset.\");\n }\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Partition } from \"../partition/partition\";\n\nexport class BinFilePartition implements Partition {\n binary: Uint8Array = new Uint8Array(0);\n\n constructor(\n public readonly offset: number,\n public readonly filename: string,\n ) {}\n\n async load(): Promise<boolean> {\n try {\n const response = await fetch(this.filename);\n if (!response.ok) {\n console.error(\n `Failed to fetch ${this.filename}: ${response.statusText}`,\n );\n return false;\n }\n this.binary = new Uint8Array(await response.arrayBuffer());\n return true;\n } catch (e) {\n console.error(`Error loading file ${this.filename}:`, e);\n return false;\n }\n }\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Partition } from \"../partition/partition\";\nimport { BinFilePartition } from \"./bin-file-partition\";\n\nexport class ESPImage {\n partitions: Array<Partition> = [];\n\n addBootloader(fileName: string) {\n this.partitions.push(new BinFilePartition(0x1000, fileName));\n }\n\n addPartitionTable(fileName: string) {\n this.partitions.push(new BinFilePartition(0x8000, fileName));\n }\n\n addApp(fileName: string) {\n this.partitions.push(new BinFilePartition(0x10000, fileName));\n }\n\n addPartition(partition: Partition) {\n this.partitions.push(partition);\n }\n}\n","/**\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * CRCTable copied from https://github.com/espressif/esp-idf/blob/master/components/nvs_flash/mock/int/crc.cpp\n * Licenced under Apache 2.0, Copyright: 2015-2016 Espressif Systems (Shanghai) PTE LTD\n * Removed L notation from all entries.\n */\nconst crc32Table: number[] = [\n 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,\n 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,\n 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,\n 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,\n 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,\n 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,\n 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,\n 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,\n 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,\n 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,\n 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,\n 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,\n 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,\n 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,\n 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,\n 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,\n 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,\n 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,\n 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,\n 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,\n 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,\n 0xb7bd5c3b, 0xc0ba6cad,\n\n 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af,\n 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,\n 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2,\n 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,\n 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9,\n 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,\n 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c,\n 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,\n 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703,\n 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,\n 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226,\n 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,\n 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d,\n 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,\n 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, 0xff0f6a70,\n 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,\n 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7,\n 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,\n 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a,\n 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,\n 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1,\n 0x5a05df1b, 0x2d02ef8d,\n];\n\n/**\n * CRC function copied and adapted from: https://github.com/SheetJS/js-crc32/blob/master/crc32.js\n * Licencsed under Apache 2.0: https://github.com/SheetJS/js-crc32/blob/master/LICENSE\n * Changes made:\n * - Typescript notation added.\n * - Referencing above CRC32 Table.\n * - Returns Uint8Array from computed value.\n */\nexport function crc32(buf: Uint8Array, seed = 0xffffffff): Uint8Array {\n let C = seed ^ -1;\n const L = buf.length - 7;\n let i = 0;\n for (i = 0; i < L; ) {\n C = (C >>> 8) ^ crc32Table[(C ^ buf[i++]) & 0xff];\n C = (C >>> 8) ^ crc32Table[(C ^ buf[i++]) & 0xff];\n C = (C >>> 8) ^ crc32Table[(C ^ buf[i++]) & 0xff];\n C = (C >>> 8) ^ crc32Table[(C ^ buf[i++]) & 0xff];\n C = (C >>> 8) ^ crc32Table[(C ^ buf[i++]) & 0xff];\n C = (C >>> 8) ^ crc32Table[(C ^ buf[i++]) & 0xff];\n C = (C >>> 8) ^ crc32Table[(C ^ buf[i++]) & 0xff];\n C = (C >>> 8) ^ crc32Table[(C ^ buf[i++]) & 0xff];\n }\n while (i < L + 7) C = (C >>> 8) ^ crc32Table[(C ^ buf[i++]) & 0xff];\n C = C ^ -1;\n const buffer = new ArrayBuffer(4);\n const view = new DataView(buffer);\n view.setUint32(0, C, true);\n return new Uint8Array(buffer);\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport enum NvsType {\n U8 = 0x01,\n I8 = 0x11,\n U16 = 0x02,\n I16 = 0x12,\n U32 = 0x04,\n I32 = 0x14,\n U64 = 0x08,\n I64 = 0x18,\n STR = 0x21,\n BLOB = 0x42,\n ANY = 0xff,\n}\n\n// Values correspond to the NVS documentation.\nexport enum NvsEntryState {\n Empty = 0b11,\n Written = 0b10,\n Erased = 0b00,\n}\n\nexport class NVSSettings {\n static readonly BLOCK_SIZE: number = 32;\n static readonly PAGE_SIZE: number = 4096;\n static readonly PAGE_MAX_ENTRIES: number = 126;\n static readonly PAGE_ACTIVE: number = 0xfffffffe;\n static readonly PAGE_FULL: number = 0xfffffffc;\n static readonly NVS_VERSION: number = 0xfe; // version 2\n static readonly DEFAULT_NAMESPACE: string = \"storage\";\n}\n","/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { crc32 } from \"../utils/crc32\";\nimport { NVSSettings, NvsType } from \"./nvs-settings\";\n\nconst NVS_BLOCK_SIZE = NVSSettings.BLOCK_SIZE;\n\nexport class NvsEntry implements NvsKeyValue {\n namespaceIndex: number;\n type: NvsType;\n key: string;\n data: string | number;\n chunkIndex: number;\n\n headerNamespace: Uint8Array;\n headerType: Uint8Array;\n headerSpan: Uint8Array;\n headerChunkIndex: Uint8Array;\n headerCRC32: Uint8Array;\n headerK