|
| 1 | +<!doctype html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | + <meta charset="utf-8" /> |
| 5 | + <meta |
| 6 | + name="viewport" |
| 7 | + content="width=device-width, initial-scale=1, maximum-scale=1" |
| 8 | + /> |
| 9 | + <title>M5Tab5 – Web Flash & Console</title> |
| 10 | + |
| 11 | + <!-- ESP Web Tools (installer button) --> |
| 12 | + <script type="module" |
| 13 | + src="https://unpkg.com/esp-web-tools@10/dist/web/install-button.js?module"> |
| 14 | + </script> |
| 15 | + |
| 16 | + <!-- xterm.js for the terminal --> |
| 17 | + <link rel="stylesheet" |
| 18 | + href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"> |
| 19 | + <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script> |
| 20 | + <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script> |
| 21 | + |
| 22 | + <style> |
| 23 | + :root{ |
| 24 | + --bg: #0b1020; |
| 25 | + --panel: #121a32; |
| 26 | + --ink: #e8ecf6; |
| 27 | + --muted: #a8b3cf; |
| 28 | + --accent: #6ea8ff; |
| 29 | + --good: #35c274; |
| 30 | + --warn: #ffb454; |
| 31 | + --bad: #ff6b6b; |
| 32 | + --border: #223153; |
| 33 | + |
| 34 | + /* ESP Web Tools theming */ |
| 35 | + --esp-tools-button-color: var(--accent); |
| 36 | + --esp-tools-button-text-color: #0b1020; |
| 37 | + --esp-tools-button-border-radius: 10px; |
| 38 | + } |
| 39 | + [data-theme="light"]{ |
| 40 | + --bg:#f6f7fb; --panel:#ffffff; --ink:#0b1020; --muted:#51607d; |
| 41 | + --accent:#2f6bff; --border:#e6e9f3; |
| 42 | + } |
| 43 | + *{box-sizing:border-box} |
| 44 | + body{ |
| 45 | + margin:0; background:var(--bg); color:var(--ink); |
| 46 | + font: 15px/1.45 system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif; |
| 47 | + } |
| 48 | + a{ color: var(--accent); text-decoration: none; } |
| 49 | + a:hover{ text-decoration: underline; } |
| 50 | + |
| 51 | + header{ |
| 52 | + display:flex; gap:16px; align-items:center; |
| 53 | + padding:20px 24px; border-bottom:1px solid var(--border); |
| 54 | + } |
| 55 | + .brand{ font-weight:700; letter-spacing:.3px; } |
| 56 | + .spacer{ flex:1; } |
| 57 | + .theme-toggle{ background:var(--panel); color:var(--ink); border:1px solid var(--border); |
| 58 | + padding:8px 12px; border-radius:10px; cursor:pointer; } |
| 59 | + |
| 60 | + .wrap{ |
| 61 | + display:grid; gap:16px; grid-template-columns: 1.15fr 0.85fr; |
| 62 | + padding:16px; min-height: calc(100vh - 72px); |
| 63 | + } |
| 64 | + .card{ |
| 65 | + background:var(--panel); border:1px solid var(--border); |
| 66 | + border-radius:16px; padding:16px; |
| 67 | + } |
| 68 | + h1{ margin:0 0 4px 0; font-size:22px; } |
| 69 | + h2{ margin:0 0 8px 0; font-size:16px; font-weight:600; } |
| 70 | + .muted{ color:var(--muted); } |
| 71 | + .row{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; } |
| 72 | + select, button, .btn{ |
| 73 | + background:#0d1733; color:var(--ink); border:1px solid var(--border); |
| 74 | + padding:10px 12px; border-radius:10px; font-weight:600; |
| 75 | + } |
| 76 | + [data-theme="light"] select, [data-theme="light"] button, [data-theme="light"] .btn{ |
| 77 | + background:#f4f6fe; color:#0b1020; |
| 78 | + } |
| 79 | + |
| 80 | + .cols{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; } |
| 81 | + .kpi{ |
| 82 | + display:flex; flex-direction:column; gap:6px; |
| 83 | + background:rgba(255,255,255,.03); border:1px dashed var(--border); |
| 84 | + padding:10px; border-radius:12px; |
| 85 | + } |
| 86 | + .kpi b{ font-size:13px; color: var(--muted); font-weight:700; } |
| 87 | + .kpi span{ font-size:14px; } |
| 88 | + |
| 89 | + .terminal{ |
| 90 | + height: 520px; |
| 91 | + border-radius:12px; overflow:hidden; |
| 92 | + border:1px solid var(--border); |
| 93 | + background:#000; |
| 94 | + } |
| 95 | + .toolbar{ |
| 96 | + display:flex; gap:8px; align-items:center; margin-bottom:10px; |
| 97 | + } |
| 98 | + .footnote{ font-size:12px; color:var(--muted); margin-top:10px; } |
| 99 | + .list{ margin:0; padding:0; list-style:none; } |
| 100 | + .list li{ padding:8px 0; border-bottom:1px dashed var(--border); } |
| 101 | + .pill{ padding:3px 8px; border-radius:999px; border:1px solid var(--border); } |
| 102 | + .ok{ color:var(--good) } .warn{ color:var(--warn) } .bad{ color:var(--bad) } |
| 103 | + @media (max-width: 1024px){ .wrap{ grid-template-columns: 1fr; } .terminal{ height:380px; } } |
| 104 | + </style> |
| 105 | +</head> |
| 106 | +<body> |
| 107 | + <header> |
| 108 | + <div class="brand">M5Tab5 – Web Flash & Console</div> |
| 109 | + <div class="spacer"></div> |
| 110 | + <button class="theme-toggle" id="themeBtn" title="Toggle theme">Toggle Theme</button> |
| 111 | + </header> |
| 112 | + |
| 113 | + <main class="wrap"> |
| 114 | + <!-- LEFT: flasher + builds --> |
| 115 | + <section class="card"> |
| 116 | + <h1>Flash your device</h1> |
| 117 | + <p class="muted" id="supportMsg"> |
| 118 | + 1) Connect the Tab5 over USB. 2) Pick a build. 3) Click <b>Install</b>. |
| 119 | + (Chrome/Edge on desktop; site must be served over HTTPS.)</p> |
| 120 | + |
| 121 | + <div class="row" style="margin:12px 0;"> |
| 122 | + <label for="buildSel"><b>Build:</b></label> |
| 123 | + <select id="buildSel" aria-label="Select build" style="min-width:260px"></select> |
| 124 | + <button id="refreshBtn" title="Refresh release list">Refresh</button> |
| 125 | + <span id="chipBadge" class="pill">Chip: <span id="chipTxt">—</span></span> |
| 126 | + <span id="statusBadge" class="pill">Status: <span id="statusTxt" class="warn">Idle</span></span> |
| 127 | + </div> |
| 128 | + |
| 129 | + <div class="cols" style="margin:10px 0 16px"> |
| 130 | + <div class="kpi"><b>Selected build</b><span id="selTag">—</span></div> |
| 131 | + <div class="kpi"><b>Binary</b><span id="selBin">—</span></div> |
| 132 | + </div> |
| 133 | + |
| 134 | + <!-- ESP Web Tools install button --> |
| 135 | + <esp-web-install-button id="installer" manifest=""> |
| 136 | + <button slot="activate">Install selected build</button> |
| 137 | + <span slot="unsupported"> |
| 138 | + Your browser doesn’t support Web Serial. Use Chrome or Edge on desktop. |
| 139 | + </span> |
| 140 | + <span slot="not-allowed"> |
| 141 | + This page must be served via HTTPS (or localhost). |
| 142 | + </span> |
| 143 | + </esp-web-install-button> |
| 144 | + |
| 145 | + <div class="footnote"> |
| 146 | + • Using a <b>merged firmware</b> single image at offset <code>0</code> as recommended for ESP-IDF 4+ projects. |
| 147 | + • We dynamically generate the manifest in-page (Blob URL) and point it to the release asset’s download URL. :contentReference[oaicite:3]{index=3} |
| 148 | + </div> |
| 149 | + |
| 150 | + <hr style="border:none;border-top:1px solid var(--border); margin:16px 0"> |
| 151 | + |
| 152 | + <h2>Recent builds</h2> |
| 153 | + <ul class="list" id="recentList"></ul> |
| 154 | + </section> |
| 155 | + |
| 156 | + <!-- RIGHT: terminal --> |
| 157 | + <aside class="card"> |
| 158 | + <h1>Device console</h1> |
| 159 | + <div class="toolbar"> |
| 160 | + <button id="portBtn">Connect</button> |
| 161 | + <button id="disconnectBtn" disabled>Disconnect</button> |
| 162 | + <button id="clearBtn">Clear</button> |
| 163 | + <label class="pill" style="display:flex;gap:6px;align-items:center;padding:6px 10px;"> |
| 164 | + <input type="checkbox" id="autoscroll" checked/> Autoscroll |
| 165 | + </label> |
| 166 | + <span class="spacer"></span> |
| 167 | + <span class="muted" id="portInfo">No port</span> |
| 168 | + </div> |
| 169 | + <div id="terminal" class="terminal"></div> |
| 170 | + <div class="footnote"> |
| 171 | + Note: you can’t flash and view the serial log at the same time on the same COM port. Disconnect the console before clicking Install. :contentReference[oaicite:4]{index=4} |
| 172 | + </div> |
| 173 | + </aside> |
| 174 | + </main> |
| 175 | + |
| 176 | + <footer class="card" style="margin:16px;"> |
| 177 | + <div class="row"> |
| 178 | + <div><b>About this project</b> — M5Tab5 User Demo (ESP-IDF 5.4.2 / ESP32-P4). See the |
| 179 | + <a href="https://github.com/baba-dev/M5Tab5-UserDemo" target="_blank" rel="noreferrer">repository</a>.</div> |
| 180 | + </div> |
| 181 | + </footer> |
| 182 | + |
| 183 | +<script> |
| 184 | +(() => { |
| 185 | + const OWNER = "baba-dev"; |
| 186 | + const REPO = "M5Tab5-UserDemo"; |
| 187 | + const RELEASES_API = `https://api.github.com/repos/${OWNER}/${REPO}/releases?per_page=20`; |
| 188 | + |
| 189 | + const buildSel = document.getElementById('buildSel'); |
| 190 | + const recentList = document.getElementById('recentList'); |
| 191 | + const installer = document.getElementById('installer'); |
| 192 | + const selTag = document.getElementById('selTag'); |
| 193 | + const selBin = document.getElementById('selBin'); |
| 194 | + const chipTxt = document.getElementById('chipTxt'); |
| 195 | + const statusTxt = document.getElementById('statusTxt'); |
| 196 | + |
| 197 | + // theme |
| 198 | + const themeBtn = document.getElementById('themeBtn'); |
| 199 | + const setTheme = t => document.documentElement.setAttribute('data-theme', t); |
| 200 | + setTheme(localStorage.getItem('theme') || 'dark'); |
| 201 | + themeBtn.onclick = () => { |
| 202 | + const next = (document.documentElement.getAttribute('data-theme') === 'dark') ? 'light' : 'dark'; |
| 203 | + setTheme(next); localStorage.setItem('theme', next); |
| 204 | + }; |
| 205 | + |
| 206 | + // fetch last 5 "build-*" releases |
| 207 | + async function loadReleases(){ |
| 208 | + buildSel.innerHTML = `<option>Loading…</option>`; |
| 209 | + recentList.innerHTML = ""; |
| 210 | + const res = await fetch(RELEASES_API); |
| 211 | + const all = await res.json(); |
| 212 | + const builds = all |
| 213 | + .filter(r => r.tag_name && r.tag_name.startsWith('build-')) |
| 214 | + .slice(0, 10); // take more, we’ll filter assets |
| 215 | + const rows = []; |
| 216 | + let choices = []; |
| 217 | + for (const r of builds){ |
| 218 | + if (!Array.isArray(r.assets)) continue; |
| 219 | + for (const a of r.assets){ |
| 220 | + if (!a.name.endsWith('.bin')) continue; |
| 221 | + choices.push({ |
| 222 | + tag: r.tag_name, |
| 223 | + published: r.published_at, |
| 224 | + name: a.name, |
| 225 | + url: a.browser_download_url |
| 226 | + }); |
| 227 | + } |
| 228 | + } |
| 229 | + // last 5 by published date |
| 230 | + choices.sort((a,b) => new Date(b.published) - new Date(a.published)); |
| 231 | + choices = choices.slice(0,5); |
| 232 | + |
| 233 | + buildSel.innerHTML = ""; |
| 234 | + for (const c of choices){ |
| 235 | + const opt = document.createElement('option'); |
| 236 | + opt.value = JSON.stringify(c); |
| 237 | + opt.textContent = `${c.tag} — ${c.name}`; |
| 238 | + buildSel.appendChild(opt); |
| 239 | + |
| 240 | + const li = document.createElement('li'); |
| 241 | + li.innerHTML = `<b>${c.tag}</b> • ${c.name} <span class="muted">(${new Date(c.published).toLocaleString()})</span>`; |
| 242 | + recentList.appendChild(li); |
| 243 | + } |
| 244 | + |
| 245 | + if (choices.length){ |
| 246 | + setSelection(choices[0]); |
| 247 | + } else { |
| 248 | + buildSel.innerHTML = `<option>No builds found</option>`; |
| 249 | + } |
| 250 | + } |
| 251 | + |
| 252 | + function setSelection(choice){ |
| 253 | + selTag.textContent = choice.tag; |
| 254 | + selBin.textContent = choice.name; |
| 255 | + |
| 256 | + // Build a dynamic manifest Blob (supported by ESP Web Tools docs) |
| 257 | + const manifest = { |
| 258 | + name: "M5Tab5 User Demo", |
| 259 | + version: choice.tag, |
| 260 | + new_install_prompt_erase: true, |
| 261 | + builds: [{ |
| 262 | + // P4 flashing is supported via esptool-js; chipFamily list in docs |
| 263 | + // doesn’t yet mention P4, so we advertise as ESP32 single merged bin. |
| 264 | + chipFamily: "ESP32", |
| 265 | + improv: false, |
| 266 | + parts: [{ path: choice.url, offset: 0 }] |
| 267 | + }] |
| 268 | + }; |
| 269 | + const blob = new Blob([JSON.stringify(manifest)], {type: "application/json"}); |
| 270 | + const url = URL.createObjectURL(blob); |
| 271 | + installer.setAttribute('manifest', url); |
| 272 | + } |
| 273 | + |
| 274 | + document.getElementById('refreshBtn').onclick = loadReleases; |
| 275 | + buildSel.onchange = () => setSelection(JSON.parse(buildSel.value)); |
| 276 | + |
| 277 | + // update chip + status from installer events when available |
| 278 | + installer.addEventListener('esp-web-install', (e) => { |
| 279 | + // Custom events aren’t documented exhaustively; keep simple |
| 280 | + statusTxt.textContent = "Installing…"; |
| 281 | + statusTxt.className = "warn"; |
| 282 | + }); |
| 283 | + installer.addEventListener('esp-web-install-success', () => { |
| 284 | + statusTxt.textContent = "Installed"; |
| 285 | + statusTxt.className = "ok"; |
| 286 | + }); |
| 287 | + installer.addEventListener('esp-web-install-error', () => { |
| 288 | + statusTxt.textContent = "Error"; |
| 289 | + statusTxt.className = "bad"; |
| 290 | + }); |
| 291 | + |
| 292 | + // serial console (Web Serial + xterm) |
| 293 | + const term = new window.Terminal({ cursorBlink: true, convertEol: true, fontSize: 13 }); |
| 294 | + const fitAddon = new window.FitAddon.FitAddon(); |
| 295 | + term.loadAddon(fitAddon); |
| 296 | + term.open(document.getElementById('terminal')); |
| 297 | + fitAddon.fit(); |
| 298 | + |
| 299 | + let reader, port; |
| 300 | + const portBtn = document.getElementById('portBtn'); |
| 301 | + const disconnectBtn = document.getElementById('disconnectBtn'); |
| 302 | + const clearBtn = document.getElementById('clearBtn'); |
| 303 | + const portInfo = document.getElementById('portInfo'); |
| 304 | + const autoscroll = document.getElementById('autoscroll'); |
| 305 | + |
| 306 | + function setPortUI(connected, info = "No port"){ |
| 307 | + portBtn.disabled = connected; |
| 308 | + disconnectBtn.disabled = !connected; |
| 309 | + portInfo.textContent = info; |
| 310 | + } |
| 311 | + |
| 312 | + portBtn.onclick = async () => { |
| 313 | + try{ |
| 314 | + port = await navigator.serial.requestPort(); |
| 315 | + await port.open({ baudRate: 115200 }); |
| 316 | + setPortUI(true, "115200 baud"); |
| 317 | + const dec = new TextDecoderStream(); |
| 318 | + const readable = port.readable.pipeThrough(dec); |
| 319 | + reader = readable.getReader(); |
| 320 | + (async () => { |
| 321 | + while (true){ |
| 322 | + const { value, done } = await reader.read(); |
| 323 | + if (done) break; |
| 324 | + if (value){ |
| 325 | + term.write(value.replace(/\r?\n/g, "\r\n")); |
| 326 | + if (autoscroll.checked) term.scrollToBottom(); |
| 327 | + } |
| 328 | + } |
| 329 | + })(); |
| 330 | + } catch(e){ |
| 331 | + console.error(e); |
| 332 | + } |
| 333 | + }; |
| 334 | + |
| 335 | + disconnectBtn.onclick = async () => { |
| 336 | + try{ |
| 337 | + if (reader){ await reader.cancel(); reader.releaseLock(); } |
| 338 | + if (port){ await port.close(); } |
| 339 | + } finally{ |
| 340 | + setPortUI(false); |
| 341 | + } |
| 342 | + }; |
| 343 | + |
| 344 | + clearBtn.onclick = () => term.reset(); |
| 345 | + |
| 346 | + // show chip family (best effort once connected via installer) |
| 347 | + installer.addEventListener('click', () => { |
| 348 | + // The component detects chip on connect; we mirror a placeholder |
| 349 | + chipTxt.textContent = "Detecting…"; |
| 350 | + }); |
| 351 | + |
| 352 | + // init |
| 353 | + loadReleases(); |
| 354 | +})(); |
| 355 | +</script> |
| 356 | +</body> |
| 357 | +</html> |
0 commit comments