151 lines
5 KiB
TypeScript
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 };
|