feat: keep records of identifiable stacks for firmware files (#739)

This commit is contained in:
Nerivec
2025-04-07 21:29:02 +02:00
committed by GitHub
parent e43482defb
commit 1167598f77
12 changed files with 5124 additions and 758 deletions
+2 -2
View File
@@ -358,7 +358,7 @@ export function addImageToPrev(
imageType: parsedImage.imageType,
manufacturerCode: parsedImage.manufacturerCode,
sha512: computeSHA512(firmwareBuffer),
otaHeaderString: parsedImage.otaHeaderString,
otaHeaderString: parsedImage.otaHeaderString.replaceAll("\u0000", ""),
...extraMetas,
});
}
@@ -431,7 +431,7 @@ export function addImageToBase(
imageType: parsedImage.imageType,
manufacturerCode: parsedImage.manufacturerCode,
sha512: computeSHA512(firmwareBuffer),
otaHeaderString: parsedImage.otaHeaderString,
otaHeaderString: parsedImage.otaHeaderString.replaceAll("\u0000", ""),
...extraMetas,
});
}
+221
View File
@@ -0,0 +1,221 @@
import type CoreApi from "@actions/core";
import type {Context} from "@actions/github/lib/context";
import type {Octokit} from "@octokit/rest";
import {readFileSync, writeFileSync} from "node:fs";
import {join} from "node:path";
import {BASE_INDEX_MANIFEST_FILENAME, BASE_REPO_URL, REPO_BRANCH, UPGRADE_FILE_IDENTIFIER, parseImageHeader, readManifest} from "./common.js";
enum ZigBeeStackVersion {
ZigBee2006 = 0x0000,
ZigBee2007 = 0x0001,
ZigBeePro = 0x0002,
ZigBeeIP = 0x0003,
}
type FirmwareStack = {
url: string;
modelId?: string;
stack:
| "EmberZNet" // Silabs https://github.com/SiliconLabs/simplicity_sdk or https://github.com/SiliconLabs/gecko_sdk
| "zStack" // TI https://github.com/TexasInstruments/simplelink-lowpower-f2-sdk
| "Telink" // https://github.com/telink-semi/telink_zigbee_sdk
| "NXP"
| "BitCloud" // Microchip https://github.com/Microchip-MPLAB-Harmony/wireless_zigbee
| "ZBOSS" // https://github.com/TexasInstruments/simplelink-lowpower-f3-sdk or https://github.com/nrfconnect/ncs-zigbee/ or https://github.com/nrfconnect/ncs-zigbee-r22/ or https://github.com/espressif/esp-zigbee-sdk
| "Unknown";
stackDetails: string;
/** @see ZigBeeStackVersion */
zigbeeStackVersion: string;
};
type OtaSubElement = {
tagId: number;
length: number;
data: Buffer;
};
const INDEX_STACKINFO_MANIFEST_FILENAME = "index-stackinfo.json";
const SI_GBL_HEADER_TAG = Buffer.from([0xeb, 0x17, 0xa6, 0x03]);
const SI_EBL_TAG_HEADER = 0x0;
const SI_EBL_IMAGE_SIGNATURE = 0xe350;
const SI_EBL_TAG_ENC_HEADER = 0xfb05;
const TI_OAD_IMG_ID_VAL_CC26X2R1 = Buffer.from("CC26x2R1", "utf8");
const TI_OAD_IMG_ID_VAL_CC13X2R1 = Buffer.from("CC13x2R1", "utf8");
const TI_OAD_IMG_ID_VAL_CC13X4 = Buffer.from("CC13x4 ", "utf8");
const TI_OAD_IMG_ID_VAL_CC26X3 = Buffer.from("CC26x3 ", "utf8");
const TI_OAD_IMG_ID_VAL_CC26X4 = Buffer.from("CC26x4 ", "utf8");
const TI_OAD_IMG_ID_VAL_OADIMG = Buffer.from("OAD IMG ", "utf8");
const TI_OAD_IMG_ID_VAL_CC23X0R2 = Buffer.from("CC23x0R2", "utf8");
const TL_START_UP_FLAG_WHOLE = 0x544c4e4b; // Buffer.from('KNLT', 'utf8');
const TL_SR_TAG = Buffer.from("TLSR", "utf8");
function parseSubElements(otaData: Buffer, totalImageSize: number): OtaSubElement[] {
let position = 0;
const elements: OtaSubElement[] = [];
try {
while (position < totalImageSize) {
const tagId = otaData.readUInt16LE(position);
position += 2;
const length = otaData.readUInt32LE(position);
position += 4;
const data = otaData.subarray(position, position + length);
position += length;
if (data.byteLength !== length) {
throw new Error("Invalid data byte length");
}
elements.push({tagId, length, data});
}
} catch {
/* ignore */
}
return elements;
}
export function identifyStacks(github: Octokit, core: typeof CoreApi, context: Context): void {
try {
const firmwareList: FirmwareStack[] = [];
const baseManifest = readManifest(BASE_INDEX_MANIFEST_FILENAME);
for (const meta of baseManifest) {
const filePath = decodeURIComponent(meta.url.replace(BASE_REPO_URL + REPO_BRANCH, ""));
const fileBuf = readFileSync(join(".", filePath));
const otaImage = fileBuf.subarray(fileBuf.indexOf(UPGRADE_FILE_IDENTIFIER));
const header = parseImageHeader(otaImage);
const otaData = otaImage.subarray(header.otaHeaderLength);
let stack: FirmwareStack["stack"] = "Unknown";
let stackDetails = "";
for (const {tagId, data} of parseSubElements(otaData, header.totalImageSize)) {
if (data.indexOf(SI_GBL_HEADER_TAG) === 0) {
stack = "EmberZNet";
stackDetails = "GBL";
break;
}
if (data.readUInt16BE(0) === SI_EBL_TAG_HEADER && data.readUInt16BE(6) === SI_EBL_IMAGE_SIGNATURE) {
stack = "EmberZNet";
stackDetails = "EBL";
break;
}
if (data.readUInt16BE(0) === SI_EBL_TAG_ENC_HEADER) {
stack = "EmberZNet";
stackDetails = "EBL ENC";
break;
}
if (data.indexOf(TI_OAD_IMG_ID_VAL_CC26X2R1) === 0) {
stack = "zStack";
stackDetails = "CC26x2R1";
break;
}
if (data.indexOf(TI_OAD_IMG_ID_VAL_CC13X2R1) === 0) {
stack = "zStack";
stackDetails = "CC13x2R1";
break;
}
if (data.indexOf(TI_OAD_IMG_ID_VAL_CC13X4) === 0) {
stack = "zStack";
stackDetails = "CC13x4";
break;
}
if (data.indexOf(TI_OAD_IMG_ID_VAL_CC26X3) === 0) {
stack = "zStack";
stackDetails = "CC26x3";
break;
}
if (data.indexOf(TI_OAD_IMG_ID_VAL_CC26X4) === 0) {
stack = "zStack";
stackDetails = "CC26x4";
break;
}
if (data.indexOf(TI_OAD_IMG_ID_VAL_OADIMG) === 0) {
stack = "zStack";
stackDetails = "OAD IMG";
break;
}
if (data.indexOf(TI_OAD_IMG_ID_VAL_CC23X0R2) === 0) {
stack = "zStack";
stackDetails = "CC23x0R2";
break;
}
if (data.byteLength >= 12 && data.readUInt32LE(8) === TL_START_UP_FLAG_WHOLE) {
stack = "Telink";
const tlsrIndex = data.indexOf(TL_SR_TAG);
if (tlsrIndex !== -1) {
stackDetails = data.subarray(tlsrIndex, tlsrIndex + 8).toString("utf8");
}
break;
}
if (
data.indexOf(Buffer.from("nRF", "utf8")) !== -1 ||
data.indexOf(Buffer.from("nrf5", "utf8")) !== -1 ||
data.indexOf(Buffer.from("nrf_", "utf8")) !== -1
) {
stack = "ZBOSS";
stackDetails = "Nordic (fuzzy matching)";
break;
}
core.info(`UNKNOWN ${filePath} tagId=${tagId} firstBytes=${data.subarray(0, 16).toString("hex")}`);
}
if (stack === "Unknown") {
if (header.otaHeaderString.includes("Telink")) {
stack = "Telink";
stackDetails = "(fallback matching)";
} else if (header.otaHeaderString.includes("GBL")) {
stack = "EmberZNet";
stackDetails = "GBL (fallback matching)";
} else if (header.otaHeaderString.includes("EBL")) {
stack = "EmberZNet";
stackDetails = "EBL (fallback matching)";
} /* else if () {
// stack = 'zStack';
}*/
}
firmwareList.push({
url: meta.url,
// modelId: meta.modelId,
stack,
stackDetails,
zigbeeStackVersion: ZigBeeStackVersion[header.zigbeeStackVersion],
});
}
writeFileSync(INDEX_STACKINFO_MANIFEST_FILENAME, JSON.stringify(firmwareList, undefined, 2), "utf8");
} catch (error) {
core.error((error as Error).message);
core.debug((error as Error).stack!);
}
}
// // @ts-expect-error run locally
// identifyStacks({}, console, {});
+10 -8
View File
@@ -275,7 +275,7 @@ function checkImagesAgainstManifests(github: Octokit, core: typeof CoreApi, cont
imageType: parsedImage.imageType,
manufacturerCode: parsedImage.manufacturerCode,
sha512: computeSHA512(firmwareBuffer),
otaHeaderString: parsedImage.otaHeaderString,
otaHeaderString: parsedImage.otaHeaderString.replaceAll("\u0000", ""),
});
} catch (error) {
core.error(`Removing ${firmwareFilePath}: ${error}`);
@@ -307,7 +307,7 @@ function checkImagesAgainstManifests(github: Octokit, core: typeof CoreApi, cont
imageType: parsedImage.imageType,
manufacturerCode: parsedImage.manufacturerCode,
sha512: computeSHA512(firmwareBuffer),
otaHeaderString: parsedImage.otaHeaderString,
otaHeaderString: parsedImage.otaHeaderString.replaceAll("\u0000", ""),
...extraMetas,
});
}
@@ -397,10 +397,12 @@ export async function reProcessAllImages(
// To run locally uncomment below and run with `npx tsx src/ghw_reprocess_all_images.ts`
// const core = {
// info: (msg) => console.log(msg),
// warning: (msg) => console.log(msg),
// error: (msg) => console.error(msg),
// startGroup: () => {},
// endGroup: () => {},
// debug: console.debug,
// info: console.info,
// warning: console.warn,
// error: console.error,
// startGroup: console.group,
// endGroup: console.groupEnd,
// }
// checkImagesAgainstManifests(null, core, null, false);
// // @ts-expect-error run locally
// checkImagesAgainstManifests({}, core, {}, false);
+4 -2
View File
@@ -1,5 +1,7 @@
import {readFileSync} from "node:fs";
import {parseImageHeader} from "./common.js";
import {UPGRADE_FILE_IDENTIFIER, parseImageHeader} from "./common.js";
console.log(parseImageHeader(readFileSync(process.argv[2])));
const firmwareBuffer = readFileSync(process.argv[2]);
console.log(parseImageHeader(firmwareBuffer.subarray(firmwareBuffer.indexOf(UPGRADE_FILE_IDENTIFIER))));