292 lines
No EOL
10 KiB
JavaScript
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");
|
|
}
|
|
};
|
|
} |