diff --git a/src/commands/bootloader/index.ts b/src/commands/bootloader/index.ts index 2c35639..511eb5a 100644 --- a/src/commands/bootloader/index.ts +++ b/src/commands/bootloader/index.ts @@ -1,19 +1,16 @@ -import { readdirSync, readFileSync } from 'node:fs' -import { join } from 'node:path' +import { readFileSync } from 'node:fs' import { input, select } from '@inquirer/prompts' import { Command } from '@oclif/core' import { Presets, SingleBar } from 'cli-progress' -import { DATA_FOLDER, logger } from '../../index.js' +import { DEFAULT_FIRMWARE_GBL_PATH, logger } from '../../index.js' import { BootloaderEvent, BootloaderMenu, GeckoBootloader } from '../../utils/bootloader.js' import { FirmwareValidation } from '../../utils/enums.js' import { FIRMWARE_LINKS } from '../../utils/firmware-links.js' import { getPortConf } from '../../utils/port.js' import { AdapterModel, FirmwareVariant } from '../../utils/types.js' - -const SUPPORTED_VERSIONS_REGEX = /(7\.4\.\d\.\d)|(8\.0\.\d\.\d)/ -const FIRMWARE_EXT = '.gbl' +import { browseToFile } from '../../utils/utils.js' const clearNVM3SonoffZBDongleE: () => Buffer = () => { const start = 'eb17a603080000000000000300000000f40a0af41c00000000000000000000000000000000000000000000000000000000000000fd0303fd0480000000600b00' @@ -96,26 +93,6 @@ export default class Bootloader extends Command { return this.exit(0) } - private async downloadFirmware(url: string): Promise { - 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 - } - private async navigateMenu(gecko: GeckoBootloader): Promise { const answer = await select<-1 | BootloaderMenu>({ choices: [ @@ -141,7 +118,7 @@ export default class Bootloader extends Command { while (validFirmware !== FirmwareValidation.VALID) { firmware = await this.selectFirmware(gecko) - validFirmware = await gecko.validateFirmware(firmware, SUPPORTED_VERSIONS_REGEX) + validFirmware = await gecko.validateFirmware(firmware) if (validFirmware === FirmwareValidation.CANCELLED) { return false @@ -155,11 +132,31 @@ export default class Bootloader extends Command { return gecko.navigate(answer, firmware) } + private async downloadFirmware(url: string): Promise { + 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 + } + private async selectFirmware(gecko: GeckoBootloader): Promise { const enum FirmwareSource { PRE_DEFINED = 0, URL = 1, - DATA_FOLDER = 2, + FILE = 2, } const firmwareSource = await select({ choices: [ @@ -169,7 +166,7 @@ export default class Bootloader extends Command { disabled: gecko.adapterModel === undefined, }, { name: 'Provide URL', value: FirmwareSource.URL }, - { name: `Select file in data folder (${DATA_FOLDER})`, value: FirmwareSource.DATA_FOLDER }, + { name: `Browse to file`, value: FirmwareSource.FILE }, ], message: 'Firmware Source', }) @@ -239,27 +236,10 @@ export default class Bootloader extends Command { return this.downloadFirmware(url) } - case FirmwareSource.DATA_FOLDER: { - const files = readdirSync(DATA_FOLDER) - const fileChoices = [] - - for (const file of files) { - if (file.endsWith(FIRMWARE_EXT)) { - fileChoices.push({ name: file, value: file }) - } - } - - if (fileChoices.length === 0) { - logger.error(`Found no firmware GBL file in '${DATA_FOLDER}'.`) - return this.exit(1) - } - - const firmwareFile = await select({ - choices: fileChoices, - message: 'Firmware file', - }) + case FirmwareSource.FILE: { + const firmwareFile = await browseToFile('Firmware file', DEFAULT_FIRMWARE_GBL_PATH) - return readFileSync(join(DATA_FOLDER, firmwareFile)) + return readFileSync(firmwareFile) } } } diff --git a/src/commands/router/index.ts b/src/commands/router/index.ts index 84ed494..d121d63 100644 --- a/src/commands/router/index.ts +++ b/src/commands/router/index.ts @@ -1,4 +1,4 @@ -import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' +import { existsSync, readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { pathToFileURL } from 'node:url' @@ -36,7 +36,7 @@ import { EUI64, NodeId, PanId } from 'zigbee-herdsman/dist/zspec/tstypes.js' import { DataType } from 'zigbee-herdsman/dist/zspec/zcl/index.js' import { BuffaloZdo } from 'zigbee-herdsman/dist/zspec/zdo/buffaloZdo.js' -import { DATA_FOLDER, DEFAULT_ROUTER_TOKENS_BACKUP_PATH, logger } from '../../index.js' +import { DATA_FOLDER, DEFAULT_ROUTER_SCRIPT_MJS_PATH, DEFAULT_ROUTER_TOKENS_BACKUP_PATH, logger } from '../../index.js' import { APPLICATION_ZDO_SEQUENCE_MASK, DEFAULT_APS_OPTIONS, DEFAULT_ZDO_REQUEST_RADIUS } from '../../utils/consts.js' import { emberFullVersion, @@ -597,27 +597,10 @@ export default class Router extends Command { } private async menuRunScript(): Promise { - const files = readdirSync(DATA_FOLDER) - const fileChoices = [] - - for (const file of files) { - if (file.endsWith('.mjs')) { - fileChoices.push({ name: file, value: file }) - } - } - - if (fileChoices.length === 0) { - logger.error(`Found no mjs file in '${DATA_FOLDER}'.`) - return false - } - - const jsFile = await select({ - choices: fileChoices, - message: 'File to run', - }) + const jsFile = await browseToFile('File to run', DEFAULT_ROUTER_SCRIPT_MJS_PATH) try { - const scriptToRun = await import(pathToFileURL(join(DATA_FOLDER, jsFile)).toString()) + const scriptToRun = await import(pathToFileURL(jsFile).toString()) scriptToRun.default(this, logger) } catch (error) { diff --git a/src/utils/bootloader.ts b/src/utils/bootloader.ts index 64432ab..9624bcd 100644 --- a/src/utils/bootloader.ts +++ b/src/utils/bootloader.ts @@ -71,6 +71,7 @@ const GBL_END_TAG = Buffer.from([0xfc, 0x04, 0x04, 0xfc]) const GBL_METADATA_TAG = Buffer.from([0xf6, 0x08, 0x08, 0xf6]) const VALID_FIRMWARE_CRC32 = 558161692 +const SUPPORTED_VERSIONS_REGEX = /(7\.4\.\d\.\d)|(8\.0\.\d\.\d)/ const FORCE_RESET_SUPPORT_ADAPTERS: ReadonlyArray = ['Sonoff ZBDongle-E', 'Sonoff ZBDongle-E - ROUTER'] const ALWAYS_FORCE_RESET_ADAPTERS: ReadonlyArray<(typeof FORCE_RESET_SUPPORT_ADAPTERS)[number]> = ['Sonoff ZBDongle-E - ROUTER'] @@ -238,7 +239,7 @@ export class GeckoBootloader extends EventEmitter { } } - public async validateFirmware(firmware: Buffer | undefined, supportedVersionsRegex: RegExp): Promise { + public async validateFirmware(firmware: Buffer | undefined): Promise { if (!firmware) { logger.error(`Cannot proceed without a firmware file.`, NS) return FirmwareValidation.INVALID @@ -293,15 +294,18 @@ export class GeckoBootloader extends EventEmitter { logger.info(`Firmware file metadata: ${JSON.stringify(recdMetadata)}`, NS) - if (!TCP_REGEX.test(this.portConf.path) && recdMetadata.baudrate !== this.portConf.baudRate) { - logger.warning( - `Firmware file baudrate ${recdMetadata.baudrate} differs from your current port configuration of ${this.portConf.baudRate}.`, - NS, - ) - } + // checks irrelevant for router firmware + if (!recdMetadata.fw_type.includes('router')) { + if (!TCP_REGEX.test(this.portConf.path) && recdMetadata.baudrate !== this.portConf.baudRate) { + logger.warning( + `Firmware file baudrate ${recdMetadata.baudrate} differs from your current port configuration of ${this.portConf.baudRate}.`, + NS, + ) + } - if (!recdMetadata.ezsp_version || !supportedVersionsRegex.test(recdMetadata.ezsp_version)) { - logger.warning(`Firmware file version is not recognized as currently supported by Zigbee2MQTT ember driver.`, NS) + if (!recdMetadata.ezsp_version || !SUPPORTED_VERSIONS_REGEX.test(recdMetadata.ezsp_version)) { + logger.warning(`Firmware file version is not recognized as currently supported by Zigbee2MQTT ember driver.`, NS) + } } const proceed = await confirm({ diff --git a/src/utils/utils.ts b/src/utils/utils.ts index e40875d..f8f67e0 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,4 +1,4 @@ -import { existsSync, readdirSync, readFileSync, renameSync } from 'node:fs' +import { existsSync, readdirSync, readFileSync, renameSync, statSync } from 'node:fs' import { dirname, extname, join } from 'node:path' import { input, select } from '@inquirer/prompts' @@ -96,13 +96,14 @@ export const loadStackConfig = (): typeof DEFAULT_STACK_CONFIG => { } export const browseToFile = async (message: string, defaultValue: string, toWrite: boolean = false): Promise => { - const choices: { name: string; value: number }[] = [ - { name: `Use default (${defaultValue})`, value: 0 }, - { name: `Enter path manually`, value: 1 }, - { name: `Select in data folder (${DATA_FOLDER})`, value: 2 }, - ] - - const pathOpt = await select({ choices, message }) + const pathOpt = await select({ + choices: [ + { name: `Use default (${defaultValue})`, value: 0 }, + { name: `Enter path manually`, value: 1 }, + { name: `Select in data folder (${DATA_FOLDER})`, value: 2 }, + ], + message, + }) let filepath: string = defaultValue switch (pathOpt) { @@ -119,26 +120,25 @@ export const browseToFile = async (message: string, defaultValue: string, toWrit case 2: { const files = readdirSync(DATA_FOLDER) - const fileChoices = [] + const fileChoices = [{ name: `Go back`, value: '-1' }] for (const file of files) { if (extname(file) === extname(defaultValue)) { - fileChoices.push({ name: file, value: file }) + const { mtime } = statSync(join(DATA_FOLDER, file)) + + fileChoices.push({ name: `[${mtime.toISOString()}] ${file}`, value: file }) } } - if (fileChoices.length === 0) { - logger.error(`Found no file in '${DATA_FOLDER}'. Using default '${defaultValue}'.`) - break + let chosenFile = '-1' + + if (fileChoices.length === 1) { + logger.error(`Found no file in '${DATA_FOLDER}'.`) + } else { + chosenFile = await select({ choices: fileChoices, message }) } - filepath = join( - DATA_FOLDER, - await select({ - choices: fileChoices, - message, - }), - ) + filepath = chosenFile === '-1' ? await browseToFile(message, defaultValue, toWrite) : join(DATA_FOLDER, chosenFile) break }