Skip to content

Commit

Permalink
Manage firmware links through JSON for pre-defined.
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerivec committed Nov 8, 2024
1 parent faff443 commit c218a3b
Show file tree
Hide file tree
Showing 8 changed files with 330 additions and 322 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/update-firmware-links.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Update firmware links

on:
schedule:
- cron: '0 12 1 * *'
workflow_dispatch:

jobs:
update-fw-links:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version-file: 'package.json'

- run: npm ci
- run: npm run build
- run: npm run update-fw-links

- name: Commit changes
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add .
git commit -m "Update firmware links" || echo 'Nothing to commit'
git push
59 changes: 25 additions & 34 deletions src/commands/bootloader/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AdapterModel, FirmwareVariant, SelectChoices } from '../../utils/types.js'
import type { AdapterModel, FirmwareLinks, FirmwareVariant, SelectChoices } from '../../utils/types.js'

import { readFileSync } from 'node:fs'

Expand All @@ -8,10 +8,10 @@ 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 { FIRMWARE_LINKS } from '../../utils/firmware-links.js'
import { getPortConf } from '../../utils/port.js'
import { browseToFile } from '../../utils/utils.js'
import { browseToFile, fetchJson } from '../../utils/utils.js'

const clearNVM3SonoffZBDongleE: () => Buffer = () => {
const start = 'eb17a603080000000000000300000000f40a0af41c00000000000000000000000000000000000000000000000000000000000000fd0303fd0480000000600b00'
Expand All @@ -35,7 +35,7 @@ const clearNVM3SonoffZBDongleE: () => Buffer = () => {

const CLEAR_NVM3_BUFFERS: Partial<Record<AdapterModel, () => Buffer>> = {
'Sonoff ZBDongle-E': clearNVM3SonoffZBDongleE,
'Sonoff ZBDongle-E - ROUTER': clearNVM3SonoffZBDongleE,
'ROUTER - Sonoff ZBDongle-E': clearNVM3SonoffZBDongleE,
}

export default class Bootloader extends Command {
Expand All @@ -49,8 +49,8 @@ export default class Bootloader extends Command {

const adapterModelChoices: SelectChoices<AdapterModel | undefined> = [{ name: 'Not in this list', value: undefined }]

for (const k in FIRMWARE_LINKS.recommended) {
adapterModelChoices.push({ name: k, value: k as AdapterModel })
for (const model of ADAPTER_MODELS) {
adapterModelChoices.push({ name: model, value: model })
}

const adapterModel = await select<AdapterModel | undefined>({
Expand Down Expand Up @@ -162,63 +162,54 @@ export default class Bootloader extends Command {
const firmwareSource = await select<FirmwareSource>({
choices: [
{
name: 'Use pre-defined firmware (recommended or latest based on your adapter)',
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',
message: 'Firmware source',
})

switch (firmwareSource) {
case FirmwareSource.PRE_DEFINED: {
const firmwareLinks = await fetchJson<FirmwareLinks>(PRE_DEFINED_FIRMWARE_LINKS_URL)
// valid adapterModel since select option disabled if not
const recommended = FIRMWARE_LINKS.recommended[gecko.adapterModel!]
const latest = FIRMWARE_LINKS.latest[gecko.adapterModel!]
const official = FIRMWARE_LINKS.official[gecko.adapterModel!]
const experimental = FIRMWARE_LINKS.experimental[gecko.adapterModel!]
const recommended = firmwareLinks.latest[gecko.adapterModel!]
const official = firmwareLinks.official[gecko.adapterModel!]
const experimental = firmwareLinks.experimental[gecko.adapterModel!]
const firmwareVariant = await select<FirmwareVariant>({
choices: [
{
name: `Recommended for Zigbee2MQTT`,
value: 'recommended',
description: recommended.url
? `Version: ${recommended.version}, RTS/CTS: ${recommended.settings.rtscts}, URL: ${recommended.url}`
: undefined,
disabled: !recommended.url,
},
{
name: `Latest`,
value: 'latest',
description: latest.url
? `Version: ${latest.version}, RTS/CTS: ${latest.settings.rtscts}, URL: ${latest.url}`
: undefined,
disabled: !latest.url,
description: recommended ? recommended : undefined,
disabled: !recommended,
},
{
name: `Latest from manufacturer`,
value: 'official',
description: official.url
? `Version: ${official.version}, RTS/CTS: ${official.settings.rtscts}, URL: ${official.url}`
: undefined,
disabled: !official.url,
description: official ? official : undefined,
disabled: !official,
},
{
name: `Experimental`,
value: 'experimental',
description: experimental.url
? `Version: ${experimental.version}, RTS/CTS: ${experimental.settings.rtscts}, URL: ${experimental.url}`
: undefined,
disabled: !experimental.url,
description: experimental ? experimental : undefined,
disabled: !experimental,
},
],
message: 'Firmware version',
})
const firmwareUrl = firmwareLinks[firmwareVariant][gecko.adapterModel!]

// just in case (and to pass linter)
if (!firmwareUrl) {
return undefined
}

// valid url from choices filtering
return await this.downloadFirmware(FIRMWARE_LINKS[firmwareVariant][gecko.adapterModel!].url!)
return await this.downloadFirmware(firmwareUrl)
}

case FirmwareSource.URL: {
Expand Down
13 changes: 7 additions & 6 deletions src/utils/bootloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ 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']
const FORCE_RESET_SUPPORT_ADAPTERS: ReadonlyArray<AdapterModel> = ['Sonoff ZBDongle-E', 'ROUTER - Sonoff ZBDongle-E']
const ALWAYS_FORCE_RESET_ADAPTERS: ReadonlyArray<(typeof FORCE_RESET_SUPPORT_ADAPTERS)[number]> = ['ROUTER - Sonoff ZBDongle-E']

export enum BootloaderEvent {
FAILED = 'failed',
Expand Down Expand Up @@ -209,7 +209,7 @@ export class GeckoBootloader extends EventEmitter<GeckoBootloaderEventMap> {

const confirmed = await confirm({
default: false,
message: 'Confirm NVM3 clearing? (Cannot be undone.)',
message: 'Confirm NVM3 clearing? (Cannot be undone; will reset the adapter to factory defaults.)',
})

if (!confirmed) {
Expand All @@ -226,7 +226,7 @@ export class GeckoBootloader extends EventEmitter<GeckoBootloaderEventMap> {
switch (this.adapterModel) {
// TODO: support per adapter
case 'Sonoff ZBDongle-E':
case 'Sonoff ZBDongle-E - ROUTER': {
case 'ROUTER - Sonoff ZBDongle-E': {
await this.transport.serialSet({ dtr: false, rts: true })
await this.transport.serialSet({ dtr: true, rts: false }, 100)

Expand Down Expand Up @@ -380,7 +380,7 @@ export class GeckoBootloader extends EventEmitter<GeckoBootloaderEventMap> {
try {
await this.transport.initPort()

// on first knock only, try pattern reset if supported
// try force reset if supported (only on initial non-fail knock)
if (!fail && this.adapterModel && FORCE_RESET_SUPPORT_ADAPTERS.includes(this.adapterModel)) {
// XXX: always force reset Sonoff ZBDongle-E Router to prevent issues with EZSP 6.10.3 (can be removed once versions updated and no longer used)
const forceReset =
Expand All @@ -400,6 +400,7 @@ export class GeckoBootloader extends EventEmitter<GeckoBootloaderEventMap> {
}
} catch (error) {
logger.error(`Failed to open port: ${error}.`, NS)

await this.transport.close(false, false) // force failed below
this.emit(BootloaderEvent.FAILED)

Expand Down Expand Up @@ -459,7 +460,7 @@ export class GeckoBootloader extends EventEmitter<GeckoBootloaderEventMap> {
logger.warning(`Failed to exit bootloader and run firmware.`, NS)

if (this.adapterModel && FORCE_RESET_SUPPORT_ADAPTERS.includes(this.adapterModel)) {
logger.warning(`Failed to exit bootloader and run firmware. Trying force reset...`, NS)
logger.warning(`Trying force reset...`, NS)

await this.forceReset(true)
} else {
Expand Down
29 changes: 29 additions & 0 deletions src/utils/consts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
import type { AdapterModel } from './types.js'

import { EmberApsOption } from 'zigbee-herdsman/dist/adapter/ember/enums.js'

export const PRE_DEFINED_FIRMWARE_LINKS_URL = `https://github.com/Nerivec/ember-zli/raw/refs/heads/main/firmware-links.json`
export const ADAPTER_MODELS: ReadonlyArray<AdapterModel> = [
'Aeotec Zi-Stick (ZGA008)',
'EasyIOT ZB-GW04 v1.1',
'EasyIOT ZB-GW04 v1.2',
'Nabu Casa SkyConnect',
'Nabu Casa Yellow',
'SMLight SLZB06-M',
'SMLight SLZB07',
'SMLight SLZB07mg24',
'Sonoff ZBDongle-E',
'SparkFun MGM240p',
'TubeZB MGM24',
'TubeZB MGM24PB',
'ROUTER - Aeotec Zi-Stick (ZGA008)',
'ROUTER - EasyIOT ZB-GW04 v1.1',
'ROUTER - EasyIOT ZB-GW04 v1.2',
'ROUTER - Nabu Casa SkyConnect',
'ROUTER - Nabu Casa Yellow',
'ROUTER - SMLight SLZB06-M',
'ROUTER - SMLight SLZB07',
'ROUTER - SMLight SLZB07mg24',
'ROUTER - Sonoff ZBDongle-E',
'ROUTER - SparkFun MGM240p',
'ROUTER - TubeZB MGM24',
'ROUTER - TubeZB MGM24PB',
]
export const TCP_REGEX = /^tcp:\/\/[\w.-]+:\d+$/
export const BAUDRATES = [115200, 230400, 460800]
/** Read/write max bytes count at stream level */
Expand Down
Loading

0 comments on commit c218a3b

Please sign in to comment.