import BLE from '../ble.js'; import UI from '../ui.js'; import IK from '../kinematics.js'; import Settings from '../settings.js'; import Motion from '../motion.js'; import { buildQuickSequencesHTML, initQuickSequences } from '../quick-sequences.js'; let eventCleanup = []; let isHomed = false; let spots = []; let bounds = { xMin: -150, xMax: 150, yMin: -30, yMax: 145 }; let currentBg = localStorage.getItem('wiji_input_bg') || 'default'; let steps1 = 0; let steps2 = 0; let isSpelling = false; function syncStateFromStorage() { isHomed = localStorage.getItem('wiji_homed') === 'true'; steps1 = isHomed ? parseInt(localStorage.getItem('wiji_steps1') || IK.ARM.HOME_STEPS.m1) : IK.ARM.HOME_STEPS.m1; steps2 = isHomed ? parseInt(localStorage.getItem('wiji_steps2') || IK.ARM.HOME_STEPS.m2) : IK.ARM.HOME_STEPS.m2; Motion.setSteps(steps1, steps2); } function saveSteps() { if (isHomed) { localStorage.setItem('wiji_steps1', steps1); localStorage.setItem('wiji_steps2', steps2); } } function buildHTML() { return `

Input Text

Type a word and the arm will spell it out on backgrounds that support an alphabet.

Live Position
System Status
X (mm)
0.0
Y (mm)
0.0
${isHomed ? '✓ HOMED' : '⚠ NOT HOMED! Movement Locked.'}
Spell Text
${Settings.speed}
${Settings.accel}
${Settings.delay}
${buildQuickSequencesHTML()}
`; } function updatePosMarker() { const marker = document.getElementById('pos-marker'); if (!marker) return; const t1 = IK.stepsToRad(steps1); const t2 = IK.stepsToRad(steps2); const { endX, endY, valid } = IK.forward(t1, t2); if (valid && isFinite(endX) && isFinite(endY)) { const px = document.getElementById('pos-x'); const py = document.getElementById('pos-y'); if (px) px.textContent = endX.toFixed(1); if (py) py.textContent = endY.toFixed(1); const w = bounds.xMax - bounds.xMin; const h = bounds.yMax - bounds.yMin; // Bounds check to avoid marker flying off screen entirely if (endX >= bounds.xMin && endX <= bounds.xMax && endY >= bounds.yMin && endY <= bounds.yMax) { const xPct = ((endX - bounds.xMin) / w) * 100; const yPct = ((bounds.yMax - endY) / h) * 100; marker.style.left = `${xPct}%`; marker.style.top = `${yPct}%`; marker.style.display = 'block'; } else { marker.style.display = 'none'; // Out of bounds visually } } else { marker.style.display = 'none'; } } function updateHomingUI() { const banner = document.getElementById('homing-banner'); const btn = document.getElementById('btn-force-home'); if (!banner || !btn) return; if (isHomed) { banner.className = 'homing-banner homed'; banner.textContent = '✓ HOMED'; btn.textContent = 'FORCE HOME ALL'; } else { banner.className = 'homing-banner not-homed'; banner.textContent = '⚠ NOT HOMED! Movement Locked.'; btn.textContent = 'HOME ALL MOTORS'; } } async function loadSpots() { try { const res = await fetch(`web/assets/backgrounds/${currentBg}/spots.json?v=${Date.now()}`); const data = await res.json(); if (!data.hasAlphabet) { UI.log(`Background '${currentBg}' does not support an alphabet.`, 'warn'); } let parsedSpots = []; let parkPosition = { x: 0, y: 80 }; if (Array.isArray(data)) { parsedSpots = data; } else { parsedSpots = data.spots || []; if (data.bounds) bounds = data.bounds; if (data.parkPosition) parkPosition = data.parkPosition; } if (!parsedSpots.find(s => s.id === 'PARK')) { parsedSpots.push({ id: 'PARK', label: 'Park Position', x: parkPosition.x, y: parkPosition.y }); } spots = parsedSpots; document.getElementById('bg-image').src = `web/assets/backgrounds/${currentBg}/bg.svg?v=${Date.now()}`; const mapContainer = document.getElementById('map-container'); if (mapContainer) { const w = bounds.xMax - bounds.xMin; const h = bounds.yMax - bounds.yMin; // mapContainer.style.aspectRatio = `${w}/${h}`; // Let natural image aspect ratio dictate container height } updatePosMarker(); } catch(e) { UI.log('Failed to load spots.json', 'error'); } } async function sendSettings() { if (!BLE.isConnected()) return; try { await BLE.write(`SPD:${Settings.speed}`); await BLE.write(`ACC:${Settings.accel}`); } catch (e) { console.error('Failed to send settings', e); } } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function executeMove(spot, forceDirect = false) { UI.log(`Spelling: ${spot.label}`, 'info'); const modeEl = document.getElementById('text-mode-select'); const mode = forceDirect ? 'direct' : (modeEl ? modeEl.value : 'direct'); return await Motion.goto(spot.x, spot.y, mode); } async function spellText(text) { const btnSpell = document.getElementById('btn-spell'); const btnStop = document.getElementById('btn-stop'); const inputEl = document.getElementById('text-input'); if (!isHomed) { UI.log('Cannot move: System is not homed! Please click HOME ALL.', 'error'); return; } text = text.toUpperCase().replace(/\s+/g, ''); if (!text) return; isSpelling = true; btnSpell.style.display = 'none'; btnStop.style.display = 'block'; inputEl.disabled = true; // Send speed/accel right before moving just in case await sendSettings(); for (let i = 0; i < text.length; i++) { if (!isSpelling) break; const char = text[i]; // Find spot by ID (assuming ID is the uppercase letter) const spot = spots.find(s => s.id.toUpperCase() === char); if (spot) { await executeMove(spot, i === 0); // Wait for delay await delay(Settings.delay); } else { UI.log(`Character '${char}' not found on this board.`, 'warn'); } } isSpelling = false; btnSpell.style.display = 'block'; btnStop.style.display = 'none'; inputEl.disabled = false; UI.log('Finished spelling text', 'success'); } export default { mount(container) { syncStateFromStorage(); container.innerHTML = buildHTML(); updateHomingUI(); loadSpots(); // ── Background Selection ────────────────────────────────── const bgSelect = document.getElementById('bg-select'); if (bgSelect) { bgSelect.value = currentBg; bgSelect.addEventListener('change', () => { currentBg = bgSelect.value; localStorage.setItem('wiji_input_bg', currentBg); loadSpots(); initQuickSequences( currentBg, () => spots, () => ({ steps1, steps2 }), (s1, s2) => { steps1 = s1; steps2 = s2; updatePosMarker(); } ); }); } // ── Settings Sliders ────────────────────────────────────── const sSpeed = document.getElementById('slider-speed'); const sAccel = document.getElementById('slider-accel'); const sDelay = document.getElementById('slider-delay'); sSpeed.addEventListener('input', (e) => { Settings.speed = parseInt(e.target.value); document.getElementById('speed-val').textContent = Settings.speed; sendSettings(); }); sAccel.addEventListener('input', (e) => { Settings.accel = parseInt(e.target.value); document.getElementById('accel-val').textContent = Settings.accel; sendSettings(); }); sDelay.addEventListener('input', (e) => { Settings.delay = parseInt(e.target.value); document.getElementById('delay-val').textContent = Settings.delay; }); // ── Homing Logic ────────────────────────────────────────── document.getElementById('btn-force-home').addEventListener('click', async () => { isHomed = true; localStorage.setItem('wiji_homed', 'true'); updateHomingUI(); UI.log('Homing all motors...', 'info'); if (BLE.isConnected()) { try { await BLE.write('HOMEALL'); } catch (e) { UI.log(e.message, 'error'); } } else { steps1 = IK.ARM.HOME_STEPS.m1; steps2 = IK.ARM.HOME_STEPS.m2; Motion.setSteps(steps1, steps2); saveSteps(); updatePosMarker(); UI.log('[sim] HOMEALL', 'info'); } }); document.getElementById('btn-park').addEventListener('click', () => { const parkSpot = spots.find(s => s.id === 'PARK'); if (parkSpot) executeMove(parkSpot); }); // ── Spelling Logic ──────────────────────────────────────── const btnSpell = document.getElementById('btn-spell'); const btnStop = document.getElementById('btn-stop'); const inputEl = document.getElementById('text-input'); btnSpell.addEventListener('click', () => { spellText(inputEl.value); }); inputEl.addEventListener('keypress', (e) => { if (e.key === 'Enter') { spellText(inputEl.value); } }); const btnClearText = document.getElementById('btn-clear-text'); if (btnClearText) { btnClearText.addEventListener('click', () => { inputEl.value = ''; inputEl.focus(); }); } btnStop.addEventListener('click', () => { isSpelling = false; UI.log('Spelling stopped', 'info'); }); // ── Sync Steps from BLE ─────────────────────────────────── const onBLEStatus = (e) => { const msg = e.detail; if (msg.startsWith('P:')) { const [s1, s2] = msg.slice(2).split(',').map(Number); steps1 = s1; steps2 = s2; saveSteps(); updatePosMarker(); } }; document.addEventListener('ble:status', onBLEStatus); // Sync position on mount if connected if (BLE.isConnected()) { BLE.write('POS').catch(() => {}); sendSettings(); } else { updatePosMarker(); } initQuickSequences( currentBg, () => spots, () => ({ steps1, steps2 }), (s1, s2) => { steps1 = s1; steps2 = s2; updatePosMarker(); } ); eventCleanup.push(() => document.removeEventListener('ble:status', onBLEStatus)); }, unmount() { isSpelling = false; eventCleanup.forEach(fn => fn()); eventCleanup = []; } };