@microbit/microbit-universal-hex
Version:
Create micro:bit Universal Hexes.
432 lines • 18.5 kB
JavaScript
/**
* Convert between standard Intel Hex strings and Universal Hex strings.
*
* This module provides the main functionality to convert Intel Hex strings
* (with their respective Board IDs) into the Universal Hex format.
*
* It can also separate a Universal Hex string into the individual Intel Hex
* strings that forms it.
*
* The content here assumes familiarity with the
* [Universal Hex Specification](https://github.com/microbit-foundation/spec-universal-hex)
* and the rest of
* [this library documentation](https://microbit-foundation.github.io/microbit-universal-hex/).
* @packageDocumentation
*
* (c) 2020 Micro:bit Educational Foundation and the project contributors.
* SPDX-License-Identifier: MIT
*/
import * as ihex from './ihex';
var V1_BOARD_IDS = [0x9900, 0x9901];
var BLOCK_SIZE = 512;
/**
* The Board ID is used to identify the different targets from a Universal Hex.
* In this case the target represents a micro:bit version.
* For micro:bit V1 (v1.3, v1.3B and v1.5) the `boardId` is `0x9900`, and for
* V2 `0x9903`.
*/
var microbitBoardId;
(function (microbitBoardId) {
microbitBoardId[microbitBoardId["V1"] = 39168] = "V1";
microbitBoardId[microbitBoardId["V2"] = 39171] = "V2";
})(microbitBoardId || (microbitBoardId = {}));
/**
* Converts an Intel Hex string into a Hex string using the 512 byte blocks
* format and the Universal Hex specific record types.
*
* The output of this function is not a fully formed Universal Hex, but one part
* of a Universal Hex, ready to be merged by the calling code.
*
* More information on this "block" format:
* https://github.com/microbit-foundation/spec-universal-hex
*
* @throws {Error} When the Board ID is not between 0 and 2^16.
* @throws {Error} When there is an EoF record not at the end of the file.
*
* @param iHexStr - Intel Hex string to convert into the custom format with 512
* byte blocks and the customer records.
* @returns New Intel Hex string with the custom format.
*/
function iHexToCustomFormatBlocks(iHexStr, boardId) {
// Hex files for v1.3 and v1.5 continue using the normal Data Record Type
var replaceDataRecord = !V1_BOARD_IDS.includes(boardId);
// Generate some constant records
var startRecord = ihex.blockStartRecord(boardId);
var currentExtAddr = ihex.extLinAddressRecord(0);
// Pre-calculate known record lengths
var extAddrRecordLen = currentExtAddr.length;
var startRecordLen = startRecord.length;
var endRecordBaseLen = ihex.blockEndRecord(0).length;
var padRecordBaseLen = ihex.paddedDataRecord(0).length;
var hexRecords = ihex.iHexToRecordStrs(iHexStr);
var recordPaddingCapacity = ihex.findDataFieldLength(hexRecords);
if (!hexRecords.length)
return '';
if (isUniversalHexRecords(hexRecords)) {
throw new Error("Board ID " + boardId + " Hex is already a Universal Hex.");
}
// Each loop iteration corresponds to a 512-bytes block
var ih = 0;
var blockLines = [];
while (ih < hexRecords.length) {
var blockLen = 0;
// Check for an extended linear record to not repeat it after a block start
var firstRecordType = ihex.getRecordType(hexRecords[ih]);
if (firstRecordType === ihex.RecordType.ExtendedLinearAddress) {
currentExtAddr = hexRecords[ih];
ih++;
}
else if (firstRecordType === ihex.RecordType.ExtendedSegmentAddress) {
currentExtAddr = ihex.convertExtSegToLinAddressRecord(hexRecords[ih]);
ih++;
}
blockLines.push(currentExtAddr);
blockLen += extAddrRecordLen + 1;
blockLines.push(startRecord);
blockLen += startRecordLen + 1;
blockLen += endRecordBaseLen + 1;
var endOfFile = false;
while (hexRecords[ih] &&
BLOCK_SIZE >= blockLen + hexRecords[ih].length + 1) {
var record = hexRecords[ih++];
var recordType = ihex.getRecordType(record);
if (replaceDataRecord && recordType === ihex.RecordType.Data) {
record = ihex.convertRecordTo(record, ihex.RecordType.CustomData);
}
else if (recordType === ihex.RecordType.ExtendedLinearAddress) {
currentExtAddr = record;
}
else if (recordType === ihex.RecordType.ExtendedSegmentAddress) {
record = ihex.convertExtSegToLinAddressRecord(record);
currentExtAddr = record;
}
else if (recordType === ihex.RecordType.EndOfFile) {
endOfFile = true;
break;
}
blockLines.push(record);
blockLen += record.length + 1;
}
if (endOfFile) {
// Error if we encounter an EoF record and it's not the end of the file
if (ih !== hexRecords.length) {
// Might be MakeCode hex for V1 as they did this with the EoF record
if (isMakeCodeForV1HexRecords(hexRecords)) {
throw new Error("Board ID " + boardId + " Hex is from MakeCode, import this hex into the MakeCode editor to create a Universal Hex.");
}
else {
throw new Error("EoF record found at record " + ih + " of " + hexRecords.length + " in Board ID " + boardId + " hex");
}
}
// The EoF record goes after the Block End Record, it won't break 512-byte
// boundary as it was already calculated in the previous loop that it fits
blockLines.push(ihex.blockEndRecord(0));
blockLines.push(ihex.endOfFileRecord());
}
else {
// We might need additional padding records
// const charsLeft = BLOCK_SIZE - blockLen;
while (BLOCK_SIZE - blockLen > recordPaddingCapacity * 2) {
var record = ihex.paddedDataRecord(Math.min((BLOCK_SIZE - blockLen - (padRecordBaseLen + 1)) / 2, recordPaddingCapacity));
blockLines.push(record);
blockLen += record.length + 1;
}
blockLines.push(ihex.blockEndRecord((BLOCK_SIZE - blockLen) / 2));
}
}
blockLines.push(''); // Ensure there is a blank new line at the end
return blockLines.join('\n');
}
/**
* Converts an Intel Hex string into a Hex string using custom records and
* aligning the content size to a 512-byte boundary.
*
* The output of this function is not a fully formed Universal Hex, but one part
* of a Universal Hex, ready to be merged by the calling code.
*
* More information on this "section" format:
* https://github.com/microbit-foundation/spec-universal-hex
*
* @throws {Error} When the Board ID is not between 0 and 2^16.
* @throws {Error} When there is an EoF record not at the end of the file.
*
* @param iHexStr - Intel Hex string to convert into the custom format with 512
* byte blocks and the customer records.
* @returns New Intel Hex string with the custom format.
*/
function iHexToCustomFormatSection(iHexStr, boardId) {
var sectionLines = [];
var sectionLen = 0;
var ih = 0;
var addRecordLength = function (record) {
sectionLen += record.length + 1; // Extra character counted for '\n'
};
var addRecord = function (record) {
sectionLines.push(record);
addRecordLength(record);
};
var hexRecords = ihex.iHexToRecordStrs(iHexStr);
if (!hexRecords.length)
return '';
if (isUniversalHexRecords(hexRecords)) {
throw new Error("Board ID " + boardId + " Hex is already a Universal Hex.");
}
// If first record is not an Extended Segmented/Linear Address we start at 0x0
var iHexFirstRecordType = ihex.getRecordType(hexRecords[0]);
if (iHexFirstRecordType === ihex.RecordType.ExtendedLinearAddress) {
addRecord(hexRecords[0]);
ih++;
}
else if (iHexFirstRecordType === ihex.RecordType.ExtendedSegmentAddress) {
addRecord(ihex.convertExtSegToLinAddressRecord(hexRecords[0]));
ih++;
}
else {
addRecord(ihex.extLinAddressRecord(0));
}
// Add the Block Start record to the beginning of the segment
addRecord(ihex.blockStartRecord(boardId));
// Iterate through the rest of the records and add them
var replaceDataRecord = !V1_BOARD_IDS.includes(boardId);
var endOfFile = false;
while (ih < hexRecords.length) {
var record = hexRecords[ih++];
var recordType = ihex.getRecordType(record);
if (recordType === ihex.RecordType.Data) {
addRecord(replaceDataRecord
? ihex.convertRecordTo(record, ihex.RecordType.CustomData)
: record);
}
else if (recordType === ihex.RecordType.ExtendedSegmentAddress) {
addRecord(ihex.convertExtSegToLinAddressRecord(record));
}
else if (recordType === ihex.RecordType.ExtendedLinearAddress) {
addRecord(record);
}
else if (recordType === ihex.RecordType.EndOfFile) {
endOfFile = true;
break;
}
}
if (ih !== hexRecords.length) {
// The End Of File record was encountered mid-file, might be a MakeCode hex
if (isMakeCodeForV1HexRecords(hexRecords)) {
throw new Error("Board ID " + boardId + " Hex is from MakeCode, import this hex into the MakeCode editor to create a Universal Hex.");
}
else {
throw new Error("EoF record found at record " + ih + " of " + hexRecords.length + " in Board ID " + boardId + " hex ");
}
}
// Add to the section size calculation the minimum length for the Block End
// record that will be placed at the end (no padding included yet)
addRecordLength(ihex.blockEndRecord(0));
// Calculate padding required to end in a 512-byte boundary
var recordNoDataLenChars = ihex.paddedDataRecord(0).length + 1;
var recordDataMaxBytes = ihex.findDataFieldLength(hexRecords);
var paddingCapacityChars = recordDataMaxBytes * 2;
var charsNeeded = (BLOCK_SIZE - (sectionLen % BLOCK_SIZE)) % BLOCK_SIZE;
while (charsNeeded > paddingCapacityChars) {
var byteLen = (charsNeeded - recordNoDataLenChars) >> 1; // Integer div 2
var record = ihex.paddedDataRecord(Math.min(byteLen, recordDataMaxBytes));
addRecord(record);
charsNeeded = (BLOCK_SIZE - (sectionLen % BLOCK_SIZE)) % BLOCK_SIZE;
}
sectionLines.push(ihex.blockEndRecord(charsNeeded >> 1));
if (endOfFile)
sectionLines.push(ihex.endOfFileRecord());
sectionLines.push(''); // Ensure there is a blank new line at the end
return sectionLines.join('\n');
}
/**
* Creates a Universal Hex from a collection of Intel Hex strings and their
* board IDs.
*
* For the current micro:bit board versions use the values from the
* `microbitBoardId` enum.
*
* @param hexes An array of objects containing an Intel Hex string and the board
* ID associated with it.
* @param blocks Indicate if the Universal Hex format should be "blocks"
* instead of "sections". The current specification recommends using the
* default "sections" format as is much quicker in micro:bits with DAPLink
* version 0234.
* @returns A Universal Hex string.
*/
function createUniversalHex(hexes, blocks) {
if (blocks === void 0) { blocks = false; }
if (!hexes.length)
return '';
var iHexToCustomFormat = blocks
? iHexToCustomFormatBlocks
: iHexToCustomFormatSection;
var eofNlRecord = ihex.endOfFileRecord() + '\n';
var customHexes = [];
// We remove the EoF record from all but the last hex file so that the last
// blocks are padded and there is single EoF record
for (var i = 0; i < hexes.length - 1; i++) {
var customHex = iHexToCustomFormat(hexes[i].hex, hexes[i].boardId);
if (customHex.endsWith(eofNlRecord)) {
customHex = customHex.slice(0, customHex.length - eofNlRecord.length);
}
customHexes.push(customHex);
}
// Process the last hex file with a guaranteed EoF record
var lastCustomHex = iHexToCustomFormat(hexes[hexes.length - 1].hex, hexes[hexes.length - 1].boardId);
customHexes.push(lastCustomHex);
if (!lastCustomHex.endsWith(eofNlRecord)) {
customHexes.push(eofNlRecord);
}
return customHexes.join('');
}
/**
* Checks if the provided hex string is a Universal Hex.
*
* Very simple test only checking for the opening Extended Linear Address and
* Block Start records.
*
* The string is manually iterated as this method can be x20 faster than
* breaking the string into records and checking their types with the ihex
* functions.
*
* @param hexStr Hex string to check
* @return True if the hex is an Universal Hex.
*/
function isUniversalHex(hexStr) {
// Check the beginning of the Extended Linear Address record
var elaRecordBeginning = ':02000004';
if (hexStr.slice(0, elaRecordBeginning.length) !== elaRecordBeginning) {
return false;
}
// Find the index for the next record, as we have unknown line endings
var i = elaRecordBeginning.length;
while (hexStr[++i] !== ':' && i < ihex.MAX_RECORD_STR_LEN + 3)
;
// Check the beginning of the Block Start record
var blockStartBeginning = ':0400000A';
if (hexStr.slice(i, i + blockStartBeginning.length) !== blockStartBeginning) {
return false;
}
return true;
}
/**
* Checks if the provided array of hex records form part of a Universal Hex.
*
* @param records Array of hex records to check.
* @return True if the records belong to a Universal Hex.
*/
function isUniversalHexRecords(records) {
return (ihex.getRecordType(records[0]) === ihex.RecordType.ExtendedLinearAddress &&
ihex.getRecordType(records[1]) === ihex.RecordType.BlockStart &&
ihex.getRecordType(records[records.length - 1]) ===
ihex.RecordType.EndOfFile);
}
/**
* Checks if the array of records belongs to an Intel Hex file from MakeCode for
* micro:bit V1.
*
* @param records Array of hex records to check.
* @return True if the records belong to a MakeCode hex file for micro:bit V1.
*/
function isMakeCodeForV1HexRecords(records) {
var i = records.indexOf(ihex.endOfFileRecord());
if (i === records.length - 1) {
// A MakeCode v0 hex file will place the metadata in RAM before the EoF
while (--i > 0) {
if (records[i] === ihex.extLinAddressRecord(0x20000000)) {
return true;
}
}
}
while (++i < records.length) {
// Other data records used to store the MakeCode project metadata (v2 and v3)
if (ihex.getRecordType(records[i]) === ihex.RecordType.OtherData) {
return true;
}
// In MakeCode v1 metadata went to RAM memory space 0x2000_0000
if (records[i] === ihex.extLinAddressRecord(0x20000000)) {
return true;
}
}
return false;
}
/**
* Checks if the Hex string is an Intel Hex file from MakeCode for micro:bit V1.
*
* @param hexStr Hex string to check
* @return True if the hex is an Universal Hex.
*/
function isMakeCodeForV1Hex(hexStr) {
return isMakeCodeForV1HexRecords(ihex.iHexToRecordStrs(hexStr));
}
/**
* Separates a Universal Hex into its individual Intel Hexes.
*
* @param universalHexStr Universal Hex string with the Universal Hex.
* @returns An array of object with boardId and hex keys.
*/
function separateUniversalHex(universalHexStr) {
var records = ihex.iHexToRecordStrs(universalHexStr);
if (!records.length)
throw new Error('Empty Universal Hex.');
if (!isUniversalHexRecords(records)) {
throw new Error('Universal Hex format invalid.');
}
var passThroughRecords = [
ihex.RecordType.Data,
ihex.RecordType.EndOfFile,
ihex.RecordType.ExtendedSegmentAddress,
ihex.RecordType.StartSegmentAddress,
];
// Initialise the structure to hold the different hexes
var hexes = {};
var currentBoardId = 0;
for (var i = 0; i < records.length; i++) {
var record = records[i];
var recordType = ihex.getRecordType(record);
if (passThroughRecords.includes(recordType)) {
hexes[currentBoardId].hex.push(record);
}
else if (recordType === ihex.RecordType.CustomData) {
hexes[currentBoardId].hex.push(ihex.convertRecordTo(record, ihex.RecordType.Data));
}
else if (recordType === ihex.RecordType.ExtendedLinearAddress) {
// Extended Linear Address can be found as the start of a new block
// No need to check array size as it's confirmed hex ends with an EoF
var nextRecord = records[i + 1];
if (ihex.getRecordType(nextRecord) === ihex.RecordType.BlockStart) {
// Processes the Block Start record (only first 2 bytes for Board ID)
var blockStartData = ihex.getRecordData(nextRecord);
if (blockStartData.length !== 4) {
throw new Error("Block Start record invalid: " + nextRecord);
}
currentBoardId = (blockStartData[0] << 8) + blockStartData[1];
hexes[currentBoardId] = hexes[currentBoardId] || {
boardId: currentBoardId,
lastExtAdd: record,
hex: [record],
};
i++;
}
if (hexes[currentBoardId].lastExtAdd !== record) {
hexes[currentBoardId].lastExtAdd = record;
hexes[currentBoardId].hex.push(record);
}
}
}
// Form the return object with the same format as createUniversalHex() input
var returnArray = [];
Object.keys(hexes).forEach(function (boardId) {
// Ensure all hexes (and not just the last) contain the EoF record
var hex = hexes[boardId].hex;
if (hex[hex.length - 1] !== ihex.endOfFileRecord()) {
hex[hex.length] = ihex.endOfFileRecord();
}
returnArray.push({
boardId: hexes[boardId].boardId,
hex: hex.join('\n') + '\n',
});
});
return returnArray;
}
export { microbitBoardId, iHexToCustomFormatBlocks, iHexToCustomFormatSection, createUniversalHex, separateUniversalHex, isUniversalHex, isMakeCodeForV1Hex, };
//# sourceMappingURL=universal-hex.js.map