bluetooth-buzzer/src/lib/bluetooth.ts

151 lines
5 KiB
TypeScript

import { bluetoothState, buzzerState } from '../stores';
import { Ntp } from './ntp';
export type DeepronTimerButton = 'OK' | 'ESC' | '+' | '-' | 'RDY' | 'RST' | 'FULL_RESET';
const BLUETOOTH_BASE_UUID = 'deea3136-d728-4f23-823a-104290';
const BLUETOOTH_BUZZER_SERVICE_BASE_UUID = BLUETOOTH_BASE_UUID + '00';
const BLUETOOTH_BUZZER_SERVICE_UUID = BLUETOOTH_BUZZER_SERVICE_BASE_UUID + '0000';
let bluetoothDevice: BluetoothDevice | undefined = undefined;
let bluetoothService: BluetoothRemoteGATTService | undefined = undefined;
let bluetoothCharacteristics: {
currentTime: BluetoothRemoteGATTCharacteristic | undefined;
lastTriggerTime: BluetoothRemoteGATTCharacteristic | undefined;
} = {
currentTime: undefined,
lastTriggerTime: undefined
};
let ntp: Ntp | undefined = undefined;
let timeSyncResponse: ((time: bigint) => void) | undefined = undefined;
let timeSyncInterval: number | undefined = undefined;
function checkAvailability() {
if (!navigator.bluetooth) {
bluetoothState.set('UNAVAILABLE');
return false;
}
bluetoothState.set('DISCONNECTED');
return true;
}
async function startBluetooth() {
bluetoothState.set('CONNECTING');
try {
bluetoothDevice = await navigator.bluetooth.requestDevice({
filters: [
{
services: [BLUETOOTH_BUZZER_SERVICE_UUID]
}
],
optionalServices: [BLUETOOTH_BUZZER_SERVICE_UUID]
});
} catch (error) {
console.error(error);
bluetoothState.set('DISCONNECTED');
return;
}
console.log('> Requested ' + bluetoothDevice.name + ' (' + bluetoothDevice.id + ')');
let server;
try {
server = await bluetoothDevice.gatt!.connect();
} catch (error) {
console.log('> Error connecting to ' + bluetoothDevice.name + ' (' + bluetoothDevice.id + ')');
bluetoothState.set('DISCONNECTED');
return;
}
bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected);
bluetoothService = await server.getPrimaryService(BLUETOOTH_BUZZER_SERVICE_UUID);
bluetoothCharacteristics.currentTime = await bluetoothService.getCharacteristic(BLUETOOTH_BUZZER_SERVICE_BASE_UUID + '000' + (1));
await bluetoothCharacteristics.currentTime.startNotifications();
bluetoothCharacteristics.currentTime.addEventListener('characteristicvaluechanged', handleNotifications);
bluetoothCharacteristics.lastTriggerTime = await bluetoothService.getCharacteristic(BLUETOOTH_BUZZER_SERVICE_BASE_UUID + '000' + (2));
await bluetoothCharacteristics.lastTriggerTime.startNotifications();
bluetoothCharacteristics.lastTriggerTime.addEventListener('characteristicvaluechanged', handleNotifications);
ntp = new Ntp();
timeSyncInterval = setInterval(doOneTimeSync, 1000);
bluetoothState.set('CONNECTED');
}
function onDisconnected() {
console.log('> Bluetooth Device disconnected');
bluetoothDevice = undefined;
bluetoothService = undefined;
ntp = undefined;
timeSyncResponse = undefined;
if (timeSyncInterval !== undefined)
clearInterval(timeSyncInterval);
bluetoothState.set('DISCONNECTED');
}
function handleNotifications(this: BluetoothRemoteGATTCharacteristic, event: Event) {
if (!event.target) return;
const characteristic = event.target as BluetoothRemoteGATTCharacteristic;
if (characteristic.uuid == bluetoothCharacteristics.currentTime?.uuid)
handleNewCurrentTime(new DataView(characteristic.value!.buffer, 0).getBigUint64(0, true));
else if (characteristic.uuid == bluetoothCharacteristics.lastTriggerTime?.uuid)
handleNewLastTriggerTime(new DataView(characteristic.value!.buffer, 0).getBigUint64(0, true));
}
async function doOneTimeSync() {
if (ntp === undefined) return false;
if (bluetoothCharacteristics.currentTime === undefined) return false;
if (timeSyncResponse !== undefined) return false;
let sentAt = performance.now();
const remoteTime = new Promise<bigint>((resolve) => {
timeSyncResponse = resolve;
});
await bluetoothCharacteristics.currentTime.writeValueWithResponse(new ArrayBuffer(0));
let time = await remoteTime;
let receivedAt = performance.now();
ntp.handleTimeSync(BigInt(Math.floor(sentAt)), BigInt(Math.floor(receivedAt)), time);
buzzerState.update(state => {
state.offset = ntp!.currentOffset();
return state;
});
}
function handleNewCurrentTime(time: bigint) {
if (timeSyncResponse === undefined)
return;
timeSyncResponse(time);
timeSyncResponse = undefined;
}
function handleNewLastTriggerTime(time: bigint) {
if (ntp === undefined) return;
const currentOffset = ntp.currentOffset();
if (currentOffset === undefined) return;
buzzerState.update(state => {
state.lastTriggerTime = time - currentOffset;
state.connectionQuality = ntp!.currentAcceptanceRate();
return state;
});
}
export { checkAvailability, startBluetooth, doOneTimeSync };