import BLE from '../ble.js'; import UI from '../ui.js'; import IK from '../kinematics.js'; import Settings from '../settings.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_bg') || 'default'; let steps1 = 0; let steps2 = 0; 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; } function saveSteps() { if (isHomed) { localStorage.setItem('wiji_steps1', steps1); localStorage.setItem('wiji_steps2', steps2); } } function buildHTML() { return `

Board Control

Click on the map or select from the list to move the pointer.

Interactive Map
System Status
X (mm)
0.0
Y (mm)
0.0
${isHomed ? '✓ HOMED' : '⚠ NOT HOMED! Movement Locked.'}
Navigation
${Settings.speed}
${Settings.accel}
${buildQuickSequencesHTML()}
`; } function updatePositionReadout() { const t1 = IK.stepsToRad(steps1); const t2 = IK.stepsToRad(steps2); const { endX, endY } = IK.forward(t1, t2); const px = document.getElementById('pos-x'); const py = document.getElementById('pos-y'); if (px) px.textContent = isFinite(endX) ? endX.toFixed(1) : '---'; if (py) py.textContent = isFinite(endY) ? endY.toFixed(1) : '---'; } 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 (Array.isArray(data)) { spots = data; } else { spots = data.spots || []; if (data.bounds) bounds = data.bounds; } document.getElementById('bg-image').src = `web/assets/backgrounds/${currentBg}/bg.svg?v=${Date.now()}`; renderSpots(); } 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 renderSpots() { const listEl = document.getElementById('spot-list'); const markersEl = document.getElementById('markers-layer'); const mapContainer = document.getElementById('map-container'); if (!listEl || !markersEl || !mapContainer) return; const w = bounds.xMax - bounds.xMin; const h = bounds.yMax - bounds.yMin; mapContainer.style.aspectRatio = `${w}/${h}`; listEl.innerHTML = ''; markersEl.innerHTML = ''; spots.forEach(spot => { // List item const opt = document.createElement('option'); opt.value = spot.id; opt.textContent = `${spot.label.padEnd(10, ' ')} [${spot.x}, ${spot.y}]`; listEl.appendChild(opt); // Map marker const marker = document.createElement('div'); const xPct = ((spot.x - bounds.xMin) / w) * 100; const yPct = ((bounds.yMax - spot.y) / h) * 100; marker.className = 'spot-marker'; marker.style = `position: absolute; left: ${xPct}%; top: ${yPct}%; transform: translate(-50%, -50%); width: 18px; height: 18px; border-radius: 50%; background: rgba(0, 230, 118, 0.4); border: 2px solid var(--accent-green); cursor: pointer; transition: all 0.2s;`; marker.title = spot.label; marker.addEventListener('click', () => { listEl.value = spot.id; document.getElementById('btn-go').disabled = false; executeMove(spot); }); markersEl.appendChild(marker); }); } async function executeMove(spot) { if (!isHomed) { UI.log('Cannot move: System is not homed! Please click HOME ALL.', 'error'); return; } const mode = document.getElementById('mode-select').value; if (mode !== 'direct') { UI.log(`Mode '${mode}' not fully implemented yet! Using direct move.`, 'warn'); } // Calculate IK const wsCheck = IK.checkWorkspace(spot.x, spot.y); if (!wsCheck.ok) { UI.log(`⛔ ${wsCheck.reason}`, 'error'); return; } const res = IK.solve(spot.x, spot.y); if (!res.reachable) { UI.log(`Target ${spot.label} is unreachable geometrically.`, 'error'); return; } if (IK.armsCrossed(res.theta1, res.theta2)) { UI.log(`Target ${spot.label} rejected: elbows crossed!`, 'error'); return; } const newSteps1 = IK.radToSteps(res.theta1); const newSteps2 = IK.radToSteps(res.theta2); const delta1 = newSteps1 - steps1; const delta2 = newSteps2 - steps2; steps1 = newSteps1; steps2 = newSteps2; saveSteps(); updatePositionReadout(); const cmd1 = `S1${delta1 >= 0 ? '+' : ''}${delta1}`; const cmd2 = `S2${delta2 >= 0 ? '+' : ''}${delta2}`; UI.log(`Moving to ${spot.label} (${spot.x}, ${spot.y})`, 'success'); if (BLE.isConnected()) { try { await BLE.write(cmd1); await BLE.write(cmd2); } catch (e) { UI.log(`BLE error: ${e.message}`, 'error'); } } else { UI.log(`[sim] ${cmd1} ${cmd2}`, 'info'); } } 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_bg', currentBg); loadSpots(); initQuickSequences( currentBg, () => spots, () => ({ steps1, steps2 }), (s1, s2) => { steps1 = s1; steps2 = s2; updatePositionReadout(); } ); }); } // ── Settings Sliders ────────────────────────────────────── const sSpeed = document.getElementById('bc-slider-speed'); const sAccel = document.getElementById('bc-slider-accel'); if (sSpeed) { sSpeed.addEventListener('input', (e) => { Settings.speed = parseInt(e.target.value); document.getElementById('speed-val').textContent = Settings.speed; sendSettings(); }); } if (sAccel) { sAccel.addEventListener('input', (e) => { Settings.accel = parseInt(e.target.value); document.getElementById('accel-val').textContent = Settings.accel; sendSettings(); }); } // ── Homing Logic ────────────────────────────────────────── document.getElementById('btn-force-home').addEventListener('click', async () => { isHomed = true; localStorage.setItem('wiji_homed', 'true'); updateHomingUI(); steps1 = IK.ARM.HOME_STEPS.m1; steps2 = IK.ARM.HOME_STEPS.m2; saveSteps(); updatePositionReadout(); UI.log('Homing all motors...', 'info'); if (BLE.isConnected()) { try { await BLE.write('HOMEALL'); } catch (e) { UI.log(e.message, 'error'); } } else { UI.log('[sim] HOMEALL', 'info'); } }); // ── List Selection ──────────────────────────────────────── const listEl = document.getElementById('spot-list'); const goBtn = document.getElementById('btn-go'); listEl.addEventListener('change', () => { goBtn.disabled = !listEl.value; }); goBtn.addEventListener('click', () => { const selectedId = listEl.value; const spot = spots.find(s => s.id === selectedId); if (spot) { executeMove(spot); } }); // ── 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(); updatePositionReadout(); } }; document.addEventListener('ble:status', onBLEStatus); // Sync position on mount if connected if (BLE.isConnected()) { BLE.write('POS').catch(() => {}); sendSettings(); } else { updatePositionReadout(); } initQuickSequences( currentBg, () => spots, () => ({ steps1, steps2 }), (s1, s2) => { steps1 = s1; steps2 = s2; updatePositionReadout(); } ); eventCleanup.push(() => document.removeEventListener('ble:status', onBLEStatus)); }, unmount() { eventCleanup.forEach(fn => fn()); eventCleanup = []; } };