feat: introduce platform-specific BLE backends

solves lack of Linux support in #2
This commit is contained in:
2026-06-08 22:38:42 +07:00
parent 5c33891968
commit 6db0faefb4
6 changed files with 1521 additions and 187 deletions
+1
View File
@@ -27,6 +27,7 @@
"ink": "^6.6.0", "ink": "^6.6.0",
"ink-big-text": "^2.0.0", "ink-big-text": "^2.0.0",
"ink-spinner": "^5.0.0", "ink-spinner": "^5.0.0",
"node-ble": "^1.13.0",
"react": "^19.2.4", "react": "^19.2.4",
"sharp": "^0.34.5" "sharp": "^0.34.5"
}, },
+952
View File
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
export interface DiscoveredDevice {
address: string;
name: string | null;
}
export interface BleCharacteristic {
uuid: string;
write(data: Buffer, withoutResponse: boolean): Promise<void>;
subscribe(onData: (data: Buffer) => void): Promise<void>;
unsubscribe(): Promise<void>;
}
export interface BleBackend {
readonly name: string;
init(): Promise<void>;
scanFor(
matcher: (device: DiscoveredDevice) => boolean,
timeoutMs: number,
): Promise<DiscoveredDevice | null>;
stopScan(): Promise<void>;
connect(address: string): Promise<void>;
onDisconnect(callback: () => void): void;
discoverCharacteristics(
writeUUID: string,
notifyUUID: string,
normalizeUUID: (uuid: string) => string,
): Promise<{ write: BleCharacteristic; notify: BleCharacteristic }>;
disconnect(): Promise<void>;
}
export type BleBackendType = "noble" | "dbus";
/**
* Uses dbus on Linux and noble on other platforms.
* You can override with BEAMBOXCTL_BLE_BACKEND=noble|dbus.
*/
export function resolveBackendType(): BleBackendType {
const override = process.env.BEAMBOXCTL_BLE_BACKEND?.toLowerCase().trim();
if (override === "noble" || override === "dbus") {
return override;
}
return process.platform === "linux" ? "dbus" : "noble";
}
+244
View File
@@ -0,0 +1,244 @@
import { createBluetooth } from "node-ble";
import type NodeBle from "node-ble";
import { ConnectionError } from "../../utils/errors.ts";
import { logger } from "../../utils/logger.ts";
import type {
BleBackend,
BleCharacteristic,
DiscoveredDevice,
} from "../backend.ts";
const POLL_INTERVAL_MS = 1000;
class DBusCharacteristic implements BleCharacteristic {
private valueChangedListener: ((buffer: Buffer) => void) | null = null;
constructor(
private characteristic: NodeBle.GattCharacteristic,
public readonly uuid: string,
) {}
async write(data: Buffer, withoutResponse: boolean): Promise<void> {
if (withoutResponse) {
await this.characteristic.writeValueWithoutResponse(data);
} else {
await this.characteristic.writeValueWithResponse(data);
}
}
async subscribe(onData: (data: Buffer) => void): Promise<void> {
this.valueChangedListener = onData;
this.characteristic.on("valuechanged", onData);
await this.characteristic.startNotifications();
}
async unsubscribe(): Promise<void> {
if (this.valueChangedListener) {
this.characteristic.removeListener(
"valuechanged",
this.valueChangedListener,
);
this.valueChangedListener = null;
}
await this.characteristic.stopNotifications().catch(() => {});
}
}
export class DBusBackend implements BleBackend {
readonly name = "dbus";
private bluetooth: NodeBle.Bluetooth | null = null;
private destroy: (() => void) | null = null;
private adapter: NodeBle.Adapter | null = null;
private device: NodeBle.Device | null = null;
private scanning = false;
async init(): Promise<void> {
if (this.bluetooth) {
return;
}
const { bluetooth, destroy } = createBluetooth();
this.bluetooth = bluetooth;
this.destroy = destroy;
try {
this.adapter = await bluetooth.defaultAdapter();
} catch {
throw new ConnectionError(
"No Bluetooth adapter found (is bluetoothd running?)",
);
}
if (!(await this.adapter.isPowered())) {
throw new ConnectionError("Bluetooth adapter is not powered on");
}
}
private async ensureDiscovering(): Promise<void> {
if (!this.adapter) {
throw new ConnectionError("Bluetooth adapter not initialized");
}
if (!(await this.adapter.isDiscovering())) {
await this.adapter.startDiscovery();
}
this.scanning = true;
}
async scanFor(
matcher: (device: DiscoveredDevice) => boolean,
timeoutMs: number,
): Promise<DiscoveredDevice | null> {
if (!this.adapter) {
throw new ConnectionError("Bluetooth adapter not initialized");
}
await this.ensureDiscovering();
const adapter = this.adapter;
// Keep re-checking unnamed devices on every poll instead of giving up on first sight.
// Not sure if this is the best way to handle this.
const named = new Map<string, string | null>();
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const addresses = await adapter.devices();
for (const address of addresses) {
if (named.get(address)) {
continue;
}
let name: string | null = null;
try {
const remoteDevice = await adapter.getDevice(address);
name = await remoteDevice.getName().catch(async () => {
return await remoteDevice.getAlias().catch(() => null);
});
} catch {
// device vanished mid-scan??
}
if (name && named.get(address) !== name) {
logger.debug(`Discovered device: ${name} (${address})`);
}
named.set(address, name);
const candidate: DiscoveredDevice = { address, name };
if (matcher(candidate)) {
await this.stopScan();
this.device = await adapter.getDevice(address);
return candidate;
}
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
await this.stopScan();
return null;
}
async stopScan(): Promise<void> {
if (this.adapter && this.scanning) {
await this.adapter.stopDiscovery().catch(() => {});
this.scanning = false;
}
}
async connect(address: string): Promise<void> {
if (!this.adapter) {
throw new ConnectionError("Bluetooth adapter not initialized");
}
if (!this.device) {
this.device = await this.adapter.getDevice(address);
}
if (await this.device.isConnected().catch(() => false)) {
await this.device.disconnect().catch(() => {});
await new Promise((resolve) => setTimeout(resolve, 500));
}
await this.device.connect();
}
onDisconnect(callback: () => void): void {
this.device?.once("disconnect", callback);
}
async discoverCharacteristics(
writeUUID: string,
notifyUUID: string,
normalizeUUID: (uuid: string) => string,
): Promise<{ write: BleCharacteristic; notify: BleCharacteristic }> {
if (!this.device) {
throw new ConnectionError("Not connected to device");
}
const gattServer = await this.device.gatt();
const serviceUuids = await gattServer.services();
const normalizedWriteUuid = normalizeUUID(writeUUID);
const normalizedNotifyUuid = normalizeUUID(notifyUUID);
let writeChar: { char: NodeBle.GattCharacteristic; uuid: string } | null =
null;
let notifyChar: { char: NodeBle.GattCharacteristic; uuid: string } | null =
null;
for (const serviceUuid of serviceUuids) {
const service = await gattServer.getPrimaryService(serviceUuid);
const charUuids = await service.characteristics();
for (const charUuid of charUuids) {
const normalizedCharUuid = normalizeUUID(charUuid);
if (normalizedCharUuid === normalizedWriteUuid && !writeChar) {
writeChar = {
char: await service.getCharacteristic(charUuid),
uuid: charUuid,
};
}
if (normalizedCharUuid === normalizedNotifyUuid && !notifyChar) {
notifyChar = {
char: await service.getCharacteristic(charUuid),
uuid: charUuid,
};
}
}
}
if (!writeChar || !notifyChar) {
throw new ConnectionError("Could not find required characteristics");
}
return {
write: new DBusCharacteristic(writeChar.char, writeChar.uuid),
notify: new DBusCharacteristic(notifyChar.char, notifyChar.uuid),
};
}
async disconnect(): Promise<void> {
try {
await this.stopScan();
if (this.device) {
this.device.removeAllListeners();
if (await this.device.isConnected().catch(() => false)) {
await this.device.disconnect().catch(() => {});
}
this.device = null;
}
this.destroy?.();
this.destroy = null;
this.bluetooth = null;
this.adapter = null;
} catch {
// best-effort cleanup
}
}
}
+202
View File
@@ -0,0 +1,202 @@
import noble from "@stoprocent/noble";
import type { Peripheral, Characteristic } from "@stoprocent/noble";
import { ConnectionError } from "../../utils/errors.ts";
import { logger } from "../../utils/logger.ts";
import type {
BleBackend,
BleCharacteristic,
DiscoveredDevice,
} from "../backend.ts";
class NobleCharacteristic implements BleCharacteristic {
constructor(private characteristic: Characteristic) {}
get uuid(): string {
return this.characteristic.uuid;
}
async write(data: Buffer, withoutResponse: boolean): Promise<void> {
await this.characteristic.writeAsync(data, withoutResponse);
}
async subscribe(onData: (data: Buffer) => void): Promise<void> {
await this.characteristic.subscribeAsync();
this.characteristic.on("data", onData);
}
async unsubscribe(): Promise<void> {
this.characteristic.removeAllListeners();
await this.characteristic.unsubscribeAsync().catch(() => {});
}
}
export class NobleBackend implements BleBackend {
readonly name = "noble";
private peripheral: Peripheral | null = null;
private initialized = false;
async init(): Promise<void> {
if (this.initialized) {
return;
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new ConnectionError("Bluetooth adapter initialization timeout"));
}, 10000);
const checkState = (state: string) => {
if (state === "poweredOn") {
clearTimeout(timeout);
this.initialized = true;
resolve();
} else if (state === "poweredOff") {
clearTimeout(timeout);
reject(new ConnectionError("Bluetooth adapter is not powered on"));
} else if (state === "unsupported") {
clearTimeout(timeout);
reject(
new ConnectionError("Bluetooth is not supported on this device"),
);
} else if (state === "unauthorized") {
clearTimeout(timeout);
reject(new ConnectionError("Bluetooth access not authorized"));
}
};
if ((noble as any).state === "poweredOn") {
clearTimeout(timeout);
this.initialized = true;
resolve();
return;
}
noble.on("stateChange", checkState);
});
}
async scanFor(
matcher: (device: DiscoveredDevice) => boolean,
timeoutMs: number,
): Promise<DiscoveredDevice | null> {
return new Promise((resolve) => {
let found = false;
const timeoutId = setTimeout(async () => {
if (!found) {
await this.stopScan();
noble.removeListener("discover", onDiscover);
resolve(null);
}
}, timeoutMs);
const onDiscover = async (peripheral: Peripheral) => {
const device: DiscoveredDevice = {
address: peripheral.address || peripheral.id,
name: peripheral.advertisement.localName ?? null,
};
logger.debug(
`Discovered device: ${device.name ?? "(unnamed)"} (${device.address})`,
);
if (matcher(device)) {
found = true;
clearTimeout(timeoutId);
await this.stopScan();
noble.removeListener("discover", onDiscover);
this.peripheral = peripheral;
resolve(device);
}
};
noble.on("discover", onDiscover);
noble.startScanningAsync([], false).catch((err: Error) => {
clearTimeout(timeoutId);
noble.removeListener("discover", onDiscover);
resolve(null);
});
});
}
async stopScan(): Promise<void> {
await noble.stopScanningAsync().catch(() => {});
}
async connect(address: string): Promise<void> {
if (!this.peripheral) {
throw new ConnectionError(`No discovered peripheral for ${address}`);
}
if (
this.peripheral.state === "connected" ||
this.peripheral.state === "connecting"
) {
await this.peripheral.disconnectAsync().catch(() => {});
await new Promise((resolve) => setTimeout(resolve, 500));
}
await this.peripheral.connectAsync();
}
onDisconnect(callback: () => void): void {
this.peripheral?.once("disconnect", callback);
}
async discoverCharacteristics(
writeUUID: string,
notifyUUID: string,
normalizeUUID: (uuid: string) => string,
): Promise<{ write: BleCharacteristic; notify: BleCharacteristic }> {
if (!this.peripheral) {
throw new ConnectionError("Not connected to device");
}
const { services } =
await this.peripheral.discoverAllServicesAndCharacteristicsAsync();
const normalizedWriteUuid = normalizeUUID(writeUUID);
const normalizedNotifyUuid = normalizeUUID(notifyUUID);
let writeChar: Characteristic | null = null;
let notifyChar: Characteristic | null = null;
for (const service of services) {
for (const char of service.characteristics) {
const normalizedCharUuid = normalizeUUID(char.uuid);
if (normalizedCharUuid === normalizedWriteUuid) {
writeChar = char;
}
if (normalizedCharUuid === normalizedNotifyUuid) {
notifyChar = char;
}
}
}
if (!writeChar || !notifyChar) {
throw new ConnectionError("Could not find required characteristics");
}
return {
write: new NobleCharacteristic(writeChar),
notify: new NobleCharacteristic(notifyChar),
};
}
async disconnect(): Promise<void> {
try {
if (this.peripheral) {
this.peripheral.removeAllListeners();
await this.peripheral.disconnectAsync().catch(() => {});
this.peripheral = null;
}
noble.removeAllListeners();
await noble.stopScanningAsync().catch(() => {});
} catch {
// best-effort cleanup
}
}
}
+71 -187
View File
@@ -1,13 +1,20 @@
import { EventEmitter } from "node:events"; import { EventEmitter } from "node:events";
EventEmitter.defaultMaxListeners = 20; EventEmitter.defaultMaxListeners = 20;
import noble from "@stoprocent/noble";
import type { Peripheral, Characteristic } from "@stoprocent/noble";
import type { BLEConfig, ProtocolConfig } from "../protocol/index.ts"; import type { BLEConfig, ProtocolConfig } from "../protocol/index.ts";
import { PacketType } from "../protocol/index.ts"; import { PacketType } from "../protocol/index.ts";
import { DeviceNotFoundError, ConnectionError } from "../utils/errors.ts"; import { DeviceNotFoundError, ConnectionError } from "../utils/errors.ts";
import { PayloadBuilder, ResponseParser } from "../protocol/index.ts"; import { PayloadBuilder, ResponseParser } from "../protocol/index.ts";
import { logger, LogEventType } from "../utils/logger.ts"; import { logger, LogEventType } from "../utils/logger.ts";
import type { BleBackend, BleCharacteristic } from "./backend.ts";
import { resolveBackendType } from "./backend.ts";
import { NobleBackend } from "./backends/noble-backend.ts";
import { DBusBackend } from "./backends/dbus-backend.ts";
function createBackend(): BleBackend {
const type = resolveBackendType();
return type === "dbus" ? new DBusBackend() : new NobleBackend();
}
/** /**
* Handles notifications from the BeamBox device * Handles notifications from the BeamBox device
@@ -214,14 +221,15 @@ class NotificationHandler {
* Manages BLE connection and data transfer to BeamBox device * Manages BLE connection and data transfer to BeamBox device
*/ */
export class BleUploader { export class BleUploader {
private peripheral: Peripheral | null = null; private backend: BleBackend;
private writeCharacteristic: Characteristic | null = null; private writeCharacteristic: BleCharacteristic | null = null;
private notifyCharacteristic: Characteristic | null = null; private notifyCharacteristic: BleCharacteristic | null = null;
private notificationHandler: NotificationHandler; private notificationHandler: NotificationHandler;
private payloadBuilder: PayloadBuilder; private payloadBuilder: PayloadBuilder;
private chunkDelay: number; private chunkDelay: number;
private verbose: boolean = false; private verbose: boolean = false;
private isInitialized: boolean = false; private isInitialized: boolean = false;
private connectedAddress: string | null = null;
constructor( constructor(
private deviceAddress: string | null, private deviceAddress: string | null,
@@ -234,50 +242,20 @@ export class BleUploader {
this.verbose = verbose; this.verbose = verbose;
this.notificationHandler = new NotificationHandler(verbose); this.notificationHandler = new NotificationHandler(verbose);
this.payloadBuilder = new PayloadBuilder(protocolConfig); this.payloadBuilder = new PayloadBuilder(protocolConfig);
this.backend = createBackend();
logger.debug(`Using BLE backend: ${this.backend.name}`);
} }
/** /**
* Initialize Bluetooth adapter and wait for powered on state * Initialize Bluetooth adapter and wait for it to be ready
*/ */
private async initBluetooth(): Promise<void> { private async initBluetooth(): Promise<void> {
if (this.isInitialized) { if (this.isInitialized) {
return; return;
} }
return new Promise((resolve, reject) => { await this.backend.init();
const timeout = setTimeout(() => { this.isInitialized = true;
reject(new ConnectionError("Bluetooth adapter initialization timeout"));
}, 10000);
const checkState = (state: string) => {
if (state === "poweredOn") {
clearTimeout(timeout);
this.isInitialized = true;
resolve();
} else if (state === "poweredOff") {
clearTimeout(timeout);
reject(new ConnectionError("Bluetooth adapter is not powered on"));
} else if (state === "unsupported") {
clearTimeout(timeout);
reject(
new ConnectionError("Bluetooth is not supported on this device"),
);
} else if (state === "unauthorized") {
clearTimeout(timeout);
reject(new ConnectionError("Bluetooth access not authorized"));
}
};
// Check current state first
if ((noble as any).state === "poweredOn") {
clearTimeout(timeout);
this.isInitialized = true;
resolve();
return;
}
noble.on("stateChange", checkState);
});
} }
/** /**
@@ -315,50 +293,26 @@ export class BleUploader {
logger.info("Starting device scan...", LogEventType.SCAN_START); logger.info("Starting device scan...", LogEventType.SCAN_START);
return new Promise((resolve) => { const targetName = this.bleConfig.deviceName.toLowerCase();
const timeout = this.bleConfig.scanTimeout * 1000; const timeout = this.bleConfig.scanTimeout * 1000;
let found = false;
const timeoutId = setTimeout(async () => { const found = await this.backend.scanFor((device) => {
if (!found) { return (
await noble.stopScanningAsync(); !!device.name && device.name.toLowerCase().includes(targetName)
noble.removeListener("discover", onDiscover); );
resolve(null); }, timeout);
}
}, timeout);
const onDiscover = async (peripheral: Peripheral) => { if (!found) {
const name = peripheral.advertisement.localName; return null;
}
if ( logger.info(
name && `Found device: ${found.name} (${found.address})`,
name.toLowerCase().includes(this.bleConfig.deviceName.toLowerCase()) LogEventType.DEVICE_FOUND,
) { { name: found.name, address: found.address },
found = true; );
clearTimeout(timeoutId);
await noble.stopScanningAsync();
noble.removeListener("discover", onDiscover);
const address = peripheral.address || peripheral.id; return found.address;
logger.info(
`Found device: ${name} (${address})`,
LogEventType.DEVICE_FOUND,
{ name, address },
);
this.peripheral = peripheral;
resolve(address);
}
};
noble.on("discover", onDiscover);
noble.startScanningAsync([], false).catch((err: Error) => {
clearTimeout(timeoutId);
logger.error(`Scan error: ${err}`);
resolve(null);
});
});
} }
/** /**
@@ -369,7 +323,7 @@ export class BleUploader {
try { try {
await this.initBluetooth(); await this.initBluetooth();
if (!this.peripheral) { if (!this.connectedAddress) {
if (!this.deviceAddress) { if (!this.deviceAddress) {
logger.info("Scanning for device...", LogEventType.SCAN_START); logger.info("Scanning for device...", LogEventType.SCAN_START);
const address = await this.findDevice(); const address = await this.findDevice();
@@ -385,74 +339,30 @@ export class BleUploader {
LogEventType.SCAN_START, LogEventType.SCAN_START,
); );
const foundPeripheral = await new Promise<Peripheral | null>( const targetAddress = this.deviceAddress.toLowerCase();
(resolve) => { const timeout = this.bleConfig.scanTimeout * 1000;
const timeout = this.bleConfig.scanTimeout * 1000;
let found = false;
const timeoutId = setTimeout(async () => { const found = await this.backend.scanFor(
if (!found) { (device) => device.address.toLowerCase() === targetAddress,
await noble.stopScanningAsync(); timeout,
noble.removeListener("discover", onDiscover);
resolve(null);
}
}, timeout);
const onDiscover = async (peripheral: Peripheral) => {
const address = peripheral.address || peripheral.id;
if (
address.toLowerCase() === this.deviceAddress?.toLowerCase()
) {
found = true;
clearTimeout(timeoutId);
await noble.stopScanningAsync();
noble.removeListener("discover", onDiscover);
resolve(peripheral);
}
};
noble.on("discover", onDiscover);
noble.startScanningAsync([], false).catch(() => {
clearTimeout(timeoutId);
resolve(null);
});
},
); );
if (!foundPeripheral) { if (!found) {
throw new DeviceNotFoundError( throw new DeviceNotFoundError(
`Could not find device with address '${this.deviceAddress}'`, `Could not find device with address '${this.deviceAddress}'`,
); );
} }
this.peripheral = foundPeripheral;
} }
} }
// Stop scanning before connecting await this.backend.stopScan();
await noble.stopScanningAsync().catch(() => {});
// Check peripheral state before connecting
logger.debug(
`Peripheral state before connect: ${this.peripheral!.state}`,
);
// If already connected, disconnect first to ensure clean state
if (
this.peripheral!.state === "connected" ||
this.peripheral!.state === "connecting"
) {
logger.debug(
"Device already connected/connecting, disconnecting first...",
);
await this.peripheral!.disconnectAsync().catch(() => {});
await this.sleep(500); // Brief delay after disconnect
}
// Connect to device // Connect to device
logger.info("Connecting to device...", LogEventType.CONNECT_START); logger.info("Connecting to device...", LogEventType.CONNECT_START);
const connectStartTime = Date.now(); const connectStartTime = Date.now();
await this.peripheral!.connectAsync(); await this.backend.connect(this.deviceAddress!);
this.connectedAddress = this.deviceAddress;
const connectDuration = Date.now() - connectStartTime; const connectDuration = Date.now() - connectStartTime;
logger.info( logger.info(
@@ -461,9 +371,9 @@ export class BleUploader {
); );
// Handle disconnect events // Handle disconnect events
this.peripheral!.once("disconnect", () => { this.backend.onDisconnect(() => {
logger.info("Device disconnected"); logger.info("Device disconnected");
this.peripheral = null; this.connectedAddress = null;
this.writeCharacteristic = null; this.writeCharacteristic = null;
this.notifyCharacteristic = null; this.notifyCharacteristic = null;
}); });
@@ -473,10 +383,7 @@ export class BleUploader {
// Setup notifications // Setup notifications
if (this.notifyCharacteristic) { if (this.notifyCharacteristic) {
await this.notifyCharacteristic.subscribeAsync(); await this.notifyCharacteristic.subscribe((data: Buffer) => {
// Listen for notifications
this.notifyCharacteristic.on("data", (data: Buffer) => {
this.notificationHandler.handleNotification(data); this.notificationHandler.handleNotification(data);
}); });
} }
@@ -509,13 +416,6 @@ export class BleUploader {
* Discover required characteristics on the device * Discover required characteristics on the device
*/ */
private async discoverCharacteristics(): Promise<void> { private async discoverCharacteristics(): Promise<void> {
if (!this.peripheral) {
throw new ConnectionError("Not connected to device");
}
const { services } =
await this.peripheral.discoverAllServicesAndCharacteristicsAsync();
const normalizedWriteUuid = this.normalizeUUID( const normalizedWriteUuid = this.normalizeUUID(
this.bleConfig.writeCharacteristicUUID, this.bleConfig.writeCharacteristicUUID,
); );
@@ -526,38 +426,25 @@ export class BleUploader {
logger.debug(`Looking for write UUID: ${normalizedWriteUuid}`); logger.debug(`Looking for write UUID: ${normalizedWriteUuid}`);
logger.debug(`Looking for notify UUID: ${normalizedNotifyUuid}`); logger.debug(`Looking for notify UUID: ${normalizedNotifyUuid}`);
// Iterate through services and their characteristics const { write, notify } = await this.backend.discoverCharacteristics(
for (const service of services) { this.bleConfig.writeCharacteristicUUID,
logger.debug(`Service: ${service.uuid}`); this.bleConfig.notifyCharacteristicUUID,
(uuid) => this.normalizeUUID(uuid),
);
for (const char of service.characteristics) { this.writeCharacteristic = write;
const normalizedCharUuid = this.normalizeUUID(char.uuid); this.notifyCharacteristic = notify;
logger.debug(
` Characteristic: ${char.uuid} (normalized: ${normalizedCharUuid})`,
);
if (normalizedCharUuid === normalizedWriteUuid) { logger.debug(
this.writeCharacteristic = char; `Found write characteristic: ${write.uuid}`,
logger.debug( LogEventType.DISCOVER_CHAR,
`Found write characteristic: ${char.uuid}`, { type: "write", uuid: write.uuid },
LogEventType.DISCOVER_CHAR, );
{ type: "write", uuid: char.uuid }, logger.debug(
); `Found notify characteristic: ${notify.uuid}`,
} LogEventType.DISCOVER_CHAR,
if (normalizedCharUuid === normalizedNotifyUuid) { { type: "notify", uuid: notify.uuid },
this.notifyCharacteristic = char; );
logger.debug(
`Found notify characteristic: ${char.uuid}`,
LogEventType.DISCOVER_CHAR,
{ type: "notify", uuid: char.uuid },
);
}
}
}
if (!this.writeCharacteristic || !this.notifyCharacteristic) {
throw new ConnectionError("Could not find required characteristics");
}
} }
/** /**
@@ -566,16 +453,13 @@ export class BleUploader {
public async disconnect(): Promise<void> { public async disconnect(): Promise<void> {
try { try {
if (this.notifyCharacteristic) { if (this.notifyCharacteristic) {
this.notifyCharacteristic.removeAllListeners(); await this.notifyCharacteristic.unsubscribe();
await this.notifyCharacteristic.unsubscribeAsync().catch(() => {});
}
if (this.peripheral) {
this.peripheral.removeAllListeners();
await this.peripheral.disconnectAsync().catch(() => {});
} }
noble.removeAllListeners(); await this.backend.disconnect();
await noble.stopScanningAsync().catch(() => {}); this.connectedAddress = null;
this.writeCharacteristic = null;
this.notifyCharacteristic = null;
} catch (error) { } catch (error) {
logger.warning(`Error during disconnect: ${error}`); logger.warning(`Error during disconnect: ${error}`);
} }
@@ -618,7 +502,7 @@ export class BleUploader {
this.notificationHandler.logSentPacket(packet, "Image info packet"); this.notificationHandler.logSentPacket(packet, "Image info packet");
// Write without response // Write without response
await this.writeCharacteristic.writeAsync(packet, true); await this.writeCharacteristic.write(packet, true);
await this.sleep(this.protocolConfig.imageInfoDelay * 1000); await this.sleep(this.protocolConfig.imageInfoDelay * 1000);
} }
@@ -696,7 +580,7 @@ export class BleUploader {
} }
// Write without response // Write without response
await this.writeCharacteristic.writeAsync(packet, true); await this.writeCharacteristic.write(packet, true);
if (this.notificationHandler.errorFlag) { if (this.notificationHandler.errorFlag) {
logger.error("Device error flag set; aborting send."); logger.error("Device error flag set; aborting send.");