154 lines
5.9 KiB
JavaScript
154 lines
5.9 KiB
JavaScript
/**
|
||
* WijiBoard – BLE Singleton
|
||
* web/js/ble.js
|
||
*
|
||
* Provides a singleton BLE object with:
|
||
* BLE.connect() → connects to device, gets characteristics
|
||
* BLE.disconnect() → disconnects
|
||
* BLE.write(cmd) → writes ASCII command (write-without-response)
|
||
* BLE.isConnected() → boolean
|
||
* BLE.on(event, cb) → subscribe to 'connected' | 'disconnected' | 'status'
|
||
* BLE.off(event, cb) → unsubscribe
|
||
*
|
||
* Command characteristic (WRITE_NR):
|
||
* S1+<n> → step motor 1 clockwise n steps
|
||
* S1-<n> → step motor 1 counter-clockwise n steps
|
||
* S2+<n> → step motor 2 clockwise n steps
|
||
* S2-<n> → step motor 2 counter-clockwise n steps
|
||
* SPD:<n> → set max speed (steps/sec)
|
||
* ACC:<n> → set acceleration (steps/sec²)
|
||
* HOME → zero both steppers
|
||
* DISABLE → disable steppers (un-hold)
|
||
* POS → request position report
|
||
*
|
||
* Status characteristic (NOTIFY):
|
||
* P:<s1>,<s2> → current step positions for motor 1 & 2
|
||
*/
|
||
|
||
// ── UUIDs – must match src/main.cpp exactly ──────────────────────
|
||
const SERVICE_UUID = '18f3b235-9831-4c75-8ec0-210469b820a0';
|
||
const CMD_UUID = 'cd083b06-4447-4cf3-a7c3-322ecf802ce4';
|
||
const STATUS_UUID = '82e38c5b-d3ab-41d1-861c-b84dc6bb1e03';
|
||
const DEVICE_NAME = 'WijiBoard';
|
||
|
||
const BLE = (() => {
|
||
// ── Internal state ──────────────────────────────────────────
|
||
let device = null;
|
||
let server = null;
|
||
let cmdChar = null;
|
||
let statusChar = null;
|
||
const encoder = new TextEncoder();
|
||
|
||
// ── Tiny event emitter ──────────────────────────────────────
|
||
const listeners = {};
|
||
|
||
function emit(event, ...args) {
|
||
(listeners[event] || []).forEach(cb => {
|
||
try { cb(...args); } catch (e) { console.error('[BLE] listener error', e); }
|
||
});
|
||
}
|
||
|
||
function on(event, cb) {
|
||
if (!listeners[event]) listeners[event] = [];
|
||
listeners[event].push(cb);
|
||
}
|
||
|
||
function off(event, cb) {
|
||
if (!listeners[event]) return;
|
||
listeners[event] = listeners[event].filter(fn => fn !== cb);
|
||
}
|
||
|
||
// ── Disconnect handler ──────────────────────────────────────
|
||
function onDisconnected() {
|
||
console.log('[BLE] Device disconnected');
|
||
device = null;
|
||
server = null;
|
||
cmdChar = null;
|
||
statusChar = null;
|
||
emit('disconnected');
|
||
}
|
||
|
||
// ── Status notification handler ─────────────────────────────
|
||
function onStatusNotification(event) {
|
||
const value = new TextDecoder().decode(event.target.value);
|
||
emit('status', value.trim());
|
||
}
|
||
|
||
// ── connect() ───────────────────────────────────────────────
|
||
async function connect() {
|
||
console.log('[BLE] connect() called');
|
||
if (!navigator.bluetooth) {
|
||
throw new Error('Web Bluetooth API not available. Use Chrome/Edge over HTTPS or localhost.');
|
||
}
|
||
|
||
emit('connecting');
|
||
|
||
console.log('[BLE] Requesting device...');
|
||
device = await navigator.bluetooth.requestDevice({
|
||
filters: [{ name: DEVICE_NAME }],
|
||
optionalServices: [SERVICE_UUID],
|
||
});
|
||
console.log('[BLE] Device selected:', device.name);
|
||
|
||
device.addEventListener('gattserverdisconnected', onDisconnected);
|
||
|
||
console.log('[BLE] Connecting to GATT server...');
|
||
server = await device.gatt.connect();
|
||
console.log('[BLE] GATT server connected');
|
||
|
||
console.log('[BLE] Getting primary service:', SERVICE_UUID);
|
||
const service = await server.getPrimaryService(SERVICE_UUID);
|
||
console.log('[BLE] Primary service found');
|
||
|
||
console.log('[BLE] Getting command characteristic:', CMD_UUID);
|
||
cmdChar = await service.getCharacteristic(CMD_UUID);
|
||
console.log('[BLE] Command characteristic found');
|
||
|
||
// Status notifications (optional – firmware may not have this char yet)
|
||
try {
|
||
console.log('[BLE] Getting status characteristic:', STATUS_UUID);
|
||
statusChar = await service.getCharacteristic(STATUS_UUID);
|
||
console.log('[BLE] Starting notifications...');
|
||
await statusChar.startNotifications();
|
||
statusChar.addEventListener('characteristicvaluechanged', onStatusNotification);
|
||
console.log('[BLE] Notifications started');
|
||
} catch (err) {
|
||
console.warn('[BLE] Status characteristic not available – notifications disabled', err);
|
||
}
|
||
|
||
console.log('[BLE] Connection fully established, emitting connected');
|
||
emit('connected', device.name);
|
||
}
|
||
|
||
// ── disconnect() ────────────────────────────────────────────
|
||
async function disconnect() {
|
||
if (device?.gatt?.connected) {
|
||
device.gatt.disconnect();
|
||
}
|
||
}
|
||
|
||
// ── write() ─────────────────────────────────────────────────
|
||
async function write(cmd) {
|
||
if (!cmdChar) throw new Error('BLE not connected');
|
||
await cmdChar.writeValueWithoutResponse(encoder.encode(cmd));
|
||
emit('sent', cmd);
|
||
}
|
||
|
||
// ── isConnected() ───────────────────────────────────────────
|
||
function isConnected() {
|
||
return !!(device?.gatt?.connected && cmdChar);
|
||
}
|
||
|
||
// ── getDeviceName() ─────────────────────────────────────────
|
||
function getDeviceName() {
|
||
return device?.name ?? null;
|
||
}
|
||
|
||
return {
|
||
on, off, connect, disconnect, write, isConnected, getDeviceName,
|
||
SERVICE_UUID, CMD_UUID, STATUS_UUID, DEVICE_NAME,
|
||
};
|
||
})();
|
||
|
||
export default BLE;
|