100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
/**
|
|
* ESP32 image format parser.
|
|
*
|
|
* The backend produces a merged 4 MB flash image containing:
|
|
* offset 0x0000 : empty (0xFF)
|
|
* offset 0x1000 : bootloader (ESP32 image format)
|
|
* offset 0x8000 : partition table
|
|
* offset 0x10000: application (ESP32 image format) ← we parse this
|
|
*
|
|
* ESP32 image header (24 bytes, ESP-IDF esp_image_format.h):
|
|
* +0 magic 0xE9
|
|
* +1 segment_count
|
|
* +2 spi_mode
|
|
* +3 spi_speed_size
|
|
* +4 entry_addr uint32 LE
|
|
* +8 … extended fields …
|
|
* total: 24 bytes
|
|
*
|
|
* Each segment (8-byte header + data):
|
|
* +0 load_addr uint32 LE — virtual address to load data at
|
|
* +4 data_len uint32 LE
|
|
* +8 data[data_len]
|
|
*/
|
|
|
|
export const ESP32_APP_FLASH_OFFSET = 0x10000;
|
|
const ESP32_MAGIC = 0xE9;
|
|
const HEADER_SIZE = 24;
|
|
const SEG_HDR_SIZE = 8;
|
|
|
|
export interface Esp32Segment {
|
|
loadAddr: number;
|
|
data: Uint8Array;
|
|
}
|
|
|
|
export interface Esp32ParsedImage {
|
|
entryPoint: number;
|
|
segments: Esp32Segment[];
|
|
}
|
|
|
|
function parseAppAt(img: Uint8Array, base: number): Esp32ParsedImage {
|
|
const view = new DataView(img.buffer, img.byteOffset, img.byteLength);
|
|
|
|
if (view.getUint8(base) !== ESP32_MAGIC) {
|
|
throw new Error(
|
|
`Bad ESP32 magic at 0x${base.toString(16)}: ` +
|
|
`expected 0xE9, got 0x${view.getUint8(base).toString(16)}`
|
|
);
|
|
}
|
|
|
|
const segCount = view.getUint8(base + 1);
|
|
const entryPoint = view.getUint32(base + 4, /*littleEndian=*/true);
|
|
|
|
const segments: Esp32Segment[] = [];
|
|
let pos = base + HEADER_SIZE;
|
|
|
|
for (let i = 0; i < segCount; i++) {
|
|
if (pos + SEG_HDR_SIZE > img.length) break;
|
|
|
|
const loadAddr = view.getUint32(pos, true);
|
|
const dataLen = view.getUint32(pos + 4, true);
|
|
pos += SEG_HDR_SIZE;
|
|
|
|
if (pos + dataLen > img.length) {
|
|
console.warn(`[esp32ImageParser] Segment ${i} data truncated`);
|
|
break;
|
|
}
|
|
|
|
segments.push({ loadAddr, data: img.slice(pos, pos + dataLen) });
|
|
pos += dataLen;
|
|
}
|
|
|
|
return { entryPoint, segments };
|
|
}
|
|
|
|
/**
|
|
* Parse a merged ESP32 flash image or a raw app binary.
|
|
*
|
|
* Accepts:
|
|
* - 4 MB merged image (bootloader + partitions + app) — reads app at 0x10000
|
|
* - Raw app binary (magic 0xE9 at offset 0)
|
|
*
|
|
* Throws if no valid image is found.
|
|
*/
|
|
export function parseMergedFlashImage(data: Uint8Array): Esp32ParsedImage {
|
|
// Standard merged image: app at offset 0x10000
|
|
if (data.length >= ESP32_APP_FLASH_OFFSET + HEADER_SIZE && data[ESP32_APP_FLASH_OFFSET] === ESP32_MAGIC) {
|
|
return parseAppAt(data, ESP32_APP_FLASH_OFFSET);
|
|
}
|
|
|
|
// Fallback: raw app binary (no bootloader prefix)
|
|
if (data.length >= HEADER_SIZE && data[0] === ESP32_MAGIC) {
|
|
return parseAppAt(data, 0);
|
|
}
|
|
|
|
throw new Error(
|
|
`No valid ESP32 image magic found ` +
|
|
`(tried offsets 0x10000 and 0x0, image size ${data.length} bytes)`
|
|
);
|
|
}
|