ember-zli
Version:
Interact with EmberZNet-based adapters using zigbee-herdsman 'ember' driver
220 lines (219 loc) • 9.7 kB
JavaScript
import { readFileSync } from "node:fs";
import { confirm, input, select } from "@inquirer/prompts";
import { Command } from "@oclif/core";
import { Presets, SingleBar } from "cli-progress";
import { DEFAULT_FIRMWARE_GBL_PATH, logger } from "../../index.js";
import { BootloaderEvent, BootloaderMenu, GeckoBootloader } from "../../utils/bootloader.js";
import { ADAPTER_MODELS, PRE_DEFINED_FIRMWARE_LINKS_URL } from "../../utils/consts.js";
import { FirmwareValidation } from "../../utils/enums.js";
import { getPortConf } from "../../utils/port.js";
import { browseToFile, fetchJson } from "../../utils/utils.js";
export default class Bootloader extends Command {
static args = {};
static description = "Interact with the Gecko bootloader in the adapter.";
static examples = ["<%= config.bin %> <%= command.id %>"];
async run() {
const portConf = await getPortConf();
logger.debug(`Using port conf: ${JSON.stringify(portConf)}`);
const adapterModelChoices = [{ name: "Not in this list", value: undefined }];
for (const model of ADAPTER_MODELS) {
adapterModelChoices.push({ name: model, value: model });
}
const adapterModel = await select({
choices: adapterModelChoices,
message: "Adapter model",
});
const gecko = new GeckoBootloader(portConf, adapterModel);
const progressBar = new SingleBar({ clearOnComplete: true, format: "{bar} {percentage}%" }, Presets.shades_classic);
gecko.on(BootloaderEvent.FAILED, () => {
this.exit(1);
});
gecko.on(BootloaderEvent.CLOSED, () => {
this.exit(0);
});
gecko.on(BootloaderEvent.UPLOAD_START, () => {
progressBar.start(100, 0);
});
gecko.on(BootloaderEvent.UPLOAD_STOP, () => {
progressBar.stop();
});
gecko.on(BootloaderEvent.UPLOAD_PROGRESS, (percent) => {
progressBar.update(percent);
});
await gecko.connect();
let exit = false;
while (!exit) {
exit = await this.navigateMenu(gecko);
}
await gecko.transport.close(false);
return this.exit(0);
}
async navigateMenu(gecko) {
const answer = await select({
choices: [
{ name: "Get info", value: BootloaderMenu.INFO },
{ name: "Update firmware", value: BootloaderMenu.UPLOAD_GBL },
{
name: "Clear NVM3 (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#nvm3-clear)",
value: BootloaderMenu.CLEAR_NVM3,
disabled: !gecko.adapterModel,
},
{
name: "Clear APP (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#app-clear)",
value: BootloaderMenu.CLEAR_APP,
disabled: !gecko.adapterModel,
},
{ name: "Exit bootloader (run firmware)", value: BootloaderMenu.RUN },
{ name: "Force close", value: -1 },
],
message: "Menu",
});
if (answer === -1) {
logger.warning("Force closing... You may need to unplug/replug the adapter.");
return true;
}
let firmware;
if (answer === BootloaderMenu.UPLOAD_GBL) {
let validFirmware = FirmwareValidation.INVALID;
while (validFirmware !== FirmwareValidation.VALID) {
firmware = await this.selectFirmware(gecko);
validFirmware = await gecko.validateFirmware(firmware);
if (validFirmware === FirmwareValidation.CANCELLED) {
return false;
}
}
}
else if (answer === BootloaderMenu.CLEAR_NVM3) {
const confirmed = await confirm({
default: false,
message: `Confirm adapter is: ${gecko.adapterModel}?`,
});
if (!confirmed) {
logger.warning("Cancelled NVM3 clearing.");
return false;
}
const nvm3Size = await select({
choices: [
{ name: "32768", value: 32768 },
{ name: "40960", value: 40960 },
],
message: "NVM3 Size (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#nvm3-clear)",
});
const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL);
const variant = nvm3Size === 32768 ? "nvm3_32768_clear" : "nvm3_40960_clear";
firmware = await this.downloadFirmware(firmwareLinks[variant][gecko.adapterModel]);
}
else if (answer === BootloaderMenu.CLEAR_APP) {
const confirmed = await confirm({
default: false,
message: `Confirm adapter is: ${gecko.adapterModel}?`,
});
if (!confirmed) {
logger.warning("Cancelled APP clearing.");
return false;
}
const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL);
firmware = await this.downloadFirmware(firmwareLinks.app_clear[gecko.adapterModel]);
}
return await gecko.navigate(answer, firmware);
}
async downloadFirmware(url) {
try {
logger.info(`Downloading firmware from ${url}.`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
}
catch (error) {
logger.error(`Failed to download firmware file from ${url} with error ${error}.`);
}
return undefined;
}
async selectFirmware(gecko) {
let FirmwareSource;
(function (FirmwareSource) {
FirmwareSource[FirmwareSource["PRE_DEFINED"] = 0] = "PRE_DEFINED";
FirmwareSource[FirmwareSource["URL"] = 1] = "URL";
FirmwareSource[FirmwareSource["FILE"] = 2] = "FILE";
})(FirmwareSource || (FirmwareSource = {}));
const firmwareSource = await select({
choices: [
{
name: `Use pre-defined firmware (using ${PRE_DEFINED_FIRMWARE_LINKS_URL})`,
value: FirmwareSource.PRE_DEFINED,
disabled: gecko.adapterModel === undefined,
},
{ name: "Provide URL", value: FirmwareSource.URL },
{ name: "Browse to file", value: FirmwareSource.FILE },
],
message: "Firmware source",
});
switch (firmwareSource) {
case FirmwareSource.PRE_DEFINED: {
const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL);
// valid adapterModel since select option disabled if not
const official = firmwareLinks.official[gecko.adapterModel];
const darkxst = firmwareLinks.darkxst[gecko.adapterModel];
const nerivec = firmwareLinks.nerivec[gecko.adapterModel];
const nerivecPreRelease = firmwareLinks.nerivec_pre_release[gecko.adapterModel];
const firmwareVariant = await select({
choices: [
{
name: "Latest from manufacturer",
value: "official",
description: official,
disabled: !official,
},
{
name: "Latest from @darkxst",
value: "darkxst",
description: darkxst,
disabled: !darkxst,
},
{
name: "Latest from @Nerivec",
value: "nerivec",
description: nerivec,
disabled: !nerivec,
},
{
name: "Latest pre-release from @Nerivec",
value: "nerivec_pre_release",
description: nerivecPreRelease,
disabled: !nerivecPreRelease,
},
],
message: "Firmware version",
});
const firmwareUrl = firmwareLinks[firmwareVariant][gecko.adapterModel];
// just in case (and to pass linter)
if (!firmwareUrl) {
return undefined;
}
return await this.downloadFirmware(firmwareUrl);
}
case FirmwareSource.URL: {
const url = await input({
message: "Enter the URL to the firmware file",
validate(value) {
try {
new URL(value);
return true;
}
catch {
return false;
}
},
});
return await this.downloadFirmware(url);
}
case FirmwareSource.FILE: {
const firmwareFile = await browseToFile("Firmware file", DEFAULT_FIRMWARE_GBL_PATH);
return readFileSync(firmwareFile);
}
}
}
}