252 lines
11 KiB
HTML
252 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>WijiBoard – Control Dashboard</title>
|
||
<meta name="description"
|
||
content="WijiBoard SCARA arm control dashboard. Web Bluetooth interface for the ESP32-C3 stepper controller." />
|
||
|
||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||
<link rel="manifest" href="manifest.json" />
|
||
|
||
<!-- Shared styles -->
|
||
<link rel="stylesheet" href="web/styles/base.css" />
|
||
<link rel="stylesheet" href="web/styles/components.css" />
|
||
</head>
|
||
|
||
<body>
|
||
<div class="app-shell">
|
||
|
||
<!-- ── Sidebar overlay (click to close) ──────────────────────── -->
|
||
<div class="sidebar-overlay" id="sidebar-overlay"></div>
|
||
|
||
<!-- ── Sidebar ───────────────────────────────────────────────── -->
|
||
<nav class="sidebar" id="sidebar" aria-label="Main navigation">
|
||
|
||
<span class="sidebar-section-label">Navigation</span>
|
||
|
||
<a class="sidebar-nav-item active" id="nav-home" href="#home" data-route="home" role="button"
|
||
aria-label="Go to Home">
|
||
<svg class="nav-icon"><use href="web/assets/icons.svg#icon-home"></use></svg>
|
||
Home
|
||
</a>
|
||
|
||
<a class="sidebar-nav-item" id="nav-board-control" href="#board-control" data-route="board-control" role="button"
|
||
aria-label="Go to Board Control">
|
||
<svg class="nav-icon"><use href="web/assets/icons.svg#icon-board"></use></svg>
|
||
Board Control
|
||
</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-sequence-editor" href="#sequence-editor" data-route="sequence-editor" role="button"
|
||
aria-label="Go to Sequence Editor">
|
||
<svg class="nav-icon"><use href="web/assets/icons.svg#icon-sequence"></use></svg>
|
||
Sequence Editor
|
||
</a>
|
||
|
||
<a class="sidebar-nav-item" id="nav-stepper-test" href="#stepper-test" data-route="stepper-test" role="button"
|
||
aria-label="Go to Stepper Test">
|
||
<svg class="nav-icon"><use href="web/assets/icons.svg#icon-stepper"></use></svg>
|
||
Stepper Test
|
||
</a>
|
||
|
||
<!-- Spacer -->
|
||
<div style="flex:1"></div>
|
||
</nav>
|
||
|
||
<!-- ── Main area ──────────────────────────────────────────────── -->
|
||
<div style="display:flex;flex-direction:column;flex:1;overflow:hidden">
|
||
|
||
<!-- Top bar -->
|
||
<header class="top-bar" style="display:flex; justify-content:space-between; align-items:center; padding: 12px 10px; background: var(--surface-1); border-bottom: 1px solid var(--border);" role="banner">
|
||
<!-- Hamburger -->
|
||
<button class="hamburger" id="hamburger-btn" aria-label="Toggle sidebar" aria-expanded="true"
|
||
aria-controls="sidebar">
|
||
<div class="hamburger-bar"></div>
|
||
<div class="hamburger-bar"></div>
|
||
<div class="hamburger-bar"></div>
|
||
</button>
|
||
|
||
<!-- Brand -->
|
||
<div class="topbar-brand">
|
||
<div class="brand-icon" aria-hidden="true">
|
||
<svg width="16" height="16"><use href="web/assets/icons.svg#icon-brand"></use></svg>
|
||
</div>
|
||
WijiBoard
|
||
</div>
|
||
|
||
<!-- BLE actions -->
|
||
<div class="topbar-actions">
|
||
<!-- Disable steppers button (hidden by default) -->
|
||
<button class="btn btn-sm btn-ghost" id="btn-disable-steppers" style="display:none; margin-right:8px; padding:0 8px; color: var(--danger-color);" title="Disable Motors (Requires Rehoming)" aria-label="Disable Steppers">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path>
|
||
<line x1="12" y1="2" x2="12" y2="12"></line>
|
||
</svg>
|
||
</button>
|
||
|
||
<!-- BLE status pill -->
|
||
<div class="ble-status" id="ble-status" aria-live="polite">
|
||
<div class="ble-dot" id="ble-dot"></div>
|
||
<span class="ble-label" id="ble-label">Disconnected</span>
|
||
</div>
|
||
|
||
<!-- Connect / Disconnect button -->
|
||
<button class="btn btn-sm btn-primary" id="btn-ble-connect" aria-label="Connect to WijiBoard via Bluetooth">
|
||
<svg width="13" height="13"><use href="web/assets/icons.svg#icon-ble"></use></svg>
|
||
<span class="btn-text">Connect</span>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Router outlet -->
|
||
<main class="main-content" id="view" role="main" aria-label="Page content">
|
||
<!-- Sections are injected here by router.js -->
|
||
</main>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ── App bootstrap (ES module entry point) ─────────────────── -->
|
||
<script type="module">
|
||
import Router from './web/js/router.js';
|
||
import BLE from './web/js/ble.js';
|
||
import UI from './web/js/ui.js';
|
||
import HomeSection from './web/js/sections/home.js';
|
||
import StepperSection from './web/js/sections/stepper-test.js';
|
||
import BoardControlSection from './web/js/sections/board-control.js';
|
||
import InputTextSection from './web/js/sections/input-text.js';
|
||
import SequenceEditorSection from './web/js/sections/sequence-editor.js';
|
||
|
||
// ── Register routes ────────────────────────────────────────────
|
||
Router.register('home', HomeSection);
|
||
Router.register('stepper-test', StepperSection);
|
||
Router.register('board-control', BoardControlSection);
|
||
Router.register('input-text', InputTextSection);
|
||
Router.register('sequence-editor', SequenceEditorSection);
|
||
|
||
// ── Sidebar toggle logic ───────────────────────────────────────
|
||
const sidebar = document.getElementById('sidebar');
|
||
const overlay = document.getElementById('sidebar-overlay');
|
||
const hamburger = document.getElementById('hamburger-btn');
|
||
const mainContent = document.getElementById('view');
|
||
|
||
let sidebarOpen = true;
|
||
|
||
function setSidebarOpen(open) {
|
||
sidebarOpen = open;
|
||
sidebar.classList.toggle('collapsed', !open);
|
||
overlay.classList.toggle('visible', open && window.innerWidth < 900);
|
||
hamburger.classList.toggle('open', open);
|
||
hamburger.setAttribute('aria-expanded', open);
|
||
|
||
// Shift main content on desktop
|
||
const mc = hamburger.closest('.app-shell')?.querySelector('.main-content');
|
||
if (mc) mc.classList.toggle('sidebar-collapsed', !open);
|
||
}
|
||
|
||
hamburger.addEventListener('click', () => setSidebarOpen(!sidebarOpen));
|
||
overlay.addEventListener('click', () => setSidebarOpen(false));
|
||
|
||
// Close sidebar on small screens when nav link clicked
|
||
document.querySelectorAll('.sidebar-nav-item[data-route]').forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
const route = link.dataset.route;
|
||
Router.navigate('#' + route);
|
||
if (window.innerWidth < 900) setSidebarOpen(false);
|
||
});
|
||
});
|
||
|
||
// Collapse sidebar by default on small screens
|
||
if (window.innerWidth < 900) setSidebarOpen(false);
|
||
|
||
// ── Wire BLE events to shared UI ──────────────────────────────
|
||
UI.initBLEListeners();
|
||
|
||
// Connect button handler (initial state)
|
||
document.getElementById('btn-ble-connect').addEventListener('click', () => {
|
||
if (BLE.isConnected()) {
|
||
BLE.disconnect();
|
||
} else {
|
||
BLE.connect().catch(e => {
|
||
console.error('[BLE Connect Error]', e);
|
||
UI.setButtonLoading('btn-ble-connect', false);
|
||
if (!e.message?.includes('cancelled')) {
|
||
UI.log(`Connect error: ${e.message} (${e.name})`, 'error');
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// ── Init router (last step) ────────────────────────────────────
|
||
Router.init('#view', '#home');
|
||
|
||
// ── Register Service Worker for PWA ────────────────────────────
|
||
if ('serviceWorker' in navigator) {
|
||
window.addEventListener('load', () => {
|
||
navigator.serviceWorker.register('./sw.js').catch(err => {
|
||
console.warn('[PWA] Service Worker registration failed:', err);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── PWA Install Logic ──────────────────────────────────────────
|
||
window.deferredPrompt = null;
|
||
|
||
window.addEventListener('beforeinstallprompt', (e) => {
|
||
// Prevent automatic prompt
|
||
e.preventDefault();
|
||
window.deferredPrompt = e;
|
||
// Dispatch a custom event so sections can update their UI
|
||
window.dispatchEvent(new Event('pwa-installable'));
|
||
});
|
||
|
||
// ── Disable Steppers & Idle Timeout Logic ──────────────────────
|
||
const btnDisable = document.getElementById('btn-disable-steppers');
|
||
btnDisable.addEventListener('click', async () => {
|
||
if (BLE.isConnected()) {
|
||
await BLE.write('DISABLE').catch(e => UI.log(e.message, 'error'));
|
||
localStorage.setItem('wiji_homed', 'false');
|
||
UI.log('Steppers manually disabled. Rehoming required.', 'warn');
|
||
const warningEl = document.getElementById('home-warning');
|
||
if (warningEl) warningEl.style.display = 'block';
|
||
}
|
||
});
|
||
|
||
document.addEventListener('ble:status', (e) => {
|
||
if (e.detail === 'SYS:IDLE_TIMEOUT') {
|
||
localStorage.setItem('wiji_homed', 'false');
|
||
UI.log('Steppers disabled due to 30 mins inactivity. Rehoming required.', 'warn');
|
||
const warningEl = document.getElementById('home-warning');
|
||
if (warningEl) warningEl.style.display = 'block';
|
||
}
|
||
});
|
||
|
||
BLE.on('connected', async () => {
|
||
btnDisable.style.display = 'inline-flex';
|
||
await BLE.write('POS').catch(e => console.warn('POS sync failed:', e));
|
||
});
|
||
BLE.on('disconnected', () => {
|
||
btnDisable.style.display = 'none';
|
||
});
|
||
|
||
// ── Check Web Bluetooth availability ──────────────────────────
|
||
if (!navigator.bluetooth) {
|
||
setTimeout(() => {
|
||
UI.log('⚠ Web Bluetooth not available. Use Chrome or Edge on localhost/HTTPS.', 'error');
|
||
}, 600);
|
||
}
|
||
</script>
|
||
</body>
|
||
|
||
</html> |