@robotical/roboticaljs
Version:
Javascript/TS library for Robotical products
221 lines (191 loc) • 8.15 kB
text/typescript
import { RaftConnector, RaftEventFn, RaftLog, RaftSystemType, RaftSystemUtils, RaftSysTypeManager } from "@robotical/raftjs";
import SettingsManager from "./SettingsManager";
import { SystemTypeCog, SystemTypeMarty } from "./main";
import SystemTypeGeneric from "./SystemTypeGeneric/SystemTypeGeneric";
const sysTypeManager = RaftSysTypeManager.getInstance();
const settingsManager = SettingsManager.getInstance();
try {
sysTypeManager.addSystemType('Cog', () => new SystemTypeCog());
} catch (e) {
RaftLog.warn(`ConnManager - failed to add system type Cog: ${e}`);
}
try {
sysTypeManager.addSystemType('RIC', () => new SystemTypeMarty());
} catch (e) {
RaftLog.warn(`ConnManager - failed to add system type RIC: ${e}`);
}
try {
sysTypeManager.addDefaultSystemType(() => new SystemTypeGeneric());
} catch (e) {
RaftLog.warn(`ConnManager - failed to add default system type: ${e}`);
}
const connectedDevices: BluetoothDevice[] = [];
export default class ConnManager {
// SERVICE UUIDS for external use
static RICUUID = "aa76677e-9cfd-4626-a510-0d305be57c8d";
static COGUUID = "da903f65-d5c2-4f4d-a065-d1aade7af874";
// Singleton
private static _instance: ConnManager;
// Connector
private _connector = new RaftConnector(async (systemUtils: RaftSystemUtils) => {
const systemInfo = await systemUtils.getSystemInfo();
const sysType = sysTypeManager.createSystemType(systemInfo.SystemName) || sysTypeManager.createDefaultSystemType();
sysType?.deviceMgrIF.setMaxDataPointsToStore(settingsManager.getSetting("maxDatapointsToStore"));
return sysType;
});
// Callback on connection event
private _onConnectionEvent: RaftEventFn | null = null;
// Get instance
public static getInstance(): ConnManager {
if (!ConnManager._instance) {
ConnManager._instance = new ConnManager();
}
return ConnManager._instance;
}
// Set connection event listener
public setConnectionEventListener(listener: RaftEventFn) {
this._onConnectionEvent = listener;
}
// Check if connected
public isConnected(): boolean {
return this._connector.isConnected();
}
public getConnector(): RaftConnector {
return this._connector;
}
/**
* @brief Like getBleDevice but the names to ignore are passed in as an array
* @param namesToIgnore Array of names to ignore
* @returns A promise that resolves to a BluetoothDevice or null if no device is found
*/
public async getBleDevicesWithGivenNamesToIgnore(namesToIgnore: { name: string; }[]): Promise<BluetoothDevice | null> {
try {
const dev = await navigator.bluetooth.requestDevice({
filters: [{ services: [ConnManager.RICUUID] }, { services: [ConnManager.COGUUID] }],
optionalServices: [ConnManager.RICUUID, ConnManager.COGUUID],
// @ts-ignore
exclusionFilters: namesToIgnore.length > 0 ? namesToIgnore : undefined,
});
connectedDevices.push(dev);
return dev;
} catch (e) {
RaftLog.error(`getBleDevice - failed to get device ${e}`);
return null;
}
}
private async getBleDevice(uuids: string[]): Promise<BluetoothDevice | null> {
const filtersArray = uuids.map((uuid) => ({ services: [uuid] }));
const namesToIgnore: { name: string }[] = [];
console.log("connectedDevices", connectedDevices);
[...connectedDevices].forEach((dev) => {
console.log("dev.name", dev.name);
console.log("isDevConnected", dev.gatt?.connected);
const isDevConnected = dev.gatt?.connected;
if (isDevConnected && dev.name) {
namesToIgnore.push(({ name: dev.name }));
} else {
connectedDevices.splice(connectedDevices.indexOf(dev), 1);
}
});
console.log("connectedDevices after filtering", connectedDevices);
console.log("namesToIgnore", namesToIgnore);
try {
const dev = await navigator.bluetooth.requestDevice({
filters: filtersArray,
optionalServices: [ConnManager.RICUUID, ConnManager.COGUUID],
// @ts-ignore
exclusionFilters: namesToIgnore.length > 0 ? namesToIgnore : undefined,
});
connectedDevices.push(dev);
return dev;
} catch (e) {
RaftLog.error(`getBleDevice - failed to get device ${e}`);
return null;
}
}
// Get system type
public getSystemType(): RaftSystemType | null {
return this._connector.getSystemType();
}
public async initializeChannel_phoneBLE(): Promise<boolean> {
// Hook up the connector
this._connector.setEventListener((evtType, eventEnum, eventName, eventData) => {
RaftLog.verbose(`ConnManager - event ${eventName}`);
if (this._onConnectionEvent) {
this._onConnectionEvent(evtType, eventEnum, eventName, eventData);
}
});
/////
return this._connector.initializeChannel("PhoneBLE");
}
// Connect
public async connect(method: string, locator: string | object, uuids: string[]): Promise<boolean> {
this._connector.setEventListener((evtType, eventEnum, eventName, eventData) => {
RaftLog.verbose(`ConnManager - event ${eventName}`);
if (this._onConnectionEvent) {
this._onConnectionEvent(evtType, eventEnum, eventName, eventData);
}
});
await this._connector.initializeChannel(method);
// Set the connector websocket suffix
if (method === "WebBLE") {
const dev = await this.getBleDevice(uuids);
return this._connector.connect(dev as object);
}
return this._connector.connect(locator);
}
// Disconnect
public disconnect(): Promise<void> {
return this._connector.disconnect();
}
///////////////////////////////////////////////////////////////////////////////////
/// @brief Generate a UUID for service filtering based on device serial number
/// @param baseUUID Base UUID string (e.g., "aa76677e-9cfd-4626-0000-000000000000")
/// @param serialNo Serial number as an ASCII string (e.g., "1234567890123456")
/// @returns Modified UUID string
public static generateServiceFilterUUID(serialNo: string, baseUUID: string = "aa76677e-9cfd-4626-0000-000000000000"): string {
const UUID_128_BYTES = 16;
// Convert UUID string to byte array
let uuidBytes = ConnManager.uuidToByteArray(baseUUID);
// Convert serial number assuming it is decimal (or hex) digits to bytes
let serialBytes = ConnManager.hexStringToBytes(serialNo);
// Limit to 16 bytes (UUID size)
const bytesToProc = Math.min(serialBytes.length, UUID_128_BYTES);
// console.log(`generateServiceFilterUUID - serialBCD: ${serialBCD} bytesToProc: ${bytesToProc}`);
// XOR the serial BCD bytes with the UUID bytes
for (let i = 0; i < bytesToProc; i++) {
uuidBytes[15 - i] ^= serialBytes[bytesToProc - 1 - i];
}
// Convert back to UUID string format
return ConnManager.byteArrayToUUID(uuidBytes);
}
/////////////////////////////////////////////////////////////////////////////
/// @brief Convert UUID string to byte array (Big Endian order)
public static uuidToByteArray(uuid: string): Uint8Array {
return new Uint8Array(
uuid.replace(/-/g, "") // Remove dashes
.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)) // Convert hex pairs to bytes
);
}
/////////////////////////////////////////////////////////////////////////////
/// @brief Convert byte array back to UUID string
public static byteArrayToUUID(bytes: Uint8Array): string {
return [...bytes]
.map(b => b.toString(16).padStart(2, "0")) // Convert to hex
.join("")
.replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, "$1-$2-$3-$4-$5"); // Format as UUID
}
/////////////////////////////////////////////////////////////////////////////
/// @brief Convert an hex string to bytes
/// @param hex string - e.g. "1234567890123456" -> [0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56]
/// @returns byte array
public static hexStringToBytes(hex: string): Uint8Array {
// Pad to ensure even number of characters
if (hex.length % 2 !== 0) {
hex = "0" + hex;
}
return new Uint8Array(
hex.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
);
}
}