import BLE from '../ble.js';
import UI from '../ui.js';
import IK from '../kinematics.js';
import Settings from '../settings.js';
import SequenceRunner from '../sequence-runner.js';
import Motion from '../motion.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;
let hasAlphabet = false;
let savedBrowserSequences = [];
let bgSequences = [];
let sequence = {
id: "my_custom_sequence",
name: "My Sequence",
favorite: false,
steps: []
};
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';
const savedSeq = localStorage.getItem('wiji_sequence_draft');
if (savedSeq) {
try {
sequence = JSON.parse(savedSeq);
} catch(e) {}
}
const savedSeqs = localStorage.getItem('wiji_saved_sequences');
if (savedSeqs) {
try {
savedBrowserSequences = JSON.parse(savedSeqs);
} catch(e) {}
}
}
function saveSteps() {
if (isHomed) {
localStorage.setItem('wiji_steps1', steps1);
localStorage.setItem('wiji_steps2', steps2);
}
}
function saveSequenceDraft() {
localStorage.setItem('wiji_sequence_draft', JSON.stringify(sequence));
updateJSONPreview();
}
function updateJSONPreview() {
const el = document.getElementById('json-preview');
if (el) {
el.value = JSON.stringify([sequence], null, 2);
}
}
function saveToBrowser() {
const existingIdx = savedBrowserSequences.findIndex(s => s.id === sequence.id);
if (existingIdx >= 0) {
savedBrowserSequences[existingIdx] = JSON.parse(JSON.stringify(sequence));
} else {
savedBrowserSequences.push(JSON.parse(JSON.stringify(sequence)));
}
localStorage.setItem('wiji_saved_sequences', JSON.stringify(savedBrowserSequences));
renderSavedSequences();
UI.log(`Saved '${sequence.name}' to browser storage`, 'success');
}
function buildHTML() {
return `
Copy this JSON array into web/assets/backgrounds/${currentBg}/sequences.json to persist it.
${isHomed ? '✓ HOMED' : '⚠ NOT HOMED! Movement Locked.'}
`;
}
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';
}
}
function renderStepsList() {
const container = document.getElementById('steps-list');
if (!container) return;
if (sequence.steps.length === 0) {
container.innerHTML = `No steps added yet.
`;
return;
}
container.innerHTML = sequence.steps.map((step, idx) => {
let label = '';
let details = '';
if (step.type === 'spot') {
label = `Spot: ${step.id}`;
} else {
label = `XY: (${step.x}, ${step.y})`;
}
if (step.delay !== undefined) {
details = `Delay: ${step.delay}ms`;
}
return `
#${idx+1}
${label} ${details}
`;
}).join('');
// Attach listeners
container.querySelectorAll('.btn-move-up').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx);
if (idx > 0) {
[sequence.steps[idx - 1], sequence.steps[idx]] = [sequence.steps[idx], sequence.steps[idx - 1]];
saveSequenceDraft();
renderStepsList();
}
});
});
container.querySelectorAll('.btn-move-down').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx);
if (idx < sequence.steps.length - 1) {
[sequence.steps[idx + 1], sequence.steps[idx]] = [sequence.steps[idx], sequence.steps[idx + 1]];
saveSequenceDraft();
renderStepsList();
}
});
});
container.querySelectorAll('.btn-delete-step').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx);
sequence.steps.splice(idx, 1);
saveSequenceDraft();
renderStepsList();
});
});
}
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;
hasAlphabet = false;
} else {
parsedSpots = data.spots || [];
if (data.bounds) bounds = data.bounds;
if (data.parkPosition) parkPosition = data.parkPosition;
hasAlphabet = !!data.hasAlphabet;
}
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('text-input-row').style.display = hasAlphabet ? 'grid' : 'none';
document.getElementById('text-input-divider').style.display = hasAlphabet ? 'block' : 'none';
// Populate spot dropdown
const select = document.getElementById('editor-spot-select');
if (select) {
select.innerHTML = '' + spots.map(s =>
``
).join('');
}
updateJSONPreview();
await loadBgSequences();
} catch(e) {
UI.log('Failed to load spots.json', 'error');
}
}
async function loadBgSequences() {
try {
const res = await fetch(`web/assets/backgrounds/${currentBg}/sequences.json?v=${Date.now()}`);
if (res.ok) {
bgSequences = await res.json();
} else {
bgSequences = [];
}
} catch (e) {
bgSequences = [];
}
renderSavedSequences();
}
function renderSavedSequences() {
const container = document.getElementById('saved-seq-list');
if (!container) return;
let html = '';
if (bgSequences.length > 0) {
html += `From sequences.json
`;
html += bgSequences.map((s, idx) => `
${s.name}
`).join('');
}
if (savedBrowserSequences.length > 0) {
html += `From Browser Storage
`;
html += savedBrowserSequences.map((s, idx) => `
`).join('');
}
if (!html) {
html = `No saved sequences
`;
}
container.innerHTML = html;
container.querySelectorAll('.btn-load-seq').forEach(btn => {
btn.addEventListener('click', () => {
const source = btn.dataset.source;
const idx = parseInt(btn.dataset.idx);
const srcObj = source === 'bg' ? bgSequences[idx] : savedBrowserSequences[idx];
if (srcObj) {
sequence = JSON.parse(JSON.stringify(srcObj));
document.getElementById('seq-id').value = sequence.id;
document.getElementById('seq-name').value = sequence.name;
const favCheckbox = document.getElementById('seq-favorite');
if (favCheckbox) favCheckbox.checked = !!sequence.favorite;
saveSequenceDraft();
renderStepsList();
UI.log(`Loaded sequence '${sequence.name}'`, 'info');
}
});
});
container.querySelectorAll('.btn-delete-seq').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx);
if (confirm('Delete this sequence from browser storage?')) {
savedBrowserSequences.splice(idx, 1);
localStorage.setItem('wiji_saved_sequences', JSON.stringify(savedBrowserSequences));
renderSavedSequences();
}
});
});
}
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);
}
}
export default {
mount(container) {
syncStateFromStorage();
container.innerHTML = buildHTML();
updateHomingUI();
loadSpots();
renderStepsList();
// ── Input bindings for metadata ──────────────────────────────
document.getElementById('seq-id').addEventListener('input', (e) => {
sequence.id = e.target.value;
saveSequenceDraft();
});
document.getElementById('seq-name').addEventListener('input', (e) => {
sequence.name = e.target.value;
saveSequenceDraft();
});
const favCheckbox = document.getElementById('seq-favorite');
if (favCheckbox) {
favCheckbox.addEventListener('change', (e) => {
sequence.favorite = e.target.checked;
saveSequenceDraft();
});
}
// ── 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();
});
}
// ── Idle Effects Selection ──────────────────────────────────
const idleSelect = document.getElementById('se-idle-mode-select');
if (idleSelect) {
idleSelect.value = IdleEffects.getMode();
idleSelect.addEventListener('change', () => {
IdleEffects.setMode(idleSelect.value);
});
}
// ── 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;
});
// ── Add Steps Logic ───────────────────────────────────────
document.getElementById('btn-add-spot').addEventListener('click', () => {
const select = document.getElementById('editor-spot-select');
const delayInput = document.getElementById('editor-spot-delay');
const spotId = select.value;
const delay = parseInt(delayInput.value);
if (spotId) {
const step = { type: 'spot', id: spotId };
if (!isNaN(delay) && delay >= 0) step.delay = delay;
sequence.steps.push(step);
saveSequenceDraft();
renderStepsList();
select.value = "";
delayInput.value = "";
}
});
document.getElementById('btn-add-xy').addEventListener('click', () => {
const xInput = document.getElementById('editor-x');
const yInput = document.getElementById('editor-y');
const delayInput = document.getElementById('editor-xy-delay');
const x = parseFloat(xInput.value);
const y = parseFloat(yInput.value);
const delay = parseInt(delayInput.value);
if (!isNaN(x) && !isNaN(y)) {
const step = { type: 'xy', x, y };
if (!isNaN(delay) && delay >= 0) {
step.delay = delay;
}
sequence.steps.push(step);
saveSequenceDraft();
renderStepsList();
xInput.value = "";
yInput.value = "";
delayInput.value = "";
}
});
document.getElementById('btn-add-text').addEventListener('click', () => {
const textInput = document.getElementById('editor-text');
const delayInput = document.getElementById('editor-text-delay');
const text = textInput.value.trim().toUpperCase();
const delay = parseInt(delayInput.value);
if (text) {
const chars = Array.from(text);
let addedCount = 0;
for (const char of chars) {
const spot = spots.find(s =>
(s.label && s.label.toUpperCase() === char) ||
(s.id && s.id.toUpperCase() === char)
);
if (spot) {
const step = { type: 'spot', id: spot.id };
if (!isNaN(delay) && delay >= 0) step.delay = delay;
sequence.steps.push(step);
addedCount++;
}
}
if (addedCount > 0) {
saveSequenceDraft();
renderStepsList();
UI.log(`Added ${addedCount} letter spots`, 'success');
} else {
UI.log('No spots found for entered text', 'warn');
}
textInput.value = "";
delayInput.value = "";
}
});
document.getElementById('btn-clear-seq').addEventListener('click', () => {
if (confirm('Are you sure you want to clear this sequence?')) {
sequence.steps = [];
saveSequenceDraft();
renderStepsList();
}
});
document.getElementById('btn-save-browser').addEventListener('click', () => {
saveToBrowser();
});
// ── Sequence Execution ────────────────────────────────────
const btnPlay = document.getElementById('btn-play-editor');
const btnStop = document.getElementById('btn-stop-editor');
btnPlay.addEventListener('click', async () => {
if (!isHomed) {
UI.log('Cannot move: System is not homed!', 'error');
return;
}
if (sequence.steps.length === 0) return;
btnPlay.style.display = 'none';
btnStop.style.display = 'inline-block';
const modeEl = document.getElementById('seq-mode-select');
const mode = modeEl ? modeEl.value : 'direct';
await sendSettings();
await SequenceRunner.run(sequence, spots, steps1, steps2, mode, (s1, s2) => {
steps1 = s1; steps2 = s2;
updatePositionReadout();
});
btnPlay.style.display = 'inline-block';
btnStop.style.display = 'none';
});
btnStop.addEventListener('click', () => {
SequenceRunner.stop();
});
// ── 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;
saveSteps();
updatePositionReadout();
UI.log('[sim] HOMEALL', 'info');
}
});
document.getElementById('btn-park').addEventListener('click', async () => {
const parkSpot = spots.find(s => s.id === 'PARK');
if (parkSpot) {
if (!isHomed) {
UI.log('Cannot move: System is not homed!', 'error');
return;
}
const modeEl = document.getElementById('seq-mode-select');
const mode = modeEl ? modeEl.value : 'direct';
UI.log(`Moving to Park Position (${parkSpot.x}, ${parkSpot.y})`, 'info');
await Motion.goto(parkSpot.x, parkSpot.y, mode);
}
});
// ── 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);
if (BLE.isConnected()) {
BLE.write('POS').catch(() => {});
sendSettings();
} else {
updatePositionReadout();
}
eventCleanup.push(() => document.removeEventListener('ble:status', onBLEStatus));
},
unmount() {
SequenceRunner.stop();
eventCleanup.forEach(fn => fn());
eventCleanup = [];
}
};