From fd6fb4bcda880286bbaa90936cf7a4584089ba4c Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 25 Feb 2026 16:36:03 -0500 Subject: [PATCH] Add firmware version picker to web flasher Lets users choose between the latest dev build and tagged GitHub releases. The dropdown queries the GitHub Releases API on page load and swaps firmware fetch paths between Pages-relative and release-asset URLs. CI now attaches all 4 firmware files to releases (bootloader, partitions, boot_app0, firmware) so full installs work from any release version. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release-firmware.yml | 3 + docs/flasher/index.html | 155 +++++++++++++++++++++---- 2 files changed, 138 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release-firmware.yml b/.github/workflows/release-firmware.yml index d36415b3..adde7135 100644 --- a/.github/workflows/release-firmware.yml +++ b/.github/workflows/release-firmware.yml @@ -102,6 +102,9 @@ jobs: tag_name: ${{ steps.version.outputs.VERSION }} name: ${{ steps.version.outputs.VERSION }} files: | + docs/flasher/firmware/bootloader.bin + docs/flasher/firmware/partitions.bin + docs/flasher/firmware/boot_app0.bin docs/flasher/firmware/firmware.bin generate_release_notes: true draft: true diff --git a/docs/flasher/index.html b/docs/flasher/index.html index e5f6a6ad..8ee10020 100644 --- a/docs/flasher/index.html +++ b/docs/flasher/index.html @@ -175,6 +175,44 @@ } .warning strong { color: var(--warning); } + .version-select-group { + margin-bottom: 1.5rem; + } + + .version-select-group label { + display: block; + color: var(--muted); + font-size: 0.875rem; + margin-bottom: 0.5rem; + } + + .version-select-group select { + width: 100%; + padding: 0.625rem 1rem; + background: rgba(0,0,0,0.3); + color: var(--text); + border: 1px solid rgba(255,255,255,0.15); + border-radius: 8px; + font-family: inherit; + font-size: 1rem; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%2394a3b8' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 1rem center; + } + + .version-select-group select:focus { + outline: none; + border-color: var(--primary); + } + + .version-note { + font-size: 0.8rem; + color: var(--warning); + margin-top: 0.5rem; + } + footer { text-align: center; margin-top: 2rem; @@ -202,6 +240,14 @@ +
+ + + +
+
@@ -266,18 +312,29 @@ const progressContainer = document.getElementById('progress-container'); const progressFill = document.getElementById('progress-fill'); const progressText = document.getElementById('progress-text'); + const versionSelect = document.getElementById('version-select'); + const versionNote = document.getElementById('version-note'); - const FIRMWARE_FILES = { - full: [ - { offset: 0x0, name: 'bootloader.bin', path: 'firmware/bootloader.bin' }, - { offset: 0x8000, name: 'partitions.bin', path: 'firmware/partitions.bin' }, - { offset: 0xe000, name: 'boot_app0.bin', path: 'firmware/boot_app0.bin' }, - { offset: 0x10000, name: 'firmware.bin', path: 'firmware/firmware.bin' } - ], - update: [ - { offset: 0x10000, name: 'firmware.bin', path: 'firmware/firmware.bin' } - ] - }; + const REQUIRED_FULL_ASSETS = ['bootloader.bin', 'partitions.bin', 'boot_app0.bin', 'firmware.bin']; + + // Current firmware path prefix — empty string for relative (latest), or a release download URL + let firmwarePathPrefix = ''; + // Track whether the selected release has all assets for full install + let releaseHasFullAssets = true; + + function makeFirmwareFiles(prefix) { + return { + full: [ + { offset: 0x0, name: 'bootloader.bin', path: prefix + 'bootloader.bin' }, + { offset: 0x8000, name: 'partitions.bin', path: prefix + 'partitions.bin' }, + { offset: 0xe000, name: 'boot_app0.bin', path: prefix + 'boot_app0.bin' }, + { offset: 0x10000, name: 'firmware.bin', path: prefix + 'firmware.bin' } + ], + update: [ + { offset: 0x10000, name: 'firmware.bin', path: prefix + 'firmware.bin' } + ] + }; + } function log(message, type = 'info') { logDiv.classList.remove('hidden'); @@ -300,10 +357,72 @@ progressText.textContent = text; } - // Identical to rnode-flasher's approach + // Version selector logic + let latestVersion = 'dev'; + + async function loadVersions() { + // Load deployed manifest version first + try { + const manifest = await fetch('manifest-install.json').then(r => r.json()); + latestVersion = manifest.version; + document.getElementById('firmware-version').textContent = latestVersion; + } catch (e) {} + + // Update the "Latest" option text + versionSelect.options[0].textContent = `Latest (${latestVersion})`; + + // Fetch GitHub releases + try { + const resp = await fetch('https://api.github.com/repos/torlando-tech/pyxis/releases'); + if (!resp.ok) return; + const releases = await resp.json(); + + for (const release of releases) { + if (release.draft || release.prerelease) continue; + const assetNames = release.assets.map(a => a.name); + if (!assetNames.includes('firmware.bin')) continue; + + const option = document.createElement('option'); + const hasAll = REQUIRED_FULL_ASSETS.every(f => assetNames.includes(f)); + option.value = release.tag_name; + option.textContent = release.tag_name + (release.name && release.name !== release.tag_name ? ` — ${release.name}` : ''); + option.dataset.downloadUrl = `https://github.com/torlando-tech/pyxis/releases/download/${release.tag_name}/`; + option.dataset.hasFullAssets = hasAll ? 'true' : 'false'; + versionSelect.appendChild(option); + } + } catch (e) { + // API failures are non-critical — user can still flash latest + } + } + + versionSelect.addEventListener('change', function() { + const selected = this.options[this.selectedIndex]; + if (this.value === 'latest') { + firmwarePathPrefix = 'firmware/'; + releaseHasFullAssets = true; + document.getElementById('firmware-version').textContent = latestVersion; + } else { + firmwarePathPrefix = selected.dataset.downloadUrl; + releaseHasFullAssets = selected.dataset.hasFullAssets === 'true'; + document.getElementById('firmware-version').textContent = this.value; + } + + // Handle full-install availability + if (!releaseHasFullAssets) { + eraseCheckbox.checked = false; + eraseCheckbox.disabled = true; + versionNote.textContent = 'Full install unavailable for this release (missing bootloader/partition assets)'; + versionNote.classList.remove('hidden'); + } else { + eraseCheckbox.disabled = false; + versionNote.classList.add('hidden'); + } + }); + async function flash() { const useFullInstall = eraseCheckbox.checked; - const files = useFullInstall ? FIRMWARE_FILES.full : FIRMWARE_FILES.update; + const fw = makeFirmwareFiles(firmwarePathPrefix); + const files = useFullInstall ? fw.full : fw.update; let transport = null; flashBtn.disabled = true; @@ -393,13 +512,9 @@ flashBtn.addEventListener('click', flash); - // Load manifest version - fetch('manifest-install.json') - .then(r => r.json()) - .then(data => { - document.getElementById('firmware-version').textContent = data.version; - }) - .catch(() => {}); + // Initialize: set default prefix and load versions + firmwarePathPrefix = 'firmware/'; + loadVersions();