esptool-mergebin-js/mergebin.js
2023-02-27 15:26:14 -08:00

292 lines
No EOL
10 KiB
JavaScript

// SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
//
// SPDX-License-Identifier: MIT
const FLASH_MODES = {"qio": 0, "qout": 1, "dio": 2, "dout": 3};
const ESP_IMAGE_MAGIC = 0xE9;
export class MergeBin {
constructor() {
this._files = [];
this.flashSize = "keep";
this.flashMode = "keep";
this.flashFreq = "keep";
this.fillFlashSize = null;
this.targetOffset = 0;
this.chipDefs = {"esp32": null, "esp32c3": null, "esp32s3": null, "esp32s2": null, "esp8266": null};
}
addFile(contents, offset) {
if (contents instanceof ArrayBuffer) {
contents = new Uint8Array(contents);
}
this._files.push({contents: contents, offset: offset});
}
setFlashSize(size = "keep") {
if (size != "keep") {
if (this._flashSizeBytes(size)) {
this.flashSize = size;
}
} else {
this.flashSize = size;
}
}
setFlashMode(mode) {
if (mode != "keep") {
if (Object.keys(FLASH_MODES).includes(mode)) {
this.flashMode = mode;
} else {
throw new Error(`Invalid flash mode '${mode}'. Valid modes are: ${Object.keys(FLASH_MODES).join(", ")}`);
}
} else {
this.flashMode = mode;
}
}
setFlashFreq(freq) {
// No good way to validate before we know the chip
this.flashFreq = freq;
}
setFillFlashSize(size) {
if (this._flashSizeBytes > 0) {
this.fillFlashSize = size;
}
}
setTargetOffset(offset) {
if (offset < 0) {
throw new Error("Target offset must be >= 0");
}
this.targetOffset = offset;
}
_sorted(array, key) {
return array.sort(function(a, b) {
var aVal = a[key]; var bVal = b[key];
return ((aVal < bVal) ? -1 : ((aVal > bVal) ? 1 : 0));
});
}
async loadChip(chipname) {
// Check if we've already loaded it
if (Object.keys(this.chipDefs).includes(chipname) && this.chipDefs[chipname] != null) {
return this.chipDefs[chipname];
}
// Load the chip definition. I wish there was a less awkward way to do this
if (chipname == "esp32") {
const {ESP32} = await import(`./targets/${chipname}.js`);
this.chipDefs[chipname] = new ESP32();
} else if (chipname == "esp32c3") {
const {ESP32C3} = await import(`./targets/${chipname}.js`);
this.chipDefs[chipname] = new ESP32C3();
} else if (chipname == "esp32s3") {
const {ESP32S3} = await import(`./targets/${chipname}.js`);
this.chipDefs[chipname] = new ESP32S3();
} else if (chipname == "esp32s2") {
const {ESP32S2} = await import(`./targets/${chipname}.js`);
this.chipDefs[chipname] = new ESP32S2();
} else if (chipname == "esp8266") {
const {ESP8266} = await import(`./targets/${chipname}.js`);
this.chipDefs[chipname] = new ESP8266();
}
// Return the chip definition if it exists
if (Object.keys(this.chipDefs).includes(chipname)) {
return this.chipDefs[chipname];
}
return null;
}
_calcBufferSize(inputFiles) {
if (this.fillFlashSize != null) {
return this._flashSizeBytes(this.fillFlashSize);
}
let lastFile = inputFiles[inputFiles.length - 1];
let highestAddress = lastFile.offset + lastFile.contents.byteLength;
return highestAddress - this.targetOffset;
}
_flashSizeBytes = (size) => {
/*
Given a flash size of the type passed in this.flashSize
(ie 512KB or 1MB) then return the size in bytes.
*/
if (size.indexOf("MB") > -1) {
return parseInt(size.slice(0, size.indexOf("MB"))) * 1024 * 1024;
} else if (size.indexOf("KB") > -1) {
return parseInt(size.slice(0, size.indexOf("KB"))) * 1024;
}
throw new Error(`Unknown size ${size}`);
};
async generate(chip) {
// This is where everything comes together
let chipClass = await this.loadChip(chip);
if (!chipClass) {
msg = `Invalid chip choice: ${chip}\n (choose from ${Object.keys(this.chipDefs).join(", ")})`;
throw new Error(msg);
}
// sort the files by offset.
// The AddrFilenamePairAction has already checked for overlap
let inputFiles = this._sorted(this._files, "offset");
if (!inputFiles) {
throw new Error("No input files added");
}
if (inputFiles[0].offset < this.targetOffset) {
throw new Error(`Output file target offset is ${this.toHex(this.targetOffset)}. Input file offset ${this.toHex(firstOffset)} is before this.`);
}
let outputBuffer = new Uint8Array(this._calcBufferSize(inputFiles)).fill(0xFF);
let outputPointer = 0;
const write = (data) => {
// Use outputPointer to keep track of offset
outputBuffer.set(data, outputPointer);
// Advance the outputPointer
outputPointer += data.byteLength;
};
const padTo = (flashOffset) => {
// Create a buffer that goes from the current pointer to the flash offset in size
let padBuffer = new Uint8Array(flashOffset - this.targetOffset - outputPointer).fill(0xFF);
write(padBuffer);
};
for (let file of Object.values(inputFiles)) {
padTo(file.offset);
var image = file.contents;
image = this._updateImageFlashParams(chipClass, file.offset, chip, image);
write(image);
}
if (this.fillFlashSize) {
padTo(this._flashSizeBytes(this.fillFlashSize));
}
console.log(`Generated output buffer of ${outputBuffer.byteLength} bytes, ready to flash to offset ${this.toHex(this.targetOffset)}`);
return outputBuffer;
}
_parseFlashSize(chipClass) {
if (Object.keys(chipClass.FLASH_SIZES).includes(this.flashSize)) {
return chipClass.FLASH_SIZES[this.flashSize];
}
throw new Error(
`Flash size '${arg}' is not supported by this chip type. \n` +
`Supported sizes: ${Object.keys(chipClass.FLASH_SIZES).join(", ")}`
);
}
_parseFlashFreq(chipClass) {
if (Object.keys(chipClass.FLASH_FREQUENCY).includes(this.flashFreq)) {
return chipClass.FLASH_FREQUENCY[this.flashFreq];
}
throw new Error(
`Flash frequency '${arg}' is not supported by this chip type. \n` +
`Supported frequencies: ${Object.keys(chipClass.FLASH_FREQUENCY).join(", ")}`
);
}
_updateImageFlashParams(esp, address, chip, image) {
if (image.length < 8) {
return image; // not long enough to be a bootloader image
}
// unpack the (potential) image header
var header = Array.from(new Uint8Array(image, 0, 4));
let headerMagic = header[0];
let headerFlashMode = header[2];
let headerFlashSizeFreq = header[3];
if (address != esp.BOOTLOADER_FLASH_OFFSET) {
return image; // not flashing bootloader offset, so don't modify this
}
if (this.flashMode == "keep" && this.flashFreq == "keep" && this.flashSize == "keep") {
return image; // all settings are 'keep', not modifying anything
}
// easy check if this is an image: does it start with a magic byte?
if (headerMagic != ESP_IMAGE_MAGIC) {
console.log(`Warning: Image file at ${this.toHex(address)} doesn't look like an image file, so not changing any flash settings.`);
return image;
}
// Verification of the image is skipped at this time due to complexity
// After the 8-byte header comes the extended header for chips others than ESP8266.
// The 15th byte of the extended header indicates if the image is protected by
// a SHA256 checksum. In that case we should not modify the header because
// the checksum check would fail.
var extendedHeader = Array.from(new Uint8Array(image, 8, 24));
var shaImpliesKeep = chip != "esp8266" && extendedHeader(15) == 1;
const print_keep_warning = (argToKeep, argUsed) => {
console.log(
`Warning: Image file at ${this.toHex(addr)} is protected with a hash checksum, ` +
`so not changing the flash ${argToKeep.toLowerCase()} setting. ` +
`Use setFlash${argToKeep}("keep") instead of setFlash${argToKeep}(${argUsed}) ` +
`in order to remove this warning, or use the --dont-append-digest option ` +
`for the elf2image command in order to generate an image file ` +
`without a hash checksum`);
};
if (this.flashMode != "keep") {
var newFlashMode = FLASH_MODES[this.flashMode];
if (headerFlashMode != newFlashMode && shaImpliesKeep) {
print_keep_warning("Mode", this.flashMode);
} else {
headerFlashMode = newFlashMode;
}
}
let flashFreq = headerFlashSizeFreq & 0x0F;
if (this.flashFreq != "keep") {
var newFlashFreq = this._parseFlashFreq(esp);
if (flashFreq != newFlashFreq && shaImpliesKeep) {
print_keep_warning("Freq", this.flashFreq);
} else {
flashFreq = newFlashFreq;
}
}
let flashSize = headerFlashSizeFreq & 0xF0;
if (this.flashSize != "keep") {
var newFlashSize = this._parseFlashSize(esp);
if (flashSize != newFlashSize && shaImpliesKeep) {
print_keep_warning("Size", this.flashSize);
} else {
flashSize = newFlashSize;
}
}
var flashParams = new Uint8Array([headerFlashMode, flashSize + flashFreq]);
if (flashParams != headerFlashSizeFreq) {
console.log(`Flash params set to ${this.toHex(flashParams[0] << 8 + flashParams[1], 4)}`);
image.set(flashParams, 3);
}
return image;
}
toHex(value, size = 2) {
let hex = value.toString(16).toUpperCase();
if (hex.startsWith("-")) {
return "-0x" + hex.substring(1).padStart(size, "0");
} else {
return "0x" + hex.padStart(size, "0");
}
};
}