Skip to content

Commit 9e0d45b

Browse files
AlCalzoneCopilot
andauthored
fix: download firmware releases from NabuCasa/silabs-firmware-builder releases branch (#49)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 8e7b3f9 commit 9e0d45b

File tree

3 files changed

+144
-52
lines changed

3 files changed

+144
-52
lines changed

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"zwave-js": "^15.21.1"
4343
},
4444
"dependencies": {
45+
"@noble/hashes": "^2.0.1",
4546
"improv-wifi-serial-sdk": "^2.5.0"
4647
}
4748
}

src/lib/firmware-download.ts

Lines changed: 130 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { type BytesView } from "@zwave-js/shared";
2+
import { sha3_256 } from "@noble/hashes/sha3.js";
3+
import { bytesToHex } from "@noble/hashes/utils.js";
24

35
/**
4-
* Utility functions for downloading and verifying Z-Wave firmware from GitHub
6+
* Utility functions for downloading and verifying Z-Wave firmware
57
*/
68

79
export interface FirmwareDownloadResult {
@@ -26,84 +28,136 @@ export class FirmwareDownloadError extends Error {
2628

2729
interface GitHubRelease {
2830
tag_name: string;
29-
assets: GitHubAsset[];
31+
draft: boolean;
32+
prerelease: boolean;
3033
}
3134

32-
interface GitHubAsset {
33-
name: string;
34-
browser_download_url: string;
35-
digest?: string;
35+
interface FirmwareManifest {
36+
metadata: {
37+
created_at: string;
38+
};
39+
firmwares: FirmwareManifestEntry[];
40+
}
41+
42+
interface FirmwareManifestEntry {
43+
filename: string;
44+
checksum: string;
45+
size: number;
3646
}
3747

48+
const SILABS_FIRMWARE_REPO = 'NabuCasa/silabs-firmware-builder';
49+
const RELEASES_BRANCH = 'releases';
50+
const RELEASES_LATEST_API_URL = `https://api.github.com/repos/${SILABS_FIRMWARE_REPO}/releases/latest`;
51+
const RELEASES_API_URL = `https://api.github.com/repos/${SILABS_FIRMWARE_REPO}/releases?per_page=30`;
52+
const RELEASES_BRANCH_RAW_BASE_URL =
53+
`https://raw.githubusercontent.com/${SILABS_FIRMWARE_REPO}/${RELEASES_BRANCH}`;
54+
const ZWA2_FIRMWARE_PREFIX = 'zwa2_controller';
55+
3856
/**
39-
* Downloads the latest Z-Wave firmware from the GitHub repository
57+
* Downloads the latest Z-Wave firmware from silabs-firmware-builder release manifests
4058
* @returns Promise resolving to firmware file name and data
4159
* @throws FirmwareDownloadError if download or verification fails
4260
*/
4361
export async function downloadLatestFirmware(): Promise<FirmwareDownloadResult> {
4462
try {
45-
// Fetch the latest release information
46-
const releaseResponse = await fetch(
47-
'https://api.github.com/repos/NabuCasa/zwave-firmware/releases/latest'
48-
);
63+
let latestRelease: GitHubRelease | undefined;
64+
const latestReleaseResponse = await fetch(RELEASES_LATEST_API_URL);
65+
if (latestReleaseResponse.ok) {
66+
latestRelease = await latestReleaseResponse.json();
67+
}
68+
69+
const releaseResponse = await fetch(RELEASES_API_URL);
4970

50-
if (!releaseResponse.ok) {
71+
if (!releaseResponse.ok && !latestRelease) {
5172
throw new FirmwareDownloadError(
52-
`Failed to fetch release information: ${releaseResponse.status} ${releaseResponse.statusText}`
73+
`Failed to fetch release list: ${releaseResponse.status} ${releaseResponse.statusText}`
5374
);
5475
}
5576

56-
const release: GitHubRelease = await releaseResponse.json();
57-
58-
// Find the GBL file in the assets
59-
const gblAsset = release.assets.find(asset =>
60-
asset.name.toLowerCase().endsWith('.gbl')
61-
);
77+
const releases: GitHubRelease[] = releaseResponse.ok
78+
? await releaseResponse.json()
79+
: [];
80+
const stableReleases = releases.filter(release => !release.draft && !release.prerelease);
6281

63-
if (!gblAsset) {
64-
throw new FirmwareDownloadError(
65-
`No GBL firmware file found in release ${release.tag_name}`
66-
);
82+
if (
83+
latestRelease &&
84+
!stableReleases.some(release => release.tag_name === latestRelease.tag_name)
85+
) {
86+
stableReleases.unshift(latestRelease);
6787
}
6888

69-
// Extract expected checksum from the digest property
70-
let expectedChecksum: string | null = null;
71-
if (gblAsset.digest) {
72-
// The digest format is "sha256:hash"
73-
const digestMatch = gblAsset.digest.match(/^sha256:([a-fA-F0-9]{64})$/);
74-
if (digestMatch) {
75-
expectedChecksum = digestMatch[1];
76-
}
89+
if (stableReleases.length === 0) {
90+
throw new FirmwareDownloadError('No stable releases found in firmware repository');
7791
}
7892

79-
// Download the firmware file through a CORS proxy
80-
// GitHub doesn't provide CORS headers for release downloads, so we need a proxy
81-
const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(gblAsset.browser_download_url)}`;
82-
const firmwareResponse = await fetch(proxyUrl);
93+
let lastReleaseError: unknown;
8394

84-
if (!firmwareResponse.ok) {
85-
throw new FirmwareDownloadError(
86-
`Failed to download firmware: ${firmwareResponse.status} ${firmwareResponse.statusText}`
87-
);
88-
}
95+
for (const release of stableReleases) {
96+
try {
97+
const manifestUrl = `${RELEASES_BRANCH_RAW_BASE_URL}/${release.tag_name}/manifest.json`;
8998

90-
const firmwareArrayBuffer = await firmwareResponse.arrayBuffer();
91-
const firmwareData = new Uint8Array(firmwareArrayBuffer);
99+
const manifestResponse = await fetch(manifestUrl);
100+
if (!manifestResponse.ok) {
101+
if (manifestResponse.status === 404) {
102+
continue;
103+
}
92104

93-
// Verify checksum if available
94-
if (expectedChecksum) {
95-
const actualChecksum = await calculateSHA256(firmwareData);
96-
if (actualChecksum.toLowerCase() !== expectedChecksum.toLowerCase()) {
97-
throw new FirmwareDownloadError(
98-
`Checksum verification failed. Expected: ${expectedChecksum}, Got: ${actualChecksum}`
105+
throw new FirmwareDownloadError(
106+
`Failed to fetch manifest for release ${release.tag_name}: ${manifestResponse.status} ${manifestResponse.statusText}`
107+
);
108+
}
109+
110+
const manifest: FirmwareManifest = await manifestResponse.json();
111+
const firmwareEntry = manifest.firmwares.find(
112+
firmware =>
113+
firmware.filename.startsWith(ZWA2_FIRMWARE_PREFIX) &&
114+
firmware.filename.toLowerCase().endsWith('.gbl')
99115
);
116+
117+
if (!firmwareEntry) {
118+
continue;
119+
}
120+
121+
const firmwareUrl = `${RELEASES_BRANCH_RAW_BASE_URL}/${release.tag_name}/${firmwareEntry.filename}`;
122+
const firmwareResponse = await fetch(firmwareUrl);
123+
124+
if (!firmwareResponse.ok) {
125+
throw new FirmwareDownloadError(
126+
`Failed to download firmware from ${release.tag_name}: ${firmwareResponse.status} ${firmwareResponse.statusText}`
127+
);
128+
}
129+
130+
const firmwareArrayBuffer = await firmwareResponse.arrayBuffer();
131+
const firmwareData = new Uint8Array(firmwareArrayBuffer);
132+
133+
if (firmwareEntry.size !== firmwareData.length) {
134+
throw new FirmwareDownloadError(
135+
`Firmware size verification failed for ${firmwareEntry.filename}. Expected: ${firmwareEntry.size}, Got: ${firmwareData.length}`
136+
);
137+
}
138+
139+
await verifyChecksum(firmwareData, firmwareEntry.checksum);
140+
141+
return {
142+
fileName: firmwareEntry.filename,
143+
data: firmwareData,
144+
};
145+
} catch (error) {
146+
lastReleaseError = error;
147+
continue;
100148
}
101149
}
102150

103-
return {
104-
fileName: gblAsset.name,
105-
data: firmwareData
106-
};
151+
if (lastReleaseError) {
152+
throw new FirmwareDownloadError(
153+
`Unable to download ${ZWA2_FIRMWARE_PREFIX} firmware from available releases`,
154+
lastReleaseError
155+
);
156+
}
157+
158+
throw new FirmwareDownloadError(
159+
`No ${ZWA2_FIRMWARE_PREFIX} firmware found in available release manifests`
160+
);
107161

108162
} catch (error) {
109163
if (error instanceof FirmwareDownloadError) {
@@ -137,3 +191,27 @@ async function calculateSHA256(data: Uint8Array): Promise<string> {
137191
const hashArray = Array.from(new Uint8Array(hashBuffer));
138192
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
139193
}
194+
195+
async function verifyChecksum(data: Uint8Array, checksum: string): Promise<void> {
196+
const [algorithm, expectedHash] = checksum.split(':', 2);
197+
198+
if (!algorithm || !expectedHash) {
199+
throw new FirmwareDownloadError(`Invalid checksum format: ${checksum}`);
200+
}
201+
202+
let actualHash: string;
203+
204+
if (algorithm.toLowerCase() === 'sha256') {
205+
actualHash = await calculateSHA256(data);
206+
} else if (algorithm.toLowerCase() === 'sha3-256') {
207+
actualHash = bytesToHex(sha3_256(data));
208+
} else {
209+
throw new FirmwareDownloadError(`Unsupported checksum algorithm: ${algorithm}`);
210+
}
211+
212+
if (actualHash.toLowerCase() !== expectedHash.toLowerCase()) {
213+
throw new FirmwareDownloadError(
214+
`Checksum verification failed. Expected: ${expectedHash}, Got: ${actualHash}`
215+
);
216+
}
217+
}

0 commit comments

Comments
 (0)