diff options
Diffstat (limited to '')
| -rw-r--r-- | index.html | 866 |
1 files changed, 866 insertions, 0 deletions
diff --git a/index.html b/index.html new file mode 100644 index 0000000..b6cf570 --- /dev/null +++ b/index.html @@ -0,0 +1,866 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Emisar D3AA Simulator</title> +<style> + * { box-sizing: border-box; margin: 0; padding: 0; } + + body { + background: #111; + color: #ccc; + font-family: 'Courier New', monospace; + font-size: 13px; + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; + padding: 20px; + gap: 20px; + } + + h1 { color: #888; font-size: 14px; letter-spacing: 2px; text-transform: uppercase; } + + .main-layout { + display: flex; + gap: 30px; + flex-wrap: wrap; + justify-content: center; + width: 100%; + max-width: 860px; + } + + /* ---- Left: flashlight visual ---- */ + .flashlight { + display: flex; + flex-direction: column; + align-items: center; + gap: 18px; + flex: 0 0 220px; + } + + .led-housing { + position: relative; + width: 160px; + height: 160px; + border-radius: 50%; + background: radial-gradient(circle at 50% 50%, #333 0%, #1a1a1a 60%, #111 100%); + border: 2px solid #2a2a2a; + display: flex; + align-items: center; + justify-content: center; + box-shadow: inset 0 0 20px rgba(0,0,0,0.8); + } + + #main-led { + width: 110px; + height: 110px; + border-radius: 50%; + background: radial-gradient(circle at 50% 40%, #fff 0%, #ffe8b0 30%, #ff9900 60%, transparent 100%); + opacity: 0; + /* transition: opacity 0.05s, box-shadow 0.05s; */ + } + + .aux-leds { + display: flex; + gap: 8px; + align-items: center; + } + + /* Two-concentric-circle aux LED: inner lights on dim, both on bright */ + .aux-ring { + position: relative; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + .aux-ring-outer { + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #2a2a2a; + box-sizing: border-box; + } + .aux-ring-inner { + position: absolute; + width: 9px; + height: 9px; + border-radius: 50%; + background: #1a1a1a; + } + + /* Combined RGB indicator */ + .aux-combined { + width: 14px; + height: 14px; + border-radius: 50%; + background: #1a1a1a; + border: 1px solid #2a2a2a; + flex-shrink: 0; + } + + /* Button LED indicator */ + .btn-led-wrap { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + } + #btn-led { + width: 8px; + height: 8px; + border-radius: 50%; + background: #222; + border: 1px solid #333; + /* transition: background 0.1s, box-shadow 0.1s; */ + } + + /* Physical switch button */ + #switch-btn { + width: 70px; + height: 70px; + border-radius: 50%; + background: radial-gradient(circle at 45% 35%, #4a4a4a, #222); + border: 3px solid #555; + cursor: pointer; + outline: none; + -webkit-user-select: none; + user-select: none; + /* transition: transform 0.05s, border-color 0.05s; */ + box-shadow: 0 4px 12px rgba(0,0,0,0.6), inset 0 1px 2px rgba(255,255,255,0.1); + display: flex; + align-items: center; + justify-content: center; + color: #666; + font-size: 10px; + letter-spacing: 1px; + } + #switch-btn:active, #switch-btn.pressed { + transform: scale(0.93); + border-color: #888; + box-shadow: 0 2px 6px rgba(0,0,0,0.8), inset 0 1px 2px rgba(255,255,255,0.05); + } + + /* ---- Right: controls & info ---- */ + .controls { + flex: 1; + min-width: 280px; + max-width: 480px; + display: flex; + flex-direction: column; + gap: 14px; + } + + .panel { + background: #1a1a1a; + border: 1px solid #2a2a2a; + border-radius: 6px; + padding: 12px 14px; + } + .panel h3 { + color: #666; + font-size: 11px; + letter-spacing: 1px; + text-transform: uppercase; + margin-bottom: 10px; + } + + .slider-row { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; + } + .slider-row label { + flex: 0 0 80px; + color: #888; + font-size: 12px; + } + .slider-row input[type=range] { + flex: 1; + height: 4px; + -webkit-appearance: none; + appearance: none; + background: #333; + border-radius: 2px; + outline: none; + } + .slider-row input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: #888; + cursor: pointer; + } + .slider-row input[type=range]::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: #888; + cursor: pointer; + border: none; + } + .slider-row .val { + flex: 0 0 60px; + text-align: right; + color: #aaa; + font-size: 12px; + } + + /* State table */ + .state-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px 16px; + } + .state-row { + display: flex; + justify-content: space-between; + gap: 8px; + border-bottom: 1px solid #222; + padding: 2px 0; + font-size: 11px; + } + .state-row .k { color: #666; } + .state-row .v { color: #aaa; font-weight: bold; } + .state-row .v.bright { color: #ffcc44; } + + /* EEPROM hex grid */ + #eeprom-grid { + font-size: 11px; + line-height: 1.5; + letter-spacing: 1px; + color: #444; + overflow-x: auto; + white-space: nowrap; + } + #eeprom-grid span { + display: inline-block; + width: 22px; + text-align: center; + /* transition: color 0.3s, background 0.3s; */ + } + #eeprom-grid span.changed { + color: #ffcc44; + background: rgba(255,200,50,0.1); + } + #eeprom-grid span.nonzero { + color: #88aacc; + } + .eeprom-addr { + color: #333; + display: inline-block; + width: 30px; + margin-right: 4px; + } + + /* Shared button base */ + .btn { + background: #2a2a2a; + border: 1px solid #444; + border-radius: 4px; + color: #aaa; + font-family: inherit; + cursor: pointer; + } + .btn:hover { border-color: #888; color: #ccc; } + + /* Firmware selector row */ + .fw-select-row { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #666; + flex-wrap: wrap; + } + #fw-select { + flex: 1; + min-width: 140px; + background: #222; + border: 1px solid #444; + border-radius: 4px; + color: #aaa; + font-family: inherit; + font-size: 12px; + padding: 2px 6px; + outline: none; + } + #fw-select:hover { border-color: #888; } + #fw-upload-btn, #fw-delete-btn { font-size: 12px; padding: 2px 8px; } + #fw-delete-btn { color: #c44; } + #fw-delete-btn:hover { border-color: #c44; } + #fw-file-input { display: none; } + + /* Status bar */ + #status-bar { + color: #444; + font-size: 11px; + text-align: center; + height: 16px; + } + #status-bar.ok { color: #4a8; } + #status-bar.err { color: #c44; } + + #eeprom-reset-btn { + font-size: 10px; + padding: 1px 6px; + margin-left: 8px; + vertical-align: middle; + } +</style> +</head> +<body> +<h1>Emisar D3AA — Simulator</h1> + +<div class="main-layout"> + <!-- LEFT: Flashlight visual --> + <div class="flashlight"> + <div class="led-housing"> + <div id="main-led"></div> + </div> + + <div class="aux-leds"> + <div class="aux-ring" id="aux-r" title="Aux Red (PA3)"> + <div class="aux-ring-outer"></div> + <div class="aux-ring-inner"></div> + </div> + <div class="aux-ring" id="aux-g" title="Aux Green (PA2)"> + <div class="aux-ring-outer"></div> + <div class="aux-ring-inner"></div> + </div> + <div class="aux-ring" id="aux-b" title="Aux Blue (PA0)"> + <div class="aux-ring-outer"></div> + <div class="aux-ring-inner"></div> + </div> + + <div id="aux-rgb" class="aux-combined" title="Combined AUX RGB"></div> + + <div class="btn-led-wrap"> + <div id="btn-led" title="Button LED (PA7)"></div> + </div> + </div> + + <button id="switch-btn" title="E-switch (PA4, active-low)">SWITCH</button> + + <div id="status-bar">Loading...</div> + </div> + + <!-- RIGHT: Controls & info --> + <div class="controls"> + + <!-- Firmware selector --> + <div class="panel"> + <h3>Firmware</h3> + <div class="fw-select-row"> + <select id="fw-select"></select> + <button id="fw-upload-btn" class="btn" title="Upload custom .hex firmware">Upload</button> + <button id="fw-delete-btn" class="btn" title="Delete selected custom firmware">Del</button> + </div> + <input type="file" id="fw-file-input" accept=".hex,.txt"> + </div> + + <!-- Sliders --> + <div class="panel"> + <h3>Inputs</h3> + <div class="slider-row"> + <label>Battery</label> + <input type="range" id="voltage-slider" min="125" max="210" value="200" step="1"> + <span class="val" id="voltage-val">4.00 V</span> + </div> + <div class="slider-row"> + <label>Temperature</label> + <input type="range" id="temp-slider" min="0" max="80" value="25" step="1"> + <span class="val" id="temp-val">25 °C</span> + </div> + </div> + + <!-- Firmware state --> + <div class="panel"> + <h3>Firmware State</h3> + <div class="state-grid"> + <div class="state-row"><span class="k">Level</span><span class="v" id="s-level">—</span></div> + <div class="state-row"><span class="k">DAC</span><span class="v" id="s-dac">—</span></div> + <div class="state-row"><span class="k">VREF</span><span class="v" id="s-vref">—</span></div> + <div class="state-row" id="row-chan"><span class="k">Channel</span><span class="v" id="s-chan">—</span></div> + <div class="state-row"><span class="k">Voltage</span><span class="v" id="s-volt">—</span></div> + <div class="state-row"><span class="k">Temp</span><span class="v" id="s-temp">—</span></div> + <div class="state-row"><span class="k">HDR</span><span class="v" id="s-hdr">—</span></div> + <div class="state-row"><span class="k">Boost</span><span class="v" id="s-boost">—</span></div> + <div class="state-row"><span class="k">NFET</span><span class="v" id="s-nfet">—</span></div> + <div class="state-row"><span class="k">Ticks</span><span class="v" id="s-tick">—</span></div> + </div> + </div> + + <!-- EEPROM --> + <div class="panel"> + <h3>EEPROM (256 bytes) <button id="eeprom-reset-btn" class="btn">Reset</button></h3> + <div id="eeprom-grid"></div> + </div> + + </div> +</div> + +<script type="module"> +import { D3AA } from './sim.js'; + +let sim = null; +let prevEeprom = new Uint8Array(256).fill(0); + +// EEPROM localStorage persistence +const EEPROM_KEY = 'd3aa-eeprom'; +const CUSTOM_FW_KEY = 'd3aa-custom-fw'; +const BUILTIN_FW_LABEL = 'Anduril (built-in)'; +let builtinHex = null; +let activeFwLabel = BUILTIN_FW_LABEL; +let lastSavedEeprom = new Uint8Array(256).fill(0xFF); +let eepromSaveTimer = null; + +const statusBar = document.getElementById('status-bar'); + +const CPU_FREQ = 12_000_000; + +// Pre-cached DOM refs for the poll loop +const auxRings = { + r: { outer: document.querySelector('#aux-r .aux-ring-outer'), inner: document.querySelector('#aux-r .aux-ring-inner') }, + g: { outer: document.querySelector('#aux-g .aux-ring-outer'), inner: document.querySelector('#aux-g .aux-ring-inner') }, + b: { outer: document.querySelector('#aux-b .aux-ring-outer'), inner: document.querySelector('#aux-b .aux-ring-inner') }, +}; +const elAuxRGB = document.getElementById('aux-rgb'); +const elBtnLed = document.getElementById('btn-led'); + +// VREF index → voltage string +const VREF_TABLE = ['1.024V', '2.048V', '4.096V', '2.500V']; + +let t0 = Date.now(); + +// ---- LED rendering helpers ---- +function setAuxRing(ring, state, r, g, b) { + if (state === 0) { + ring.outer.style.borderColor = '#2a2a2a'; + ring.outer.style.boxShadow = 'none'; + ring.inner.style.background = '#1a1a1a'; + ring.inner.style.boxShadow = 'none'; + } else if (state === 1) { + ring.outer.style.borderColor = '#2a2a2a'; + ring.outer.style.boxShadow = 'none'; + ring.inner.style.background = `rgb(${r},${g},${b})`; + ring.inner.style.boxShadow = `0 0 4px 1px rgba(${r},${g},${b},0.5)`; + } else { + ring.outer.style.borderColor = `rgb(${r},${g},${b})`; + ring.outer.style.boxShadow = `0 0 6px 2px rgba(${r},${g},${b},0.4)`; + ring.inner.style.background = `rgb(${r},${g},${b})`; + ring.inner.style.boxShadow = `0 0 5px 1px rgba(${r},${g},${b},0.7)`; + } +} + +function setAuxLed(el, state, r, g, b) { + if (state === 0) { + el.style.background = '#1a1a1a'; + el.style.boxShadow = 'none'; + } else { + const op = state === 1 ? 0.5 : 1.0; + const glow = state === 2 ? `, 0 0 8px 2px rgba(${r},${g},${b},0.7)` : ''; + el.style.background = `rgba(${r},${g},${b},${op})`; + el.style.boxShadow = `inset 0 0 2px rgba(255,255,255,0.3)${glow}`; + } +} + +function setRgbCombined(el, stateR, stateG, stateB) { + const sc = [0, 0.25, 1.0]; + const r = Math.round(sc[Math.min(stateR, 2)] * 255); + const g = Math.round(sc[Math.min(stateG, 2)] * 220); + const b = Math.round(sc[Math.min(stateB, 2)] * 255); + if (r === 0 && g === 0 && b === 0) { + el.style.background = '#1a1a1a'; + el.style.boxShadow = 'none'; + } else { + const lum = Math.max(r / 255, g / 220, b / 255); + el.style.background = `rgb(${r},${g},${b})`; + el.style.boxShadow = `0 0 ${Math.round(4 + 8 * lum)}px rgba(${r},${g},${b},${(0.5 + 0.3 * lum).toFixed(2)})`; + } +} + +function setMainLed(level, dac) { + const led = document.getElementById('main-led'); + if (level === 0) { + led.style.opacity = '0'; + led.style.boxShadow = 'none'; + return; + } + const t = Math.pow(level / 150, 0.35); + const warmness = Math.max(0, 1 - t * 1.2); + const r = 255; + const g = Math.round(255 - warmness * 60); + const b = Math.round(255 - warmness * 180); + const glowRad = Math.round(20 + 80 * t); + const glowSprd = Math.round(4 + 30 * t); + const outerRad = Math.round(40 + 120 * t); + const glowAlpha= (0.3 + 0.65 * t).toFixed(2); + + led.style.opacity = (0.3 + 0.7 * t).toFixed(2); + led.style.boxShadow = + `0 0 ${glowRad}px ${glowSprd}px rgba(${r},${g},${b},${glowAlpha}),` + + `0 0 ${outerRad}px rgba(${r},${g},${b},${(glowAlpha * 0.4).toFixed(2)})`; +} + +// ---- EEPROM rendering ---- +const eepromEl = document.getElementById('eeprom-grid'); +const eepromSpans = []; +for (let row = 0; row < 16; row++) { + const addrEl = document.createElement('span'); + addrEl.className = 'eeprom-addr'; + addrEl.textContent = (row * 16).toString(16).padStart(2, '0').toUpperCase() + ':'; + eepromEl.appendChild(addrEl); + for (let col = 0; col < 16; col++) { + const sp = document.createElement('span'); + sp.textContent = '00'; + eepromEl.appendChild(sp); + eepromSpans.push(sp); + } + eepromEl.appendChild(document.createElement('br')); +} + +function updateEeprom(eepromData) { + for (let i = 0; i < 256; i++) { + const v = eepromData[i]; + const sp = eepromSpans[i]; + const hex = v.toString(16).padStart(2, '0').toUpperCase(); + if (sp.textContent !== hex) sp.textContent = hex; + const changed = v !== prevEeprom[i]; + const nz = v !== 0x00 && v !== 0xFF; + if (changed) { + sp.className = 'changed'; + } else if (nz) { + sp.className = 'nonzero'; + } else { + sp.className = ''; + } + } + prevEeprom.set(eepromData); +} + +// ---- RAF polling loop ---- +function poll() { + requestAnimationFrame(poll); + if (!sim) return; + + // catch up simulator + const dt = Date.now() - t0; + const cycles = CPU_FREQ / 1000 * dt; + sim.step(cycles - sim.getState().cycles); + + // Read state from emulator + const st = sim.getState(); + + // Update main LED + setMainLed(st.level, st.dac); + + // Aux LEDs: R=PA3, G=PA2, B=PA0 + setAuxRing(auxRings.r, st.auxR, 255, 50, 50); + setAuxRing(auxRings.g, st.auxG, 50, 220, 80); + setAuxRing(auxRings.b, st.auxB, 80, 130, 255); + setRgbCombined(elAuxRGB, st.auxR, st.auxG, st.auxB); + + // Button LED: white + setAuxLed(elBtnLed, st.btnLed, 220, 220, 220); + + // State panel + const voltV = st.voltage ? (st.voltage / 50).toFixed(2) + ' V' : '—'; + const tempC = st.tempC + ' °C'; + const vrefStr = VREF_TABLE[st.vref] || st.vref; + + document.getElementById('s-level').textContent = st.level; + document.getElementById('s-level').className = 'v' + (st.level > 0 ? ' bright' : ''); + document.getElementById('s-dac').textContent = st.dac; + document.getElementById('s-vref').textContent = vrefStr; + document.getElementById('s-chan').textContent = st.channel; + document.getElementById('s-volt').textContent = voltV; + document.getElementById('s-temp').textContent = tempC; + document.getElementById('s-hdr').textContent = st.hdr ? 'HI' : 'lo'; + document.getElementById('s-boost').textContent = st.boost ? 'ON' : 'off'; + document.getElementById('s-nfet').textContent = st.nfet ? 'ON' : 'off'; + document.getElementById('s-tick').textContent = st.tickCount; + + // EEPROM + updateEeprom(st.eeprom); + + // Persist EEPROM to localStorage (throttled to once per second) + if (!eepromSaveTimer) { + for (let i = 0; i < 256; i++) { + if (st.eeprom[i] !== lastSavedEeprom[i]) { + const snapshot = new Uint8Array(st.eeprom); + lastSavedEeprom.set(snapshot); + eepromSaveTimer = setTimeout(() => { eepromSaveTimer = null; }, 1000); + localStorage.setItem(EEPROM_KEY, JSON.stringify(Array.from(snapshot))); + break; + } + } + } +} + +// ---- Button input (mouse + touch) ---- +const switchBtn = document.getElementById('switch-btn'); + +function onPress(e) { + e.preventDefault(); + if (!sim) return; + switchBtn.classList.add('pressed'); + sim.buttonPress(); +} +function onRelease(e) { + e.preventDefault(); + if (!sim) return; + switchBtn.classList.remove('pressed'); + sim.buttonRelease(); +} + +switchBtn.addEventListener('mousedown', onPress, { passive: false }); +switchBtn.addEventListener('mouseup', onRelease, { passive: false }); +switchBtn.addEventListener('mouseleave', onRelease, { passive: false }); +switchBtn.addEventListener('touchstart', onPress, { passive: false }); +switchBtn.addEventListener('touchend', onRelease, { passive: false }); +switchBtn.addEventListener('touchcancel',onRelease, { passive: false }); + +// ---- Space bar actuates switch ---- +document.addEventListener('keydown', e => { + if (e.code === 'Space' && !e.repeat) { e.preventDefault(); onPress(e); } +}); +document.addEventListener('keyup', e => { + if (e.code === 'Space') { e.preventDefault(); onRelease(e); } +}); + +// ---- Slider inputs ---- +const voltageSlider = document.getElementById('voltage-slider'); +const voltageVal = document.getElementById('voltage-val'); +const tempSlider = document.getElementById('temp-slider'); +const tempVal = document.getElementById('temp-val'); + +voltageSlider.addEventListener('input', () => { + const vbat50 = parseInt(voltageSlider.value); + voltageVal.textContent = (vbat50 / 50).toFixed(2) + ' V'; + if (sim) sim.setVoltage(vbat50); +}); + +tempSlider.addEventListener('input', () => { + const c = parseInt(tempSlider.value); + tempVal.textContent = c + ' °C'; + if (sim) sim.setTemperature(c); +}); + +// ---- Firmware management ---- +const fwSelect = document.getElementById('fw-select'); +const fwUploadBtn = document.getElementById('fw-upload-btn'); +const fwDeleteBtn = document.getElementById('fw-delete-btn'); +const fwFileInput = document.getElementById('fw-file-input'); + +function loadCustomFirmwares() { + try { + const data = JSON.parse(localStorage.getItem(CUSTOM_FW_KEY)); + return Array.isArray(data) ? data : []; + } catch (e) { + return []; + } +} + +function saveCustomFirmwares(list) { + localStorage.setItem(CUSTOM_FW_KEY, JSON.stringify(list)); +} + +function populateFwSelect() { + fwSelect.innerHTML = ''; + const builtinOpt = document.createElement('option'); + builtinOpt.value = '__builtin__'; + builtinOpt.textContent = BUILTIN_FW_LABEL; + fwSelect.appendChild(builtinOpt); + const customs = loadCustomFirmwares(); + customs.forEach((fw, i) => { + const opt = document.createElement('option'); + opt.value = String(i); + opt.textContent = fw.label; + fwSelect.appendChild(opt); + }); + // Restore selection to active firmware + if (activeFwLabel === BUILTIN_FW_LABEL) { + fwSelect.value = '__builtin__'; + } else { + const idx = customs.findIndex(fw => fw.label === activeFwLabel); + if (idx >= 0) fwSelect.value = String(idx); + } +} + +function switchFirmware(hexData, label) { + try { + // Snapshot current EEPROM and persist + if (sim) { + const snap = sim.getEepromSnapshot(); + lastSavedEeprom.set(snap); + localStorage.setItem(EEPROM_KEY, JSON.stringify(Array.from(snap))); + } + sim = null; + + const runner = new D3AA(); + runner.loadProgram(hexData); + t0 = Date.now(); + + // Restore EEPROM + const savedEeprom = localStorage.getItem(EEPROM_KEY); + if (savedEeprom) { + try { + const bytes = new Uint8Array(JSON.parse(savedEeprom)); + runner.loadEeprom(bytes); + lastSavedEeprom.set(bytes.subarray(0, 256)); + } catch (e) { + console.warn('Failed to restore EEPROM:', e); + } + } + + // Push slider values + runner.setVoltage(parseInt(voltageSlider.value)); + runner.setTemperature(parseInt(tempSlider.value)); + + sim = runner; + activeFwLabel = label; + statusBar.textContent = 'Running: ' + label; + statusBar.className = 'ok'; + } catch (err) { + statusBar.textContent = 'Error: ' + err.message; + statusBar.className = 'err'; + console.error(err); + } +} + +// Upload button triggers hidden file input +fwUploadBtn.addEventListener('click', () => fwFileInput.click()); + +// File input change: read hex, prompt for label, store and switch +fwFileInput.addEventListener('change', () => { + const file = fwFileInput.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + const hexText = reader.result; + const defaultLabel = file.name.replace(/\.[^.]+$/, ''); + const label = prompt('Label for this firmware:', defaultLabel); + if (!label) { fwFileInput.value = ''; return; } + const customs = loadCustomFirmwares(); + customs.push({ label, hex: hexText }); + saveCustomFirmwares(customs); + populateFwSelect(); + fwSelect.value = String(customs.length - 1); + switchFirmware(hexText, label); + fwFileInput.value = ''; + }; + reader.readAsText(file); +}); + +// Select change: switch firmware +fwSelect.addEventListener('change', () => { + const val = fwSelect.value; + if (val === '__builtin__') { + if (builtinHex) { + switchFirmware(builtinHex, BUILTIN_FW_LABEL); + } else { + statusBar.textContent = 'Loading built-in firmware...'; + statusBar.className = ''; + fetch('./anduril.hank-emisar-d3aa.hex') + .then(r => { if (!r.ok) throw new Error('Fetch failed: ' + r.status); return r.text(); }) + .then(hex => { builtinHex = hex; switchFirmware(hex, BUILTIN_FW_LABEL); }) + .catch(err => { statusBar.textContent = 'Error: ' + err.message; statusBar.className = 'err'; }); + } + } else { + const customs = loadCustomFirmwares(); + const idx = parseInt(val); + const fw = customs[idx]; + if (fw) switchFirmware(fw.hex, fw.label); + } +}); + +// Delete button: remove selected custom firmware +fwDeleteBtn.addEventListener('click', () => { + const val = fwSelect.value; + if (val === '__builtin__') return; + const customs = loadCustomFirmwares(); + const idx = parseInt(val); + const fw = customs[idx]; + if (!fw) return; + if (!confirm('Delete "' + fw.label + '"?')) return; + const wasActive = (fw.label === activeFwLabel); + customs.splice(idx, 1); + saveCustomFirmwares(customs); + populateFwSelect(); + if (wasActive) { + fwSelect.value = '__builtin__'; + if (builtinHex) { + switchFirmware(builtinHex, BUILTIN_FW_LABEL); + } else { + fetch('./anduril.hank-emisar-d3aa.hex') + .then(r => { if (!r.ok) throw new Error('Fetch failed: ' + r.status); return r.text(); }) + .then(hex => { builtinHex = hex; switchFirmware(hex, BUILTIN_FW_LABEL); }) + .catch(err => { statusBar.textContent = 'Error: ' + err.message; statusBar.className = 'err'; }); + } + } +}); + +// ---- EEPROM reset button ---- +document.getElementById('eeprom-reset-btn').addEventListener('click', () => { + if (!sim) return; + const erased = new Uint8Array(256).fill(0xFF); + sim.loadEeprom(erased); + lastSavedEeprom.set(erased); + localStorage.removeItem(EEPROM_KEY); +}); + +// ---- Init AVR simulator ---- +statusBar.textContent = 'Loading firmware...'; +statusBar.className = ''; + +try { + const hexResponse = await fetch('./anduril.hank-emisar-d3aa.hex'); + if (!hexResponse.ok) throw new Error('Failed to fetch hex file: ' + hexResponse.status); + const hexData = await hexResponse.text(); + builtinHex = hexData; + + sim = new D3AA(); + sim.loadProgram(hexData); + + // Restore EEPROM from localStorage if available + const savedEeprom = localStorage.getItem(EEPROM_KEY); + if (savedEeprom) { + try { + const bytes = new Uint8Array(JSON.parse(savedEeprom)); + sim.loadEeprom(bytes); + lastSavedEeprom.set(bytes.subarray(0, 256)); + } catch (e) { + console.warn('Failed to load saved EEPROM:', e); + } + } + + // Push initial slider values + sim.setVoltage(parseInt(voltageSlider.value)); + sim.setTemperature(parseInt(tempSlider.value)); + + populateFwSelect(); + + statusBar.textContent = 'Running: ' + BUILTIN_FW_LABEL; + statusBar.className = 'ok'; + + + t0 = Date.now(); + + // Start rendering + requestAnimationFrame(poll); +} catch (err) { + statusBar.textContent = 'Error: ' + err; + statusBar.className = 'err'; + console.error(err); +} +</script> +</body> +</html> |
