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();