Skip to content

Commit

Permalink
Improve file browsing (mtime, navigate back).
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerivec committed Oct 30, 2024
1 parent 5f94424 commit f097bd2
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 99 deletions.
78 changes: 29 additions & 49 deletions src/commands/bootloader/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -96,26 +93,6 @@ export default class Bootloader extends Command {
return this.exit(0)
}

private async downloadFirmware(url: string): Promise<Buffer | undefined> {
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<boolean> {
const answer = await select<-1 | BootloaderMenu>({
choices: [
Expand All @@ -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
Expand All @@ -155,11 +132,31 @@ export default class Bootloader extends Command {
return gecko.navigate(answer, firmware)
}

private async downloadFirmware(url: string): Promise<Buffer | undefined> {
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<Buffer | undefined> {
const enum FirmwareSource {
PRE_DEFINED = 0,
URL = 1,
DATA_FOLDER = 2,
FILE = 2,
}
const firmwareSource = await select<FirmwareSource>({
choices: [
Expand All @@ -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',
})
Expand Down Expand Up @@ -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<string>({
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)
}
}
}
Expand Down
25 changes: 4 additions & 21 deletions src/commands/router/index.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -597,27 +597,10 @@ export default class Router extends Command {
}

private async menuRunScript(): Promise<boolean> {
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<string>({
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) {
Expand Down
22 changes: 13 additions & 9 deletions src/utils/bootloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AdapterModel> = ['Sonoff ZBDongle-E', 'Sonoff ZBDongle-E - ROUTER']
const ALWAYS_FORCE_RESET_ADAPTERS: ReadonlyArray<(typeof FORCE_RESET_SUPPORT_ADAPTERS)[number]> = ['Sonoff ZBDongle-E - ROUTER']

Expand Down Expand Up @@ -238,7 +239,7 @@ export class GeckoBootloader extends EventEmitter<GeckoBootloaderEventMap> {
}
}

public async validateFirmware(firmware: Buffer | undefined, supportedVersionsRegex: RegExp): Promise<FirmwareValidation> {
public async validateFirmware(firmware: Buffer | undefined): Promise<FirmwareValidation> {
if (!firmware) {
logger.error(`Cannot proceed without a firmware file.`, NS)
return FirmwareValidation.INVALID
Expand Down Expand Up @@ -293,15 +294,18 @@ export class GeckoBootloader extends EventEmitter<GeckoBootloaderEventMap> {

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({
Expand Down
40 changes: 20 additions & 20 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -96,13 +96,14 @@ export const loadStackConfig = (): typeof DEFAULT_STACK_CONFIG => {
}

export const browseToFile = async (message: string, defaultValue: string, toWrite: boolean = false): Promise<string> => {
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<number>({ choices, message })
const pathOpt = await select<number>({
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) {
Expand All @@ -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<string>({ choices: fileChoices, message })
}

filepath = join(
DATA_FOLDER,
await select<string>({
choices: fileChoices,
message,
}),
)
filepath = chosenFile === '-1' ? await browseToFile(message, defaultValue, toWrite) : join(DATA_FOLDER, chosenFile)

break
}
Expand Down

0 comments on commit f097bd2

Please sign in to comment.