248 lines
8.2 KiB
TypeScript
248 lines
8.2 KiB
TypeScript
import { get } from 'svelte/store';
|
|
import { injectDummyDevices, isConnected, isPaired, isConnecting, pairedDevices, availableDevices, activeDeviceId, saveConnectionState, loadConnectionState, targetDeviceId, resetLocal, resetRemote } from './store';
|
|
import { BLE } from './protocol/constants';
|
|
import { parseIncomingFrame } from './protocol';
|
|
import { registerTransport, handleTransportDisconnect, handleTransportConnect } from './transport';
|
|
import { addToast, clearAllToasts } from './toast';
|
|
import { SETTINGS } from './settings';
|
|
|
|
let rxCharacteristic: BluetoothRemoteGATTCharacteristic | null = null;
|
|
let txCharacteristic: BluetoothRemoteGATTCharacteristic | null = null;
|
|
let device: BluetoothDevice | null = null;
|
|
|
|
export async function restoreSession() {
|
|
try {
|
|
const devices = await getPairedDevices();
|
|
if (devices.length > 0) {
|
|
isPaired.set(true);
|
|
startScanningAdvertisements(devices);
|
|
|
|
const savedState = loadConnectionState();
|
|
if (savedState && savedState.autoConnect && savedState.transport === 'ble') {
|
|
const targetDev = devices.find(d => d.id === savedState.deviceId);
|
|
if (targetDev) {
|
|
addToast("Versuche automatische Wiederverbindung...", "info");
|
|
await connectBuzzer(targetDev);
|
|
}
|
|
} else if (savedState) {
|
|
targetDeviceId.set(savedState.deviceId);
|
|
device = devices.find(d => d.id === savedState.deviceId) || devices[0];
|
|
} else {
|
|
device = devices[0];
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Session-Wiederherstellung fehlgeschlagen:", error);
|
|
}
|
|
}
|
|
|
|
async function startScanningAdvertisements(devices: BluetoothDevice[]) {
|
|
for (const dev of devices) {
|
|
// Sicherheits-Check für Mock-Objekte
|
|
if (typeof dev.addEventListener !== 'function') continue;
|
|
|
|
dev.addEventListener('advertisementreceived', () => {
|
|
availableDevices.update(set => {
|
|
const newSet = new Set(set);
|
|
newSet.add(dev.id);
|
|
return newSet;
|
|
});
|
|
});
|
|
|
|
try {
|
|
// Auch hier vorher prüfen
|
|
if (typeof dev.watchAdvertisements === 'function') {
|
|
await dev.watchAdvertisements();
|
|
}
|
|
} catch (e) {
|
|
console.warn("Scanning für Gerät nicht möglich:", dev.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function pairBuzzer() {
|
|
try {
|
|
const newDevice = await navigator.bluetooth.requestDevice({
|
|
filters: [{ services: [BLE.SERVICE_UUID] }]
|
|
});
|
|
|
|
isPaired.set(true);
|
|
|
|
const devices = await getPairedDevices();
|
|
startScanningAdvertisements(devices);
|
|
|
|
await connectBuzzer(newDevice);
|
|
|
|
} catch (error) {
|
|
console.error("Pairing abgebrochen oder fehlgeschlagen", error);
|
|
}
|
|
}
|
|
|
|
export async function connectBuzzer(targetDevice?: BluetoothDevice | Event) {
|
|
console.debug("connectBuzzer aufgerufen");
|
|
if (targetDevice instanceof Event) {
|
|
targetDevice = undefined;
|
|
}
|
|
|
|
if (get(isConnecting)) return;
|
|
|
|
if (targetDevice) {
|
|
if (device && device.gatt?.connected && device.id !== (targetDevice as BluetoothDevice).id) {
|
|
device.gatt.disconnect();
|
|
}
|
|
device = targetDevice as BluetoothDevice;
|
|
}
|
|
|
|
clearAllToasts();
|
|
|
|
if (!device) return;
|
|
|
|
device.removeEventListener('gattserverdisconnected', handleDisconnect);
|
|
device.addEventListener('gattserverdisconnected', handleDisconnect);
|
|
|
|
isConnecting.set(true);
|
|
console.debug("connectBuzzer: Verbindungsversuch mit", device.name);
|
|
|
|
const connectionTimeout = setTimeout(() => {
|
|
isConnecting.set(false);
|
|
addToast("Verbindungsaufbau fehlgeschlagen (Timeout).", "error", true);
|
|
handleTransportDisconnect();
|
|
}, SETTINGS.bluetooth.connectionTimeoutMs);
|
|
|
|
try {
|
|
const server = await device.gatt?.connect();
|
|
if (!server) throw new Error("GATT Server nicht gefunden");
|
|
|
|
const service = await server.getPrimaryService(BLE.SERVICE_UUID);
|
|
rxCharacteristic = await service.getCharacteristic(BLE.RX_UUID);
|
|
txCharacteristic = await service.getCharacteristic(BLE.TX_UUID);
|
|
|
|
await txCharacteristic.startNotifications();
|
|
txCharacteristic.addEventListener('characteristicvaluechanged', handleIncomingData);
|
|
|
|
clearTimeout(connectionTimeout);
|
|
|
|
// Hardware-Setup ist fertig -> Übergabe an den Transport-Layer
|
|
activeDeviceId.set(device.id);
|
|
saveConnectionState({ transport: 'ble', deviceId: device.id, autoConnect: true });
|
|
targetDeviceId.set(device.id);
|
|
|
|
addToast(`Verbunden mit <b>${device.name}</b>`, "success");
|
|
|
|
// Führt die Requests aus und setzt $isConnected = true
|
|
await handleTransportConnect(sendBleFrame);
|
|
|
|
} catch (error) {
|
|
clearTimeout(connectionTimeout);
|
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
console.error("Bluetooth Fehler während connectBuzzer:", errMsg);
|
|
|
|
if (errMsg.includes("no longer in range")) {
|
|
addToast("Geräte-Referenz veraltet (Out of Range). Bitte über den 'Pairen'-Button neu autorisieren.", "error", true);
|
|
|
|
if (device) {
|
|
const deadId = device.id;
|
|
pairedDevices.update(devices => devices.filter(d => d.id !== deadId));
|
|
availableDevices.update(set => {
|
|
const newSet = new Set(set);
|
|
newSet.delete(deadId);
|
|
return newSet;
|
|
});
|
|
}
|
|
} else {
|
|
addToast("Verbindungsfehler: " + errMsg, "error");
|
|
}
|
|
|
|
handleTransportDisconnect();
|
|
} finally {
|
|
isConnecting.set(false);
|
|
}
|
|
}
|
|
|
|
export function disconnectBuzzer() {
|
|
console.debug("disconnectBuzer aufgerufen");
|
|
|
|
if (device) {
|
|
saveConnectionState({
|
|
transport: 'ble',
|
|
deviceId: device.id,
|
|
autoConnect: false
|
|
});
|
|
|
|
isConnected.set(false);
|
|
|
|
if (device.gatt?.connected) {
|
|
device.gatt.disconnect();
|
|
}
|
|
}
|
|
resetRemote();
|
|
addToast("Verbindung mit <b>" + device.name + "</b> getrennt", "info");
|
|
}
|
|
|
|
export async function forgetDevice(targetDevice: BluetoothDevice) {
|
|
console.debug("forgetDevice aufgerufen");
|
|
try {
|
|
if (targetDevice.gatt?.connected) {
|
|
targetDevice.gatt.disconnect();
|
|
}
|
|
await targetDevice.forget();
|
|
addToast(`Gerät ${targetDevice.name} vergessen`, "success");
|
|
|
|
const devices = await getPairedDevices();
|
|
if (devices.length === 0) {
|
|
isPaired.set(false);
|
|
}
|
|
} catch (error) {
|
|
console.error("Fehler beim Löschen des Geräts:", error);
|
|
addToast("Konnte Gerät nicht entfernen", "error", true);
|
|
}
|
|
}
|
|
|
|
export async function getPairedDevices() {
|
|
let rawDevices: BluetoothDevice[] = [];
|
|
|
|
// 1. Physische Geräte abrufen, falls die API verfügbar ist
|
|
if ('bluetooth' in navigator && 'getDevices' in navigator.bluetooth) {
|
|
try {
|
|
rawDevices = await navigator.bluetooth.getDevices();
|
|
} catch (error) {
|
|
console.error("Fehler beim Abrufen der gekoppelten Geräte:", error);
|
|
}
|
|
}
|
|
|
|
// 2. Physische Geräte in den Store schreiben
|
|
pairedDevices.set(rawDevices);
|
|
|
|
// 3. Testdaten anfügen
|
|
injectDummyDevices();
|
|
|
|
// 4. Den aktualisierten Store-Inhalt (inkl. Dummies) für die weiterverarbeitenden Funktionen zurückgeben
|
|
return get(pairedDevices);
|
|
}
|
|
|
|
function handleDisconnect() {
|
|
console.debug("handleDisconnect aufgerufen");
|
|
|
|
if (get(isConnected)) {
|
|
addToast("Verbindung zu Buzzer verloren", "warning");
|
|
}
|
|
|
|
resetRemote();
|
|
registerTransport(null);
|
|
rxCharacteristic = null;
|
|
txCharacteristic = null;
|
|
}
|
|
|
|
function handleIncomingData(event: Event) {
|
|
const target = event.target as BluetoothRemoteGATTCharacteristic;
|
|
if (target.value) {
|
|
parseIncomingFrame(target.value, (buffer) => sendBleFrame(buffer));
|
|
}
|
|
}
|
|
|
|
export async function sendBleFrame(buffer: ArrayBuffer) {
|
|
// TODO: MTU Check einfügen!
|
|
if (!rxCharacteristic) return;
|
|
await rxCharacteristic.writeValueWithoutResponse(buffer);
|
|
}
|