UNPKG

esp-controller

Version:

Typescript package for connecting and flashing images to your ESP device.

1 lines 122 kB
{"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-begin.ts","../src/esp/command.flash-data.ts","../src/esp/command.read-reg.ts","../src/esp/command.mem-begin.ts","../src/esp/command.mem-data.ts","../src/esp/command.mem-end.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/**\n * Decodes a Base64 string into a Uint8Array.\n * @param base64 The Base64 encoded string.\n * @returns The decoded Uint8Array.\n */\nexport function base64ToUint8Array(base64: string): Uint8Array {\n const binaryString = atob(base64);\n const len = binaryString.length;\n const bytes = new Uint8Array(len);\n for (let i = 0; i < len; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes;\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 // Commands supported by ROM and Stub loaders\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 // Stub loader only commands\n ERASE_FLASH = 0xd0,\n ERASE_REGION = 0xd1,\n READ_FLASH = 0xd2,\n RUN_USER_CODE = 0xd3,\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 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 { 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 EspCommandReadReg extends EspCommandPacket {\n private readRegData = new ArrayBuffer(4);\n\n constructor(address: number) {\n super();\n\n this.command = EspCommand.READ_REG;\n this.direction = EspPacketDirection.REQUEST;\n new DataView(this.readRegData).setUint32(0, address, true);\n this.data = new Uint8Array(this.readRegData);\n }\n}\n","import { EspCommand, EspCommandPacket, EspPacketDirection } from \"./command\";\n\nexport class EspCommandMemBegin extends EspCommandPacket {\n constructor(\n public totalSize: number,\n public numPackets: number,\n public packetSize: number,\n public offset: number,\n ) {\n super();\n this.direction = EspPacketDirection.REQUEST;\n this.command = EspCommand.MEM_BEGIN;\n this.checksum = 0; // Not used for this command\n\n const dataPayload = new Uint8Array(16);\n const view = new DataView(dataPayload.buffer);\n view.setUint32(0, this.totalSize, true);\n view.setUint32(4, this.numPackets, true);\n view.setUint32(8, this.packetSize, true);\n view.setUint32(12, this.offset, true);\n\n this.data = dataPayload;\n }\n}\n","import { EspCommand, EspCommandPacket, EspPacketDirection } from \"./command\";\n\nexport class EspCommandMemData extends EspCommandPacket {\n constructor(\n public binary: Uint8Array,\n public sequence: number,\n public packetSize: number,\n ) {\n super();\n\n const chunk = binary.slice(\n sequence * packetSize,\n (sequence + 1) * packetSize,\n );\n\n const header = new Uint8Array(16);\n const view = new DataView(header.buffer);\n view.setUint32(0, chunk.length, true); // Data size\n view.setUint32(4, sequence, true); // Sequence number\n view.setUint32(8, 0, true); // Zero\n view.setUint32(12, 0, true); // Zero\n\n this.direction = EspPacketDirection.REQUEST;\n this.command = EspCommand.MEM_DATA;\n this.data = new Uint8Array([...header, ...chunk]);\n this.checksum = this.generateChecksum(chunk);\n }\n}\n","import { EspCommand, EspCommandPacket, EspPacketDirection } from \"./command\";\n\nexport class EspCommandMemEnd extends EspCommandPacket {\n constructor(\n public executeFlag: number,\n public entryPoint: number,\n ) {\n super();\n this.direction = EspPacketDirection.REQUEST;\n this.command = EspCommand.MEM_END;\n this.checksum = 0; // Not used\n\n const dataPayload = new Uint8Array(8);\n const view = new DataView(dataPayload.buffer);\n view.setUint32(0, this.executeFlag, true);\n view.setUint32(4, this.entryPoint, true);\n\n this.data = dataPayload;\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, base64ToUint8Array } from \"../utils/common\";\nimport { EspCommand, EspCommandPacket, EspPacketDirection } from \"./command\";\nimport { ESPImage } from \"../image/image\";\nimport { Partition } from \"../partition/partition\";\n\n// Import all necessary command classes\nimport { EspCommandSync } from \"./command.sync\";\nimport { EspCommandSpiAttach } from \"./command.spi-attach\";\nimport { EspCommandSpiSetParams } from \"./command.spi-set-params\";\nimport { EspCommandFlashBegin } from \"./command.flash-begin\";\nimport { EspCommandFlashData } from \"./command.flash-data\";\nimport { EspCommandReadReg } from \"./command.read-reg\";\nimport { EspCommandMemBegin } from \"./command.mem-begin\";\nimport { EspCommandMemData } from \"./command.mem-data\";\nimport { EspCommandMemEnd } from \"./command.mem-end\";\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 * Known chip families and their magic values.\n */\nexport enum ChipFamily {\n ESP32 = 0x00f01d83,\n ESP32S2 = 0x000007c6,\n ESP32S3 = 0x9,\n ESP32C3 = 0x6921506f,\n ESP32C6 = 0x2ce0806f,\n ESP32H2 = 0xca02c06f,\n ESP8266 = 0xfff0c101,\n UNKNOWN = 0xffffffff,\n}\n\n/**\n * Interface representing the structure of a flasher stub JSON file.\n */\nexport interface Stub {\n entry: number;\n text_start: number;\n text: string; // Base64 encoded\n data_start: number;\n data: string; // Base64 encoded\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 detected chip family. Null if not yet detected. */\n chip: ChipFamily | null;\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 chip: null,\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 this.connection.chip = null;\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 { value, done } = await Promise.race([\n responseReader.read(),\n timeoutPromise,\n ]);\n\n if (done) {\n throw new Error(\"Stream closed unexpectedly while syncing.\");\n }\n\n if (value) {\n try {\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 } catch {\n // Ignore parsing errors and continue reading from the stream\n }\n }\n }\n } catch (e) {\n console.log(`Sync attempt ${i + 1} failed.`, 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 public async detectChip(): Promise<ChipFamily> {\n if (!this.connection.synced) {\n throw new Error(\"Device must be synced to detect chip type.\");\n }\n const CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000;\n const readRegCmd = new EspCommandReadReg(CHIP_DETECT_MAGIC_REG_ADDR);\n await this.writeToConnection(readRegCmd.getSlipStreamEncodedPacketData());\n const response = await this.readResponse(EspCommand.READ_REG);\n\n const magicValue = response.value;\n\n const numericChipValues = Object.values(ChipFamily).filter(\n (v) => typeof v === \"number\",\n ) as ChipFamily[];\n\n const chip =\n numericChipValues.find((c) => c === magicValue) || ChipFamily.UNKNOWN;\n\n this.connection.chip = chip;\n console.log(\n `Detected chip: ${ChipFamily[chip]} (Magic value: ${toHex(new Uint8Array(new Uint32Array([magicValue]).buffer))})`,\n );\n\n if (chip === ChipFamily.UNKNOWN) {\n throw new Error(\"Could not detect a supported chip family.\");\n }\n return chip;\n }\n\n public async loadToRam(\n binary: Uint8Array,\n offset: number,\n execute = false,\n entryPoint = 0,\n ) {\n console.log(\n `Loading binary to RAM at offset ${toHex(new Uint8Array(new Uint32Array([offset]).buffer))}`,\n );\n const packetSize = 1460;\n const numPackets = Math.ceil(binary.length / packetSize);\n\n const memBeginCmd = new EspCommandMemBegin(\n binary.length,\n numPackets,\n packetSize,\n offset,\n );\n await this.writeToConnection(memBeginCmd.getSlipStreamEncodedPacketData());\n await this.readResponse(EspCommand.MEM_BEGIN);\n\n for (let i = 0; i < numPackets; i++) {\n const memDataCmd = new EspCommandMemData(binary, i, packetSize);\n await this.writeToConnection(memDataCmd.getSlipStreamEncodedPacketData());\n await this.readResponse(EspCommand.MEM_DATA, 1000);\n }\n\n if (execute) {\n console.log(`Executing from entry point ${entryPoint}`);\n const memEndCmd = new EspCommandMemEnd(1, entryPoint);\n await this.writeToConnection(memEndCmd.getSlipStreamEncodedPacketData());\n await this.readResponse(EspCommand.MEM_END);\n }\n }\n\n /**\n * Fetches the stub for the given chip family from the local file system.\n * @param chip The chip family to fetch the stub for.\n * @returns A promise that resolves to the Stub object.\n */\n private async getStubForChip(chip: ChipFamily): Promise<Stub> {\n const chipNameMap: { [key in ChipFamily]?: string } = {\n [ChipFamily.ESP32]: \"32\",\n [ChipFamily.ESP32S2]: \"32s2\",\n [ChipFamily.ESP32S3]: \"32s3\",\n [ChipFamily.ESP32C3]: \"32c3\",\n [ChipFamily.ESP32C6]: \"32c6\",\n [ChipFamily.ESP32H2]: \"32h2\",\n [ChipFamily.ESP8266]: \"8266\",\n };\n\n const chipName = chipNameMap[chip];\n if (!chipName) {\n throw new Error(`No stub file mapping for chip: ${ChipFamily[chip]}`);\n }\n\n const stubUrl = `./stub-flasher/stub_flasher_${chipName}.json`;\n console.log(`Fetching stub from ${stubUrl}`);\n\n try {\n const response = await fetch(stubUrl);\n if (!response.ok) {\n throw new Error(`Failed to fetch stub file: ${response.statusText}`);\n }\n return await response.json();\n } catch (e) {\n console.error(`Error loading stub for ${ChipFamily[chip]}:`, e);\n throw e;\n }\n }\n\n private async uploadStub(stub: Stub): Promise<void> {\n const text = base64ToUint8Array(stub.text);\n const data = base64ToUint8Array(stub.data);\n\n await this.loadToRam(text, stub.text_start, false);\n await this.loadToRam(data, stub.data_start, false);\n\n console.log(`Starting stub at entry point 0x${stub.entry.toString(16)}...`);\n const memEndCmd = new EspCommandMemEnd(1, stub.entry);\n await this.writeToConnection(memEndCmd.getSlipStreamEncodedPacketData());\n\n await this.readResponse(EspCommand.MEM_END);\n console.log(\"Stub started successfully.\");\n\n await this.awaitOhaiResponse();\n }\n\n private async awaitOhaiResponse(timeout = 2000): Promise<void> {\n let responseReader: ReadableStreamDefaultReader<Uint8Array> | undefined;\n // The \"OHAI\" payload is 4 bytes: 0x4F, 0x48, 0x41, 0x49\n const ohaiPacket = new Uint8Array([0x4f, 0x48, 0x41, 0x49]);\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(timeout).then(() => {\n throw new Error(\n `Timeout: Did not receive \"OHAI\" from stub within ${timeout}ms.`,\n );\n });\n\n console.log(\"Waiting for 'OHAI' packet from stub...\");\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 waiting for 'OHAI'.\",\n );\n }\n\n // Compare the received packet with the expected \"OHAI\" signature\n if (value && value.length === ohaiPacket.length) {\n if (value.every((byte, index) => byte === ohaiPacket[index])) {\n console.log(\"'OHAI' packet received, stub confirmed.\");\n return; // Success\n }\n }\n }\n } finally {\n if (responseReader) {\n responseReader.releaseLock();\n }\n }\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 try {\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 } catch {\n // Ignore parsing errors and continue reading\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 /**\n * Main method to flash a complete image.\n * @param image The ESPImage to flash.\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 if (!this.connection.chip) {\n await this.detectChip();\n }\n\n const stub = await this.getStubForChip(this.connection.chip!);\n await this.uploadStub(stub);\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 p