Implement Text Input

This commit is contained in:
PROFERIS - Mi³osz Stocki
2026-07-07 10:29:57 +02:00
parent 2e74c7d0c1
commit 366de88ee4
10 changed files with 574 additions and 4 deletions
+1 -2
View File
@@ -1,5 +1,4 @@
- Background Graphic Section - Sequence editor (sequences stored in sequences.json up to boards and in browser local storage for individual boards, manual XY input)
- Input section
Both of these should also have effects like: Normal movement, erratic, jumpy, random Both of these should also have effects like: Normal movement, erratic, jumpy, random
- No Disable steppers but - No Disable steppers but
- Idle timeout - Idle timeout
+14 -2
View File
@@ -366,14 +366,21 @@ The `?v=Date.now()` on imports handles this automatically on page load.
- Move-to-XY numeric input with Go button - Move-to-XY numeric input with Go button
- Board boundary + mechanism exclusion zone visualisation - Board boundary + mechanism exclusion zone visualisation
- Simulation badge (yellow) when BLE not connected - Simulation badge (yellow) when BLE not connected
- [x] Home section (`home.js`) — placeholder landing page, same visual style - [x] Board Control section (`board-control.js`):
- Interactive map with clickable spots
- Speed and Acceleration controls
- Background image selector with config memory
- [x] Input Text section (`input-text.js`):
- Text input to spell words automatically
- Delay, Speed, and Acceleration sliders
- Position overlay marker on background
- [x] Firmware (`src/main.cpp`) — AccelStepper + BLE command parser + position NOTIFY - [x] Firmware (`src/main.cpp`) — AccelStepper + BLE command parser + position NOTIFY
--- ---
## 10. What Remains (Planned Sections) ## 10. What Remains (Planned Sections)
The router currently only has two routes. These sections need to be created: The router currently only has three routes. These sections need to be created/expanded:
### `board-control` section (implemented) ### `board-control` section (implemented)
- Uses predefined background maps (SVG/PNG) with corresponding JSON config files specifying clickable coordinates in physical mm. - Uses predefined background maps (SVG/PNG) with corresponding JSON config files specifying clickable coordinates in physical mm.
@@ -381,6 +388,11 @@ The router currently only has two routes. These sections need to be created:
- Supports movement modes (e.g., Direct, Erratic, Random, Snaky). - Supports movement modes (e.g., Direct, Erratic, Random, Snaky).
- Requires arm to be homed; homing state is persisted across sessions via `localStorage`. - Requires arm to be homed; homing state is persisted across sessions via `localStorage`.
### `input-text` section (implemented)
- Keyboard input that sequentially spells out words on the board.
- Requires background `spots.json` to have `"hasAlphabet": true`.
- Persisted Speed, Acceleration, and Delay settings.
### `sequence-editor` section (not started) ### `sequence-editor` section (not started)
- Record and replay a sequence of moves - Record and replay a sequence of moves
- Save/load sequences to localStorage - Save/load sequences to localStorage
+10
View File
@@ -38,6 +38,14 @@
Board Control Board Control
</a> </a>
<a class="sidebar-nav-item" id="nav-input-text" href="#input-text" data-route="input-text" role="button"
aria-label="Go to Input Text">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
</svg>
Input Text
</a>
<a class="sidebar-nav-item" id="nav-stepper-test" href="#stepper-test" data-route="stepper-test" role="button" <a class="sidebar-nav-item" id="nav-stepper-test" href="#stepper-test" data-route="stepper-test" role="button"
aria-label="Go to Stepper Test"> aria-label="Go to Stepper Test">
<svg class="nav-icon"><use href="web/assets/icons.svg#icon-stepper"></use></svg> <svg class="nav-icon"><use href="web/assets/icons.svg#icon-stepper"></use></svg>
@@ -108,11 +116,13 @@
import HomeSection from './web/js/sections/home.js'; import HomeSection from './web/js/sections/home.js';
import StepperSection from './web/js/sections/stepper-test.js'; import StepperSection from './web/js/sections/stepper-test.js';
import BoardControlSection from './web/js/sections/board-control.js'; import BoardControlSection from './web/js/sections/board-control.js';
import InputTextSection from './web/js/sections/input-text.js';
// ── Register routes ──────────────────────────────────────────── // ── Register routes ────────────────────────────────────────────
Router.register('home', HomeSection); Router.register('home', HomeSection);
Router.register('stepper-test', StepperSection); Router.register('stepper-test', StepperSection);
Router.register('board-control', BoardControlSection); Router.register('board-control', BoardControlSection);
Router.register('input-text', InputTextSection);
// ── Sidebar toggle logic ─────────────────────────────────────── // ── Sidebar toggle logic ───────────────────────────────────────
const sidebar = document.getElementById('sidebar'); const sidebar = document.getElementById('sidebar');
@@ -1,4 +1,5 @@
{ {
"hasAlphabet": true,
"bounds": { "xMin": -150, "xMax": 150, "yMin": -30, "yMax": 145 }, "bounds": { "xMin": -150, "xMax": 150, "yMin": -30, "yMax": 145 },
"spots": [ "spots": [
{ "id": "YES", "label": "YES", "x": -70, "y": 90 }, { "id": "YES", "label": "YES", "x": -70, "y": 90 },
+2
View File
@@ -45,6 +45,7 @@ This file tells the web UI where your letters/targets are located in **physical
```json ```json
{ {
"hasAlphabet": true,
"bounds": { "bounds": {
"xMin": -150, "xMin": -150,
"xMax": 150, "xMax": 150,
@@ -58,6 +59,7 @@ This file tells the web UI where your letters/targets are located in **physical
] ]
} }
``` ```
- **hasAlphabet**: (Optional, boolean) If `true`, this background is used for the "Input Text" feature. The letters in `spots` are used to spell out typed input.
- **bounds**: Defines the physical bounding box (in mm) that your background image represents. This automatically adjusts the UI's aspect ratio and coordinate mapping! - **bounds**: Defines the physical bounding box (in mm) that your background image represents. This automatically adjusts the UI's aspect ratio and coordinate mapping!
- **spots**: Array of target objects. - **spots**: Array of target objects.
- **id**: Unique identifier for the dropdown list. - **id**: Unique identifier for the dropdown list.
@@ -1,4 +1,5 @@
{ {
"hasAlphabet": true,
"bounds": { "xMin": 30, "xMax": 150, "yMin": -30, "yMax": 145 }, "bounds": { "xMin": 30, "xMax": 150, "yMin": -30, "yMax": 145 },
"spots": [ "spots": [
{ "id": "A", "label": "Aa", "x": 55, "y": 120 }, { "id": "A", "label": "Aa", "x": 55, "y": 120 },
+47
View File
@@ -1,6 +1,7 @@
import BLE from '../ble.js'; import BLE from '../ble.js';
import UI from '../ui.js'; import UI from '../ui.js';
import IK from '../kinematics.js'; import IK from '../kinematics.js';
import Settings from '../settings.js';
let eventCleanup = []; let eventCleanup = [];
let isHomed = false; let isHomed = false;
@@ -109,6 +110,21 @@ function buildHTML() {
</select> </select>
</div> </div>
<!-- Settings -->
<div style="background: var(--surface-2); padding: 10px; border-radius: var(--radius-sm); margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<label class="form-label" style="margin:0;">Speed</label>
<span id="speed-val" style="font-size: 0.8rem; color: var(--text-muted);">${Settings.speed}</span>
</div>
<input type="range" id="bc-slider-speed" min="100" max="2000" step="100" value="${Settings.speed}" style="width:100%; margin-bottom: 8px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<label class="form-label" style="margin:0;">Accel</label>
<span id="accel-val" style="font-size: 0.8rem; color: var(--text-muted);">${Settings.accel}</span>
</div>
<input type="range" id="bc-slider-accel" min="50" max="1000" step="50" value="${Settings.accel}" style="width:100%;">
</div>
<div class="form-group" style="margin-bottom: 12px;"> <div class="form-group" style="margin-bottom: 12px;">
<label class="form-label">Movement Mode</label> <label class="form-label">Movement Mode</label>
<select id="mode-select" class="form-input" style="width:100%;"> <select id="mode-select" class="form-input" style="width:100%;">
@@ -180,6 +196,16 @@ async function loadSpots() {
} }
} }
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() { function renderSpots() {
const listEl = document.getElementById('spot-list'); const listEl = document.getElementById('spot-list');
const markersEl = document.getElementById('markers-layer'); const markersEl = document.getElementById('markers-layer');
@@ -294,6 +320,26 @@ export default {
}); });
} }
// ── 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 ────────────────────────────────────────── // ── Homing Logic ──────────────────────────────────────────
document.getElementById('btn-force-home').addEventListener('click', async () => { document.getElementById('btn-force-home').addEventListener('click', async () => {
isHomed = true; isHomed = true;
@@ -348,6 +394,7 @@ export default {
// Sync position on mount if connected // Sync position on mount if connected
if (BLE.isConnected()) { if (BLE.isConnected()) {
BLE.write('POS').catch(() => {}); BLE.write('POS').catch(() => {});
sendSettings();
} else { } else {
updatePositionReadout(); updatePositionReadout();
} }
+12
View File
@@ -69,6 +69,18 @@ const HomeSection = {
</div> </div>
</button> </button>
<button class="sidebar-nav-item" data-route="input-text"
onclick="document.getElementById('nav-input-text').click()"
style="padding:14px 16px;background:var(--surface-2);text-align:left;border:none;cursor:pointer;width:100%;">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
</svg>
<div>
<div style="font-weight:600;font-size:0.875rem;color:var(--text-primary)">Input Text</div>
<div style="font-size:0.75rem;color:var(--text-muted);margin-top:2px">Spell out words automatically</div>
</div>
</button>
<button class="sidebar-nav-item" data-route="stepper-test" <button class="sidebar-nav-item" data-route="stepper-test"
onclick="document.getElementById('nav-stepper-test').click()" onclick="document.getElementById('nav-stepper-test').click()"
style="padding:14px 16px;background:var(--surface-2)"> style="padding:14px 16px;background:var(--surface-2)">
+461
View File
@@ -0,0 +1,461 @@
import BLE from '../ble.js';
import UI from '../ui.js';
import IK from '../kinematics.js';
import Settings from '../settings.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;
}
function saveSteps() {
if (isHomed) {
localStorage.setItem('wiji_steps1', steps1);
localStorage.setItem('wiji_steps2', steps2);
}
}
function buildHTML() {
return `
<style>
.board-grid {
display: grid;
grid-template-columns: 1fr 280px;
gap: 16px;
height: 100%;
align-items: stretch;
}
@media (max-width: 900px) { .board-grid { grid-template-columns: 1fr; } }
.homing-banner {
padding: 10px;
border-radius: var(--radius-sm);
font-weight: bold;
text-align: center;
margin-bottom: 12px;
transition: all 0.3s;
}
.homing-banner.homed {
background: rgba(0, 230, 118, 0.1);
color: var(--accent-green);
border: 1px solid rgba(0, 230, 118, 0.3);
}
.homing-banner.not-homed {
background: rgba(255, 82, 82, 0.1);
color: var(--accent-red, #ff5252);
border: 1px solid rgba(255, 82, 82, 0.3);
}
.pos-marker {
position: absolute;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--accent-blue);
border: 2px solid white;
transform: translate(-50%, -50%);
transition: left 0.1s linear, top 0.1s linear;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 10;
pointer-events: none;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
</style>
<div class="section-page fade-in" style="display:flex; flex-direction:column; height: 100%;">
<div class="section-header">
<h2>Input Text</h2>
<p>Type a word and the arm will spell it out on backgrounds that support an alphabet.</p>
</div>
<div class="board-grid">
<!-- ── Map Area ─────────────────────────────────────── -->
<div class="card" style="display:flex; flex-direction:column; padding: 12px; position:relative;">
<div class="card-header"><span class="card-title">Live Position</span></div>
<div style="flex:1; display:flex; align-items:center; justify-content:center; overflow:hidden;">
<div id="map-container" style="position: relative; width: 100%; max-width: 800px; aspect-ratio: 300/175; background: #111; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.5);">
<img id="bg-image" src="web/assets/backgrounds/${currentBg}/bg.svg" style="width: 100%; height: 100%; object-fit: cover; position: absolute; top:0; left:0; border-radius: 8px;" />
<div id="pos-marker" class="pos-marker" style="display:none;"></div>
</div>
</div>
</div>
<!-- ── Control Panel ──────────────────────────────── -->
<div style="display:flex; flex-direction:column; gap: 12px;">
<!-- Homing Card -->
<div class="card">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
<span class="card-title">System Status</span>
<div style="display:flex; gap: 12px;">
<div style="text-align:right"><div style="font-size:0.6rem; color:var(--text-muted); text-transform:uppercase;">X (mm)</div><div id="pos-x" style="font-weight:bold; font-size:1rem; color:var(--accent-green);">0.0</div></div>
<div style="text-align:right"><div style="font-size:0.6rem; color:var(--text-muted); text-transform:uppercase;">Y (mm)</div><div id="pos-y" style="font-weight:bold; font-size:1rem; color:var(--accent-green);">0.0</div></div>
</div>
</div>
<div id="homing-banner" class="homing-banner ${isHomed ? 'homed' : 'not-homed'}">
${isHomed ? '✓ HOMED' : '⚠ NOT HOMED! Movement Locked.'}
</div>
<button id="btn-force-home" class="btn btn-danger btn-full" style="padding: 10px; font-weight:bold;">
${isHomed ? 'FORCE HOME ALL' : 'HOME ALL MOTORS'}
</button>
</div>
<!-- Controls Card -->
<div class="card" style="flex:1; display:flex; flex-direction:column;">
<div class="card-header"><span class="card-title">Spell Text</span></div>
<div class="form-group" style="margin-bottom: 12px;">
<label class="form-label">Background Map</label>
<select id="bg-select" class="form-input" style="width:100%;">
<option value="default">Default Spirit Board (Landscape)</option>
<option value="portrait-right">Right Side Test (Portrait)</option>
</select>
</div>
<!-- Settings -->
<div style="background: var(--surface-2); padding: 10px; border-radius: var(--radius-sm); margin-bottom: 12px;">
<div class="setting-row">
<label class="form-label" style="margin:0;">Speed</label>
<span id="speed-val" style="font-size: 0.8rem; color: var(--text-muted);">${Settings.speed}</span>
</div>
<input type="range" id="slider-speed" min="100" max="2000" step="100" value="${Settings.speed}" style="width:100%; margin-bottom: 8px;">
<div class="setting-row">
<label class="form-label" style="margin:0;">Accel</label>
<span id="accel-val" style="font-size: 0.8rem; color: var(--text-muted);">${Settings.accel}</span>
</div>
<input type="range" id="slider-accel" min="50" max="1000" step="50" value="${Settings.accel}" style="width:100%; margin-bottom: 8px;">
<div class="setting-row">
<label class="form-label" style="margin:0;">Delay (s)</label>
<span id="delay-val" style="font-size: 0.8rem; color: var(--text-muted);">${(Settings.delay/1000).toFixed(1)}</span>
</div>
<input type="range" id="slider-delay" min="500" max="5000" step="500" value="${Settings.delay}" style="width:100%;">
</div>
<div class="form-group" style="margin-bottom: 12px;">
<label class="form-label">Input Text</label>
<input type="text" id="text-input" class="form-input" style="width:100%; font-size: 1.1rem; padding: 10px; text-transform: uppercase;" placeholder="Enter word to spell...">
</div>
<button id="btn-spell" class="btn btn-success btn-full" style="padding: 12px; font-weight:bold; font-size: 1.1rem;">
SPELL
</button>
<button id="btn-stop" class="btn btn-danger btn-full" style="padding: 12px; font-weight:bold; font-size: 1.1rem; display:none; margin-top: 8px;">
STOP
</button>
</div>
</div>
</div>
</div>
`;
}
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');
}
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()}`;
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}`;
}
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) {
const wsCheck = IK.checkWorkspace(spot.x, spot.y);
if (!wsCheck.ok) {
UI.log(`${wsCheck.reason}`, 'error');
return false;
}
const res = IK.solve(spot.x, spot.y);
if (!res.reachable || IK.armsCrossed(res.theta1, res.theta2)) {
UI.log(`Target ${spot.label} is unreachable or crosses arms.`, 'error');
return false;
}
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();
updatePosMarker();
const cmd1 = `S1${delta1 >= 0 ? '+' : ''}${delta1}`;
const cmd2 = `S2${delta2 >= 0 ? '+' : ''}${delta2}`;
UI.log(`Spelling: ${spot.label}`, 'info');
if (BLE.isConnected()) {
try {
await BLE.write(cmd1);
await BLE.write(cmd2);
} catch (e) {
UI.log(`BLE error: ${e.message}`, 'error');
return false;
}
} else {
console.log(`[sim] ${cmd1} ${cmd2}`);
}
return true;
}
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);
// 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;
inputEl.value = '';
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();
});
}
// ── 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/1000).toFixed(1);
});
// ── 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();
updatePosMarker();
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');
}
});
// ── 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);
}
});
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();
}
eventCleanup.push(() => document.removeEventListener('ble:status', onBLEStatus));
},
unmount() {
isSpelling = false;
eventCleanup.forEach(fn => fn());
eventCleanup = [];
}
};
+25
View File
@@ -0,0 +1,25 @@
/**
* WijiBoard Global Settings
* web/js/settings.js
*
* Manages persisted application settings and defaults.
*/
const DEFAULTS = {
speed: 600,
accel: 100,
delay: 2500
};
export default {
get speed() { return parseInt(localStorage.getItem('wiji_speed') || DEFAULTS.speed); },
set speed(val) { localStorage.setItem('wiji_speed', val); },
get accel() { return parseInt(localStorage.getItem('wiji_accel') || DEFAULTS.accel); },
set accel(val) { localStorage.setItem('wiji_accel', val); },
get delay() { return parseInt(localStorage.getItem('wiji_delay') || DEFAULTS.delay); },
set delay(val) { localStorage.setItem('wiji_delay', val); },
DEFAULTS
};