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'; import { IdleEffects } from '../idle-effects.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; currentBg = localStorage.getItem('wiji_bg') || 'default'; Motion.setSteps(steps1, steps2); } 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 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(); 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 }); } IdleEffects.setParkPosition(parkPosition.x, parkPosition.y); spots = parsedSpots; document.getElementById('bg-image').src = `web/assets/backgrounds/${currentBg}/bg.svg?v=${Date.now()}`; renderSpots(); 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 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}`; // Let natural image aspect ratio dictate container height 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; UI.log(`Moving to ${spot.label} (${spot.x}, ${spot.y}) [${mode}]`, 'success'); await Motion.goto(spot.x, spot.y, mode); } export default { mount(container) { syncStateFromStorage(); container.innerHTML = buildHTML(); updateHomingUI(); loadSpots(); // ── Idle Effects Selection ────────────────────────────────── const idleSelect = document.getElementById('bc-idle-mode-select'); if (idleSelect) { idleSelect.value = IdleEffects.getMode(); idleSelect.addEventListener('change', () => { IdleEffects.setMode(idleSelect.value); }); } // ── 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; updatePosMarker(); } ); }); } // ── 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(); 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); }); // ── 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(); 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() { eventCleanup.forEach(fn => fn()); eventCleanup = []; } };