Skip to content

Commit 406a34b

Browse files
authored
Merge pull request #11 from dadamachines/feature/webui-v0.4.1-pico-update
feat: WebUI v0.4.1 — Pico firmware update section
2 parents 91f8003 + 4f1e24e commit 406a34b

5 files changed

Lines changed: 228 additions & 5 deletions

File tree

2.07 KB
Binary file not shown.

docs/flash/70_webui_versions.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ Version History
3232
- Date
3333
- Description
3434
- Download
35+
* - **0.4.1**
36+
- 2026-04-01
37+
- System Updater: added Pico firmware update section with CDN check,
38+
download, and SD card deploy. Page renamed from "WebUI Updater" to
39+
"System Updater". New ``uploadsystem`` REST endpoint for writing to
40+
``/sdcard/system/``. Pico version exposed in ``getAppInfo`` API.
41+
- `zip <https://dadamachines.github.io/dada-tbd-firmware/webui-updates/webui-update-v0.4.1.zip>`__
3542
* - **0.4.0**
3643
- 2026-03-23
3744
- Full data migration: ``.jsn`` → ``.json`` rename for all 114 sound
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.4.0",
3-
"date": "2026-03-21",
4-
"description": "CI pipeline, .jsn→.json rename, repo cleanup"
2+
"version": "0.4.1",
3+
"date": "2026-04-01",
4+
"description": "System Updater: add Pico firmware update via CDN"
55
}

sdcard_image/www/webui-update.html

Lines changed: 218 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<head>
1515
<meta charset="UTF-8">
1616
<meta name="viewport" content="width=device-width, initial-scale=1">
17-
<title>TBD-16 — WebUI Updater</title>
17+
<title>TBD-16 — System Updater</title>
1818
<link rel="icon" href="favicon.ico" type="image/x-icon">
1919
<style>
2020
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -235,14 +235,17 @@
235235
</head>
236236
<body>
237237
<div class="app-header">
238-
<h1>&#9881; WebUI Updater</h1>
238+
<h1>&#9881; System Updater</h1>
239239
<div class="header-nav">
240240
<a href="/">&#8592; TBD-16</a>
241241
<a href="/preset-macro-manager.html">Presets &amp; Macros</a>
242242
</div>
243243
</div>
244244
<div class="container">
245245

246+
<!-- WebUI section -->
247+
<div class="section-label" style="margin-top:0;">WebUI</div>
248+
246249
<!-- Current version -->
247250
<div class="version-row" style="margin-top:0.25rem;">
248251
<div class="version-card">
@@ -300,6 +303,27 @@ <h3 id="manifest-title"></h3>
300303

301304
<!-- Log -->
302305
<div class="log-area" id="log-area"></div>
306+
307+
<!-- ═══ Pico Firmware ═══ -->
308+
<div class="section-label" style="margin-top:2.5rem;">Pico Firmware</div>
309+
310+
<div class="version-row">
311+
<div class="version-card">
312+
<div class="label">Installed Pico Version</div>
313+
<div class="value" id="pico-current-version">&mdash;</div>
314+
</div>
315+
</div>
316+
317+
<div class="online-banner" id="pico-banner">
318+
<div id="pico-content"><span class="checking">&#128269; Checking for Pico firmware updates&hellip;</span></div>
319+
</div>
320+
321+
<div class="progress-bar" id="pico-progress-bar">
322+
<div class="fill" id="pico-progress-fill" style="width:0%"></div>
323+
</div>
324+
325+
<div class="status-msg" id="pico-status-msg"></div>
326+
<div class="log-area" id="pico-log-area"></div>
303327
</div>
304328

305329
<script>
@@ -942,6 +966,198 @@ <h3 id="manifest-title"></h3>
942966
function escAttr(s) {
943967
return s.replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
944968
}
969+
970+
// ═══════════════════════════════════════════════════════════════
971+
// ══ Pico Firmware Update ══════════════════════════════════════
972+
// ═══════════════════════════════════════════════════════════════
973+
974+
var CDN_BASE = 'https://dadamachines.github.io/dada-tbd-firmware/';
975+
var RELEASES_URL = CDN_BASE + 'stable/releases.json';
976+
977+
var picoVersionEl = document.getElementById('pico-current-version');
978+
var picoBanner = document.getElementById('pico-banner');
979+
var picoContent = document.getElementById('pico-content');
980+
var picoProgressBar = document.getElementById('pico-progress-bar');
981+
var picoProgressFill = document.getElementById('pico-progress-fill');
982+
var picoStatusMsg = document.getElementById('pico-status-msg');
983+
var picoLogArea = document.getElementById('pico-log-area');
984+
985+
var currentPicoVersion = null;
986+
987+
function picoLog(msg, cls) {
988+
picoLogArea.style.display = 'block';
989+
var div = document.createElement('div');
990+
div.className = 'log-line' + (cls ? ' log-' + cls : '');
991+
div.textContent = msg;
992+
picoLogArea.appendChild(div);
993+
picoLogArea.scrollTop = picoLogArea.scrollHeight;
994+
}
995+
996+
function picoStatus(msg, type) {
997+
picoStatusMsg.textContent = msg;
998+
picoStatusMsg.className = 'status-msg ' + type;
999+
picoStatusMsg.style.display = 'block';
1000+
}
1001+
1002+
async function loadPicoVersion() {
1003+
try {
1004+
var resp = await fetch('/api/v2/device?action=getAppInfo');
1005+
if (resp.ok) {
1006+
var info = await resp.json();
1007+
currentPicoVersion = info.pico_version || null;
1008+
picoVersionEl.textContent = currentPicoVersion || 'unknown';
1009+
}
1010+
} catch (e) {
1011+
picoVersionEl.textContent = 'offline';
1012+
}
1013+
}
1014+
1015+
async function checkPicoUpdate() {
1016+
try {
1017+
var resp = await fetch(RELEASES_URL, { cache: 'no-cache' });
1018+
if (!resp.ok) throw new Error('HTTP ' + resp.status);
1019+
var releases = await resp.json();
1020+
1021+
var cdnPicoVersion = releases.picoVersion || null;
1022+
if (!cdnPicoVersion) {
1023+
picoContent.innerHTML = '<span class="checking">No Pico firmware version info on CDN.</span>';
1024+
return;
1025+
}
1026+
1027+
// Find the picoBin URL from the latest version entry
1028+
var picoBinUrl = null;
1029+
if (releases.versions && releases.versions.length > 0) {
1030+
var latest = releases.versions[0];
1031+
if (latest.files && latest.files.picoBin) {
1032+
picoBinUrl = CDN_BASE + latest.files.picoBin;
1033+
}
1034+
}
1035+
1036+
var stripped = currentPicoVersion ? currentPicoVersion.replace(/^v/, '') : '0.0.0';
1037+
var cdnStripped = cdnPicoVersion.replace(/^v/, '');
1038+
var isNewer = currentPicoVersion && compareVersions(cdnStripped, stripped) > 0;
1039+
1040+
var html = '';
1041+
1042+
if (!currentPicoVersion || currentPicoVersion === 'unknown') {
1043+
html += '<span class="checking">Cannot determine installed Pico version. ';
1044+
if (picoBinUrl) {
1045+
html += 'You can still install ' + escHtml(cdnPicoVersion) + '.</span>';
1046+
html += '<br><button class="btn btn-primary btn-sm" id="pico-install-btn" style="margin-top:0.5rem;">' +
1047+
'Install ' + escHtml(cdnPicoVersion) + '</button>';
1048+
} else {
1049+
html += 'No binary available for download yet.</span>';
1050+
}
1051+
} else if (isNewer) {
1052+
html += '<div class="update-title">&#10024; Pico Firmware Update: ' + escHtml(cdnPicoVersion) + '</div>' +
1053+
'<div class="update-meta">Installed: ' + escHtml(currentPicoVersion) + '</div>';
1054+
if (picoBinUrl) {
1055+
html += '<button class="btn btn-primary" id="pico-install-btn" style="margin-top:0.5rem;">' +
1056+
'Download &amp; Install ' + escHtml(cdnPicoVersion) + '</button>';
1057+
} else {
1058+
html += '<div class="update-desc" style="margin-top:0.5rem;">Binary not yet available for WebUI install. Use the web flasher instead.</div>';
1059+
}
1060+
} else {
1061+
html += '<span class="up-to-date">&#9989; Pico firmware is up to date (' + escHtml(cdnPicoVersion) + ')</span>';
1062+
if (picoBinUrl) {
1063+
html += '<br><button class="btn btn-secondary btn-sm" id="pico-reinstall-btn" style="margin-top:0.5rem;">' +
1064+
'&#8635; Reinstall ' + escHtml(cdnPicoVersion) + '</button>';
1065+
}
1066+
}
1067+
1068+
picoContent.innerHTML = html;
1069+
1070+
var installBtn = document.getElementById('pico-install-btn');
1071+
if (installBtn) {
1072+
installBtn.addEventListener('click', function() {
1073+
installPicoUpdate(cdnPicoVersion, picoBinUrl);
1074+
});
1075+
}
1076+
var reinstallBtn = document.getElementById('pico-reinstall-btn');
1077+
if (reinstallBtn) {
1078+
reinstallBtn.addEventListener('click', function() {
1079+
installPicoUpdate(cdnPicoVersion, picoBinUrl);
1080+
});
1081+
}
1082+
1083+
} catch (e) {
1084+
picoContent.innerHTML = '<span class="checking">Could not check for Pico firmware updates.</span>';
1085+
}
1086+
}
1087+
1088+
async function installPicoUpdate(version, binUrl) {
1089+
var btns = picoContent.querySelectorAll('button');
1090+
for (var i = 0; i < btns.length; i++) btns[i].disabled = true;
1091+
1092+
picoProgressBar.style.display = 'block';
1093+
picoProgressFill.style.width = '10%';
1094+
picoLog('Downloading Pico firmware ' + version + '...', 'info');
1095+
1096+
try {
1097+
// Step 1: Download binary from CDN
1098+
var resp = await fetch(binUrl);
1099+
if (!resp.ok) throw new Error('Download failed: HTTP ' + resp.status);
1100+
var binData = await resp.arrayBuffer();
1101+
picoLog('Downloaded ' + formatBytes(binData.byteLength), 'ok');
1102+
picoProgressFill.style.width = '40%';
1103+
1104+
// Step 2: Upload binary to SD card
1105+
picoLog('Uploading pico-firmware.bin to SD card...', 'info');
1106+
var uploadResp = await fetch(
1107+
'/api/v2/samples?action=uploadsystem&path=firmware/pico-firmware.bin',
1108+
{
1109+
method: 'POST',
1110+
headers: { 'Content-Type': 'application/octet-stream' },
1111+
body: binData
1112+
}
1113+
);
1114+
if (!uploadResp.ok) {
1115+
var errText = await uploadResp.text().catch(function() { return uploadResp.statusText; });
1116+
throw new Error('Upload failed: ' + errText);
1117+
}
1118+
var uploadResult = await uploadResp.json();
1119+
if (!uploadResult.ok) throw new Error(uploadResult.error || 'Upload rejected');
1120+
picoLog('Firmware binary uploaded (' + uploadResult.size + ' bytes)', 'ok');
1121+
picoProgressFill.style.width = '75%';
1122+
1123+
// Step 3: Write version sidecar file
1124+
picoLog('Writing version file...', 'info');
1125+
var versionResp = await fetch(
1126+
'/api/v2/samples?action=uploadsystem&path=firmware/pico-firmware-version.txt',
1127+
{
1128+
method: 'POST',
1129+
headers: { 'Content-Type': 'application/octet-stream' },
1130+
body: version
1131+
}
1132+
);
1133+
if (!versionResp.ok) {
1134+
picoLog('Warning: version file write failed', 'err');
1135+
} else {
1136+
picoLog('Version file written: ' + version, 'ok');
1137+
}
1138+
picoProgressFill.style.width = '100%';
1139+
1140+
picoStatus(
1141+
'Pico firmware ' + version + ' deployed to SD card. ' +
1142+
'It will be flashed automatically on next reboot.',
1143+
'success'
1144+
);
1145+
picoLog('Done! Reboot the device to flash the new Pico firmware.', 'ok');
1146+
1147+
picoVersionEl.textContent = currentPicoVersion + ' \u2192 ' + version + ' (pending reboot)';
1148+
1149+
} catch (e) {
1150+
picoStatus('Pico firmware update failed: ' + e.message, 'error');
1151+
picoLog('Error: ' + e.message, 'err');
1152+
picoProgressFill.style.width = '0%';
1153+
var btns2 = picoContent.querySelectorAll('button');
1154+
for (var j = 0; j < btns2.length; j++) btns2[j].disabled = false;
1155+
}
1156+
}
1157+
1158+
// Load Pico version + check for updates
1159+
loadPicoVersion().then(function() { checkPicoUpdate(); });
1160+
9451161
})();
9461162
</script>
9471163
</body>
2.09 KB
Binary file not shown.

0 commit comments

Comments
 (0)