UNPKG

dvbble

Version:

Bluetooth Low Energy library for DVBuddy devices

1,443 lines (1,263 loc) 48.8 kB
import { BleClient, BleDevice } from "@capacitor-community/bluetooth-le"; import { Capacitor } from "@capacitor/core"; import CBOR from "cbor"; type DeviceType = BluetoothDevice | BleDevice; export default class DVBDeviceBLE { private isConnected = false; private reconnectAttempts = 0; private maxReconnectAttempts = 5; private userRequestedDisconnect = false; private SERVICE_UUID = "8d53dc1d-1db7-4cd3-868b-8a527460aa84"; private CHARACTERISTIC_UUID = "da2e7828-fbce-4e01-ae9e-261174997c48"; private mtu = 140; private device: DeviceType | null = null; private service: BluetoothRemoteGATTService | null = null; private characteristic: BluetoothRemoteGATTCharacteristic | null = null; private connectCallback: (() => void) | null = null; private connectingCallback: (() => void) | null = null; private disconnectCallback: (() => void) | null = null; private messageCallback: | ((message: { op: number; group: number; id: number; data: unknown; length: number; }) => void) | null = null; private imageUploadProgressCallback: | ((progress: { percentage: number }) => void) | null = null; private imageUploadFinishedCallback: (() => void) | null = null; private buffer = new Uint8Array(); private logger = { info: console.log, error: console.error }; private seq = 0; private uploadImage: Uint8Array | null = null; private uploadOffset = 0; private uploadSlot = 0; private uploadIsInProgress = false; private duSerialNumber: string | null = null; private isRegistered = false; // DVB private serviceDVB: BluetoothRemoteGATTService | null = null; private serviceInfo: BluetoothRemoteGATTService | null = null; private listOfFiles: { name: string; length: string }[] = []; private shortname: string | null = null; private serialNumber: string | null = null; private firmwareVersion: string | null = null; private hardwareVersion: string | null = null; private duDeviceUIDVersion: string | null = null; // Serials private DEVICE_INFORMATION_SERVICE_UUID = "0000180a-0000-1000-8000-00805f9b34fb"; private SERIAL_NUMBER_UUID = "dbd00001-ff30-40a5-9ceb-a17358d31999"; private FIRMWARE_REVISION_UUID = "00002a26-0000-1000-8000-00805f9b34fb"; private HARDWARE_REVISION_UUID = "00002a27-0000-1000-8000-00805f9b34fb"; private DVB_SERVICE_UUID = "dbd00001-ff30-40a5-9ceb-a17358d31999"; private LIST_FILES_UUID = "dbd00010-ff30-40a5-9ceb-a17358d31999"; private WRITE_TO_DEVICE_UUID = "dbd00011-ff30-40a5-9ceb-a17358d31999"; private READ_FROM_DEVICE_UUID = "dbd00012-ff30-40a5-9ceb-a17358d31999"; private FORMAT_STORAGE_UUID = "dbd00013-ff30-40a5-9ceb-a17358d31999"; private SHORTNAME_UUID = "dbd00002-ff30-40a5-9ceb-a17358d31999"; private DU_SHORTNAME_UUID = "dbd00002-ff30-40a5-9ceb-a17358d31999"; private DU_DEVICE_UID_UUID = "dbd00003-ff30-40a5-9ceb-a17358d31999"; private DU_SERIAL_NUMBER_UUID = "dbd00001-ff30-40a5-9ceb-a17358d31999"; private DU_SERVER_REGISTRATION_UUID = "dbd00006-ff30-40a5-9ceb-a17358d31999"; private DU_MANUFACTURER_SERIAL_UUID = "dbd00008-ff30-40a5-9ceb-a17358d31999"; private DU_SENSOR_SETTING_UUID = "dbd00007-ff30-40a5-9ceb-a17358d31999"; private manufacturerSerialNumber: string | null = null; private async requestBrowserDevice() { const params = { acceptAllDevices: false, optionalServices: [ this.SERVICE_UUID, this.DVB_SERVICE_UUID, this.DEVICE_INFORMATION_SERVICE_UUID, ], filters: [{ namePrefix: "DVB" }], }; return navigator.bluetooth.requestDevice(params); } private async requestMobileDevice(): Promise<BleDevice> { const params = { services: [ this.SERVICE_UUID, this.DVB_SERVICE_UUID, this.DEVICE_INFORMATION_SERVICE_UUID, ], allowDuplicates: false, name: "", }; return new Promise((resolve, reject) => { BleClient.requestLEScan(params, (result) => { if (result.localName) { BleClient.stopLEScan(); resolve({ deviceId: result.device.deviceId, name: result.localName, } as BleDevice); } }).catch(reject); setTimeout(() => { BleClient.stopLEScan(); reject(new Error("Scan timeout")); }, 10000); }); } private isBleDevice(device: DeviceType): device is BleDevice { return "deviceId" in device; } private isBluetoothDevice(device: DeviceType): device is BluetoothDevice { return "gatt" in device; } private getDeviceDisplayName(device: DeviceType | null): string { if (!device) return "Unknown Device"; return this.isBleDevice(device) ? device.name || device.deviceId : device.name || "Unknown Device"; } public async connect() { if (Capacitor.isNativePlatform()) { try { this.device = await this.requestMobileDevice(); this.logger.info( `Connecting to device ${this.getDeviceDisplayName(this.device)}...`, ); await BleClient.connect(this.device.deviceId); this.logger.info( `Connected to device ${this.getDeviceDisplayName(this.device)}`, ); // Wait for services to be discovered await new Promise(resolve => setTimeout(resolve, 1000)); await BleClient.startNotifications( this.device.deviceId, this.SERVICE_UUID, this.CHARACTERISTIC_UUID, (value) => { this.notification({ target: { value } }); }, ); this.isConnected = true; } catch (error: unknown) { this.logger.error(`Connection error: ${error instanceof Error ? error.message : String(error)}`); this.isConnected = false; await this.disconnected(); throw error; } } else { try { this.device = (await this.requestBrowserDevice()) as BluetoothDevice; if (!this.device) { throw new Error("Failed to get device"); } this.device.addEventListener( "gattserverdisconnected", this.handleDisconnect.bind(this), ); this.logger.info( `Connecting to device ${this.getDeviceDisplayName(this.device)}...`, ); const server = await this.device.gatt?.connect(); if (!server) { throw new Error("Failed to connect to GATT server"); } this.logger.info("Server connected."); // Get all required services with retries const getServiceWithRetry = async (uuid: string, retries = 3): Promise<BluetoothRemoteGATTService> => { for (let i = 0; i < retries; i++) { try { const service = await server.getPrimaryService(uuid); if (service) return service; //eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { this.logger.info(`Retry ${i + 1} getting service ${uuid}`); await new Promise(resolve => setTimeout(resolve, 500)); } } throw new Error(`Failed to get service ${uuid} after ${retries} retries`); }; // Get all services in parallel const [serviceResult, serviceDVBResult, serviceInfoResult] = await Promise.all([ getServiceWithRetry(this.SERVICE_UUID), getServiceWithRetry(this.DVB_SERVICE_UUID), getServiceWithRetry(this.DEVICE_INFORMATION_SERVICE_UUID) ]); this.service = serviceResult; this.serviceDVB = serviceDVBResult; this.serviceInfo = serviceInfoResult; if (!this.serviceDVB) { throw new Error("DVB service not found"); } // Get characteristic with retries const getCharacteristicWithRetry = async (service: BluetoothRemoteGATTService, uuid: string, retries = 3): Promise<BluetoothRemoteGATTCharacteristic> => { for (let i = 0; i < retries; i++) { try { const characteristic = await service.getCharacteristic(uuid); if (characteristic) return characteristic; //eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { this.logger.info(`Retry ${i + 1} getting characteristic ${uuid}`); await new Promise(resolve => setTimeout(resolve, 500)); } } throw new Error(`Failed to get characteristic ${uuid} after ${retries} retries`); }; // Get main characteristic const characteristicResult = await getCharacteristicWithRetry(this.service, this.CHARACTERISTIC_UUID); this.characteristic = characteristicResult; if (this.characteristic) { this.characteristic.addEventListener( "characteristicvaluechanged", this.notification.bind(this), ); await this.characteristic.startNotifications(); } // Wait a bit to ensure everything is ready await new Promise(resolve => setTimeout(resolve, 1000)); this.isConnected = true; } catch (error: unknown) { this.logger.error(`Connection error: ${error instanceof Error ? error.message : String(error)}`); this.isConnected = false; await this.disconnected(); throw error; } } if (this.connectingCallback) this.connectingCallback(); this.logger.info("Service connected."); this.isConnected = true; await this.connected(); if (this.uploadIsInProgress) { this.uploadNext(); } } public getDeviceName() { return this.device?.name; } public async setDeviceInfo() { if (!this.isConnected) { this.logger.error("Device is not connected. Cannot set device info."); return; } try { await this.setFileList(); await this.setShortName(); await this.setSerialNumber(); await this.setHardwareVersion(); await this.setFirmwareVersion(); await this.setDUDeviceUID(); } catch (error: unknown) { this.logger.error(`Error setting device info: ${error instanceof Error ? error.message : String(error)}`); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any private async handleDisconnect(event: any) { this.logger.info("Device disconnected", event); this.isConnected = false; if (!this.userRequestedDisconnect) { this.logger.info("Attempting to reconnect..."); this.reconnectAttempts = 0; this.reconnect(); } else { console.log("User requested disconnect"); await this.disconnected(); } } private async reconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { this.logger.error( "Max reconnection attempts reached. Please try connecting manually.", ); await this.disconnected(); return; } this.reconnectAttempts++; this.logger.info(`Reconnection attempt ${this.reconnectAttempts}...`); try { await this.connect(); } catch (error: unknown) { this.logger.error(`Reconnection error: ${error instanceof Error ? error.message : String(error)}`); setTimeout(() => this.reconnect(), 2000); } } public disconnect() { this.userRequestedDisconnect = true; if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { return Promise.resolve(); } return BleClient.disconnect(this.device.deviceId); } else { if (!this.device || !this.isBluetoothDevice(this.device)) { return Promise.resolve(); } return this.device.gatt?.disconnect(); } } public onConnecting(callback: () => void) { this.connectingCallback = callback; return this; } public onConnect(callback: () => void) { this.connectCallback = callback; return this; } public onDisconnect(callback: () => void) { this.disconnectCallback = callback; return this; } public onMessage( callback: (message: { op: number; group: number; id: number; data: unknown; length: number; }) => void, ) { this.messageCallback = callback; return this; } public onImageUploadProgress( callback: (progress: { percentage: number }) => void, ) { this.imageUploadProgressCallback = callback; return this; } public onImageUploadFinished(callback: () => void) { this.imageUploadFinishedCallback = callback; return this; } private async connected() { this.userRequestedDisconnect = false; if (this.connectCallback) this.connectCallback(); } private async disconnected() { this.logger.info("Disconnected."); if (this.disconnectCallback) this.disconnectCallback(); this.device = null; this.service = null; this.serviceDVB = null; this.serviceInfo = null; this.characteristic = null; this.uploadIsInProgress = false; this.serialNumber = null; this.listOfFiles = []; } private async sendMessage(op: number, group: number, id: number, data?: unknown) { const _flags = 0; let encodedData: number[] = []; if (typeof data !== "undefined") { encodedData = [...new Uint8Array(CBOR.encode(data))]; } const length_lo = encodedData.length & 255; const length_hi = encodedData.length >> 8; const group_lo = group & 255; const group_hi = group >> 8; const message = [ op, _flags, length_hi, length_lo, group_hi, group_lo, this.seq, id, ...encodedData, ]; this.logger.info(`Sending message: op=${op}, group=${group}, id=${id}, length=${encodedData.length}`); if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } const messageArray = Uint8Array.from(message); await BleClient.writeWithoutResponse( this.device.deviceId, this.SERVICE_UUID, this.CHARACTERISTIC_UUID, new DataView(messageArray.buffer), ); } else { if (!this.characteristic) { throw new Error("Characteristic not available"); } await this.characteristic.writeValueWithoutResponse( Uint8Array.from(message), ); } this.seq = (this.seq + 1) % 256; } // eslint-disable-next-line @typescript-eslint/no-explicit-any private notification(event: any) { console.log("message received"); const message = new Uint8Array(event.target.value.buffer); this.buffer = new Uint8Array([...this.buffer, ...message]); const messageLength = this.buffer[2] * 256 + this.buffer[3]; if (this.buffer.length < messageLength + 8) return; this.processMessage(this.buffer.slice(0, messageLength + 8)); this.buffer = this.buffer.slice(messageLength + 8); } private processMessage(message: Uint8Array<ArrayBuffer>) { const [op, _flags, length_hi, length_lo, group_hi, group_lo, _seq, id] = message; void _flags; void _seq; const data = CBOR.decode(message.slice(8).buffer); const length = length_hi * 256 + length_lo; const group = group_hi * 256 + group_lo; this.logger.info(`Processing message - op: ${op}, group: ${group}, id: ${id}, data:`, data); if (group === 1 && id === 1) { if (data.rc === 0 || data.rc === undefined) { if (data.off !== undefined) { this.uploadOffset = data.off; this.logger.info(`Upload offset updated to: ${this.uploadOffset}`); this.uploadNext(); } } else { this.logger.error(`Upload error received: rc=${data.rc}`); this.uploadIsInProgress = false; if (this.imageUploadFinishedCallback) { this.imageUploadFinishedCallback(); } } return; } if (this.messageCallback) this.messageCallback({ op, group, id, data, length }); } public cmdReset() { return this.sendMessage(2, 0, 5); } public smpEcho(message: string | number | object) { return this.sendMessage(2, 0, 0, { d: message }); } public cmdImageState() { return this.sendMessage(0, 1, 0); } public cmdImageErase() { return this.sendMessage(2, 1, 5, {}); } public cmdImageTest(hash: Uint8Array) { return this.sendMessage(2, 1, 0, { hash, confirm: false, }); } public cmdImageConfirm(hash: Uint8Array) { return this.sendMessage(2, 1, 0, { hash, confirm: true, }); } private hash(image: BufferSource) { return crypto.subtle.digest("SHA-256", image); } private async uploadNext() { if (!this.uploadImage) { this.logger.info("No image to upload"); this.uploadIsInProgress = false; return; } if (this.uploadOffset >= this.uploadImage.byteLength) { this.logger.info("Upload complete - reached end of image"); this.uploadIsInProgress = false; if (this.imageUploadFinishedCallback) { this.imageUploadFinishedCallback(); } return; } const nmpOverhead = 8; const message: { data: Uint8Array; off: number; len?: number; sha?: Uint8Array } = { data: new Uint8Array(), off: this.uploadOffset }; if (this.uploadOffset === 0) { this.logger.info(`Starting upload of ${this.uploadImage.byteLength} bytes`); message.len = this.uploadImage.byteLength; message.sha = new Uint8Array(await this.hash(this.uploadImage)); this.logger.info("Image hash:", Array.from(message.sha).map(b => b.toString(16).padStart(2, '0')).join(' ')); } // Calculate progress percentage const progress = Math.floor((this.uploadOffset / this.uploadImage.byteLength) * 100); if (this.imageUploadProgressCallback) { this.imageUploadProgressCallback({ percentage: progress }); } const length = this.mtu - CBOR.encode(message).byteLength - nmpOverhead; message.data = new Uint8Array( this.uploadImage.slice(this.uploadOffset, this.uploadOffset + length), ); this.logger.info(`Sending chunk at offset ${this.uploadOffset}, length ${message.data.length} bytes`); this.uploadOffset += length; try { await this.sendMessage(2, 1, 1, message); this.logger.info("Chunk sent successfully"); } catch (error) { this.logger.error("Error during upload:", error); this.uploadIsInProgress = false; throw error; } } public async cmdUpload(image: Uint8Array, slot = 0) { if (this.uploadIsInProgress) { this.logger.error("Upload is already in progress."); return; } this.logger.info(`Starting firmware upload to slot ${slot}`); this.uploadIsInProgress = true; this.uploadOffset = 0; this.uploadImage = image; this.uploadSlot = slot; try { // First, send the image upload command this.logger.info("Sending image upload command..."); await this.sendMessage(2, 1, 2, { slot, size: image.byteLength, hash: new Uint8Array(await this.hash(image)) }); // Wait a bit for the device to process await new Promise(resolve => setTimeout(resolve, 500)); // Then start sending the actual data chunks this.logger.info("Starting to send image data chunks..."); await this.uploadNext(); } catch (error) { this.logger.error("Error during upload initialization:", error); this.uploadIsInProgress = false; throw error; } } public async imageInfo(image: BufferSource) { const info: { version: string, hash: Uint8Array } = { version: "", hash: new Uint8Array() }; const buffer = ArrayBuffer.isView(image) ? image.buffer : image; const view = new Uint8Array(buffer); if (view.length < 4096) { throw new Error("Image header is too short"); } const version = [view[12], view[13], view[14], view[15]].join("."); info.version = version; const hashStart = 20; const hashEnd = hashStart + 32; const hash = view.slice(hashStart, hashEnd); info.hash = hash; return info; } public getShortName() { return this.shortname; } public async setShortName(shortname?: string) { try { if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } if (!this.serviceDVB) { throw new Error("DVB service not available"); } if (!shortname) { const result = await BleClient.read( this.device.deviceId, this.DVB_SERVICE_UUID, this.SHORTNAME_UUID, ); this.shortname = new TextDecoder().decode(result); } else { const uf8encode = new TextEncoder(); const newShortName = uf8encode.encode(shortname); await BleClient.write( this.device.deviceId, this.DVB_SERVICE_UUID, this.SHORTNAME_UUID, new DataView(newShortName.buffer), ); this.shortname = shortname; } } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } if (!shortname) { const characteristic = await this.serviceDVB.getCharacteristic( this.SHORTNAME_UUID, ); const value = await characteristic.readValue(); this.shortname = new TextDecoder().decode(value); } else { const characteristic = await this.serviceDVB.getCharacteristic( this.SHORTNAME_UUID, ); const uf8encode = new TextEncoder(); const newShortName = uf8encode.encode(shortname); await characteristic.writeValue(newShortName); this.shortname = shortname; } } } catch (error) { this.logger.error(error); } } public getFileList() { return this.listOfFiles; } private async setFileList() { if (!this.isConnected) { this.logger.error("Device is not connected. Cannot set file list."); return; } try { if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } while (true) { const value = await BleClient.read( this.device.deviceId, this.DVB_SERVICE_UUID, this.LIST_FILES_UUID, ); const message = new Uint8Array(value.buffer); if (message.byteLength === 0) return; const byteString = String.fromCharCode(...message); const split_string = byteString.split(";"); const name = split_string[0]; const length = split_string[1]; this.listOfFiles.push({ name, length }); } } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } while (true) { const characteristic = await this.serviceDVB.getCharacteristic( this.LIST_FILES_UUID, ); const value = await characteristic.readValue(); const message = new Uint8Array(value.buffer); if (message.byteLength === 0) return; const byteString = String.fromCharCode(...message); const split_string = byteString.split(";"); const name = split_string[0]; const length = split_string[1]; this.listOfFiles.push({ name, length }); } } } catch (error: unknown) { if(error instanceof Error) { this.logger.error(`Error setting file list: ${error.message}`); } else { this.logger.error(`Error setting file list: ${error}`); } } } public async getFileContent(name: string, progressCallback: (progress: number) => void) { try { const arrayBuffers = []; let offset = 0; let totalSize = 0; const CHUNK_SIZE = 65536; const fileInfo = this.listOfFiles.find((file: { name: string, length: string }) => file.name === name); if (fileInfo) { totalSize = parseInt(fileInfo.length); } const utf8encoder = new TextEncoder(); if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } while (true) { try { const name_bytes: Uint8Array = utf8encoder.encode(`${name};${offset};`); await BleClient.write( this.device.deviceId, this.DVB_SERVICE_UUID, this.WRITE_TO_DEVICE_UUID, new DataView(name_bytes.buffer), ); const display_info = await BleClient.read( this.device.deviceId, this.DVB_SERVICE_UUID, this.READ_FROM_DEVICE_UUID, ); if (display_info.byteLength !== 0) { const array = new Uint8Array(display_info.buffer); array.map((x: number) => arrayBuffers.push(x)); if (arrayBuffers.length % CHUNK_SIZE === 0) { offset += CHUNK_SIZE; this.logger.info(`Reached 64 KB, updating offset: ${offset}`); } if (totalSize > 0 && progressCallback) { const progress = Math.min( 100, Math.round((arrayBuffers.length / totalSize) * 100), ); progressCallback(progress); } } else { break; } } catch (error) { this.logger.error( `Error reading data, retrying at offset ${offset}`, error, ); await new Promise((resolve) => setTimeout(resolve, 500)); // Small delay before retrying } } } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } const write_characteristic = await this.serviceDVB.getCharacteristic( this.WRITE_TO_DEVICE_UUID, ); const read_characteristic = await this.serviceDVB.getCharacteristic( this.READ_FROM_DEVICE_UUID, ); while (true) { try { if (arrayBuffers.length % CHUNK_SIZE === 0) { const name_bytes: Uint8Array = utf8encoder.encode(`${name};${offset};`); await write_characteristic.writeValue(name_bytes); } const display_info = await read_characteristic.readValue(); if (display_info.byteLength !== 0) { const array = new Uint8Array(display_info.buffer); arrayBuffers.push(...array); if (arrayBuffers.length >= offset + CHUNK_SIZE) { offset += CHUNK_SIZE; this.logger.info(`Reached 64 KB, updating offset: ${offset}`); } if (totalSize > 0 && progressCallback) { const progress = Math.min( 100, Math.round((arrayBuffers.length / totalSize) * 100), ); progressCallback(progress); } } else { break; } } catch (error) { this.logger.error( `Error reading data, retrying at offset ${offset}`, error, ); await new Promise((resolve) => setTimeout(resolve, 500)); } } } return new Uint8Array(arrayBuffers); } catch (error) { this.logger.error(error); } } public async formatStorage() { try { if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } await BleClient.read( this.device.deviceId, this.DVB_SERVICE_UUID, this.FORMAT_STORAGE_UUID, ); } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } const characteristic = await this.serviceDVB.getCharacteristic( this.FORMAT_STORAGE_UUID, ); await characteristic.readValue(); } this.logger.info("Files erased"); } catch (error) { this.logger.error(error); } } public getSerialNumber() { return this.serialNumber; } public async setSerialNumber() { try { if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } const serial = await BleClient.read( this.device.deviceId, this.DEVICE_INFORMATION_SERVICE_UUID, this.SERIAL_NUMBER_UUID, ); const serialNumber = new TextDecoder().decode(serial); this.serialNumber = serialNumber; } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } const characteristic = await this.serviceDVB.getCharacteristic( this.SERIAL_NUMBER_UUID, ); const serial = await characteristic.readValue(); const serialNumber = new TextDecoder().decode(serial); this.serialNumber = serialNumber; this.logger.info(`Serial Number: ${this.serialNumber}`); } } catch (error) { this.logger.error(error); } } public getFirmwareVersion() { return this.firmwareVersion; } public async setFirmwareVersion() { try { if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } const firmware = await BleClient.read( this.device.deviceId, this.DEVICE_INFORMATION_SERVICE_UUID, this.FIRMWARE_REVISION_UUID, ); const firmwareVersion = new TextDecoder().decode(firmware); this.logger.info("Firmware Version:", firmwareVersion); this.firmwareVersion = firmwareVersion; } else { if (!this.serviceInfo) { throw new Error("Device information service not available"); } const characteristic = await this.serviceInfo.getCharacteristic( this.FIRMWARE_REVISION_UUID, ); const firmware = await characteristic.readValue(); const firmwareVersion = new TextDecoder().decode(firmware); this.logger.info("Firmware Version:", firmwareVersion); this.firmwareVersion = firmwareVersion; } } catch (error) { this.logger.error("Error getting firmware version:", error); throw error; } } public getHardwareVersion() { return this.hardwareVersion; } public async setHardwareVersion() { try { if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } const hardware = await BleClient.read( this.device.deviceId, this.DEVICE_INFORMATION_SERVICE_UUID, this.HARDWARE_REVISION_UUID, ); const hardwareVersion = new TextDecoder().decode(hardware); this.logger.info("Hardware Version:", hardwareVersion); this.hardwareVersion = hardwareVersion; } else { if (!this.serviceInfo) { throw new Error("Device information service not available"); } const characteristic = await this.serviceInfo.getCharacteristic( this.HARDWARE_REVISION_UUID, ); const hardware = await characteristic.readValue(); const hardwareVersion = new TextDecoder().decode(hardware); this.logger.info("Hardware Version:", hardwareVersion); this.hardwareVersion = hardwareVersion; } } catch (error) { this.logger.error("Error getting firmware version:", error); throw error; } } public getDUDeviceUID() { return this.duDeviceUIDVersion; } public async setDUDeviceUID() { try { if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } const duDeviceUID = await BleClient.read( this.device.deviceId, this.DVB_SERVICE_UUID, this.DU_DEVICE_UID_UUID, ); const duDeviceUIDString = new TextDecoder().decode(duDeviceUID); this.logger.info("DUDeviceUID:", duDeviceUIDString); this.duDeviceUIDVersion = duDeviceUIDString; } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } const characteristic = await this.serviceDVB.getCharacteristic( this.DU_DEVICE_UID_UUID, ); const duDeviceUID = await characteristic.readValue(); const duDeviceUIDVersion = new TextDecoder().decode(duDeviceUID); this.logger.info("DU Device UID Version:", duDeviceUIDVersion); this.duDeviceUIDVersion = duDeviceUIDVersion; } } catch (error) { this.logger.error("Error getting DUDeviceUID", error); throw error; } } public getDUSerialNumber() { return this.duSerialNumber; } public async readDUSerialNumber() { try { if (!this.isConnected) { throw new Error("Device is not connected"); } // Add a small delay to ensure services are ready await new Promise(resolve => setTimeout(resolve, 500)); if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } this.logger.info("Reading DU Serial Number from native platform..."); const duSerialNumber = await BleClient.read( this.device.deviceId, this.DVB_SERVICE_UUID, this.DU_SERIAL_NUMBER_UUID, ); const decodedValue = new TextDecoder().decode(duSerialNumber); this.logger.info("Raw DU Serial Number value:", Array.from(new Uint8Array(duSerialNumber.buffer)).map(b => b.toString(16).padStart(2, '0')).join(' ')); this.logger.info("Decoded DU Serial Number:", decodedValue); if (!decodedValue || decodedValue.trim() === '') { throw new Error("Received empty DU Serial Number"); } this.duSerialNumber = decodedValue; } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } this.logger.info("Reading DU Serial Number from web platform..."); const characteristic = await this.serviceDVB.getCharacteristic( this.DU_SERIAL_NUMBER_UUID, ); if (!characteristic) { throw new Error("DU Serial Number characteristic not found"); } this.logger.info("DU Serial Number characteristic properties:", { read: characteristic.properties.read, write: characteristic.properties.write, writeWithoutResponse: characteristic.properties.writeWithoutResponse, notify: characteristic.properties.notify, indicate: characteristic.properties.indicate, broadcast: characteristic.properties.broadcast, authenticatedSignedWrites: characteristic.properties.authenticatedSignedWrites, reliableWrite: characteristic.properties.reliableWrite, writableAuxiliaries: characteristic.properties.writableAuxiliaries, }); const duSerialNumber = await characteristic.readValue(); const decodedValue = new TextDecoder().decode(duSerialNumber); this.logger.info("Raw DU Serial Number value:", Array.from(new Uint8Array(duSerialNumber.buffer)).map(b => b.toString(16).padStart(2, '0')).join(' ')); this.logger.info("Decoded DU Serial Number:", decodedValue); if (!decodedValue || decodedValue.trim() === '') { throw new Error("Received empty DU Serial Number"); } this.duSerialNumber = decodedValue; } } catch (error) { this.logger.error("Error reading DU Serial Number:", error); this.duSerialNumber = null; throw error; } } public getManufacturerSerialNumber() { return this.manufacturerSerialNumber; } public async readManufacturerSerialNumber() { try { if (!this.isConnected) { throw new Error("Device is not connected"); } // Add a small delay to ensure services are ready await new Promise(resolve => setTimeout(resolve, 500)); if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } this.logger.info("Reading Manufacturer Serial Number from native platform..."); const manufacturerSerial = await BleClient.read( this.device.deviceId, this.DVB_SERVICE_UUID, this.DU_MANUFACTURER_SERIAL_UUID, ); const decodedValue = new TextDecoder().decode(manufacturerSerial); this.logger.info("Raw Manufacturer Serial Number value:", Array.from(new Uint8Array(manufacturerSerial.buffer)).map(b => b.toString(16).padStart(2, '0')).join(' ')); this.logger.info("Decoded Manufacturer Serial Number:", decodedValue); this.manufacturerSerialNumber = decodedValue; } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } this.logger.info("Reading Manufacturer Serial Number from web platform..."); const characteristic = await this.serviceDVB.getCharacteristic( this.DU_MANUFACTURER_SERIAL_UUID, ); if (!characteristic) { throw new Error("Manufacturer Serial Number characteristic not found"); } // Log the characteristic properties for debugging this.logger.info("Manufacturer Serial Number characteristic properties:", { read: characteristic.properties.read, write: characteristic.properties.write, writeWithoutResponse: characteristic.properties.writeWithoutResponse, notify: characteristic.properties.notify, indicate: characteristic.properties.indicate, broadcast: characteristic.properties.broadcast, authenticatedSignedWrites: characteristic.properties.authenticatedSignedWrites, reliableWrite: characteristic.properties.reliableWrite, writableAuxiliaries: characteristic.properties.writableAuxiliaries, }); const manufacturerSerial = await characteristic.readValue(); const decodedValue = new TextDecoder().decode(manufacturerSerial); this.logger.info("Raw Manufacturer Serial Number value:", Array.from(new Uint8Array(manufacturerSerial.buffer)).map(b => b.toString(16).padStart(2, '0')).join(' ')); this.logger.info("Decoded Manufacturer Serial Number:", decodedValue); this.manufacturerSerialNumber = decodedValue; } } catch (error) { this.logger.error("Error reading Manufacturer Serial Number:", error); throw error; } } private async calculateSHA3Signature(serialNumber: string, randomValue: Uint8Array): Promise<Uint8Array> { // Test key as specified in the documentation const dvb_mac_key = new Uint8Array([ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF ]); // Convert serial number to bytes (96-bit = 12 bytes) const serialBytes = new TextEncoder().encode(serialNumber); // Combine the data as specified in the documentation const data = new Uint8Array(34); // 16 + 12 + 4 + 2 bytes data.set(dvb_mac_key, 0); // dvb_mac_key[0-15] data.set(serialBytes.slice(0, 12), 16); // 96-bit Serial Number [0-11] data.set(randomValue, 28); // Random Value [28-31] // Calculate SHA-256 hash (using SHA-256 as SHA3 is not supported by Web Crypto API) const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = new Uint8Array(hashBuffer); // Return first 4 bytes as signature return hashArray.slice(0, 4); } public async verifyDevice() { try { if (!this.isConnected) { throw new Error("Device is not connected"); } // First read the DU Serial Number await this.readDUSerialNumber(); const serialNumber = this.getDUSerialNumber(); if (!serialNumber) { throw new Error("Failed to read DU Serial Number"); } this.logger.info("DU Serial Number for verification:", serialNumber); // Generate random value (4 bytes) const randomValue = new Uint8Array(4); crypto.getRandomValues(randomValue); this.logger.info("Generated random value:", Array.from(randomValue).map(b => b.toString(16).padStart(2, '0')).join(' ')); // Write random value to DU if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } await BleClient.write( this.device.deviceId, this.DVB_SERVICE_UUID, this.DU_SERVER_REGISTRATION_UUID, new DataView(randomValue.buffer), ); } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } const characteristic = await this.serviceDVB.getCharacteristic( this.DU_SERVER_REGISTRATION_UUID, ); if (!characteristic) { throw new Error("Server Registration characteristic not found"); } if (!characteristic.properties.write && !characteristic.properties.writeWithoutResponse) { throw new Error("Characteristic does not support writing"); } if (characteristic.properties.writeWithoutResponse) { await characteristic.writeValueWithoutResponse(randomValue); } else { await characteristic.writeValue(randomValue); } } // Wait a bit for the device to process await new Promise(resolve => setTimeout(resolve, 500)); // Read the response (should be SHA3 signature) let response; if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } response = await BleClient.read( this.device.deviceId, this.DVB_SERVICE_UUID, this.DU_SERVER_REGISTRATION_UUID, ); } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } const characteristic = await this.serviceDVB.getCharacteristic( this.DU_SERVER_REGISTRATION_UUID, ); response = await characteristic.readValue(); } // Calculate expected signature const expectedSignature = await this.calculateSHA3Signature(serialNumber, randomValue); // Compare signatures const responseArray = new Uint8Array(response.buffer); this.logger.info("Received signature length:", responseArray.length); this.logger.info("Received signature:", Array.from(responseArray).map(b => b.toString(16).padStart(2, '0')).join(' ')); this.logger.info("Expected signature:", Array.from(expectedSignature).map(b => b.toString(16).padStart(2, '0')).join(' ')); if (responseArray.length !== 4) { this.logger.error(`Invalid signature length: ${responseArray.length} (expected 4)`); return false; } const isVerified = responseArray.every((byte, index) => byte === expectedSignature[index]); this.logger.info("Device registration:", isVerified ? "OK" : "Error"); return isVerified; } catch (error) { this.logger.error("Error during device registration", error); throw error; } } public async calibrateAccel() { try { if (!this.isConnected) { throw new Error("Device is not connected"); } const command = new Uint8Array([0x10]); if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } await BleClient.write( this.device.deviceId, this.DVB_SERVICE_UUID, this.DU_SENSOR_SETTING_UUID, new DataView(command.buffer), ); } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } const characteristic = await this.serviceDVB.getCharacteristic( this.DU_SENSOR_SETTING_UUID, ); if (!characteristic) { throw new Error("Sensor Setting characteristic not found"); } if (!characteristic.properties.write && !characteristic.properties.writeWithoutResponse) { throw new Error("Characteristic does not support writing"); } if (characteristic.properties.writeWithoutResponse) { await characteristic.writeValueWithoutResponse(command); } else { await characteristic.writeValue(command); } } this.logger.info("ACCEL calibration command sent successfully"); } catch (error) { this.logger.error("Error sending ACCEL calibration command", error); throw error; } } public async calibrateMagn() { try { if (!this.isConnected) { throw new Error("Device is not connected"); } const command = new Uint8Array([0x11]); if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } await BleClient.write( this.device.deviceId, this.DVB_SERVICE_UUID, this.DU_SENSOR_SETTING_UUID, new DataView(command.buffer), ); } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } const characteristic = await this.serviceDVB.getCharacteristic( this.DU_SENSOR_SETTING_UUID, ); if (!characteristic) { throw new Error("Sensor Setting characteristic not found"); } if (!characteristic.properties.write && !characteristic.properties.writeWithoutResponse) { throw new Error("Characteristic does not support writing"); } if (characteristic.properties.writeWithoutResponse) { await characteristic.writeValueWithoutResponse(command); } else { await characteristic.writeValue(command); } } this.logger.info("MAGN calibration command sent successfully"); } catch (error) { this.logger.error("Error sending MAGN calibration command", error); throw error; } } public async testHardware() { try { if (!this.isConnected) { throw new Error("Device is not connected"); } const command = new Uint8Array([0x20]); if (Capacitor.isNativePlatform()) { if (!this.device || !this.isBleDevice(this.device)) { throw new Error("Device not connected or not a BLE device"); } await BleClient.write( this.device.deviceId, this.DVB_SERVICE_UUID, this.DU_SENSOR_SETTING_UUID, new DataView(command.buffer), ); } else { if (!this.serviceDVB) { throw new Error("DVB service not available"); } const characteristic = await this.serviceDVB.getCharacteristic( this.DU_SENSOR_SETTING_UUID, ); if (!characteristic) { throw new Error("Sensor Setting characteristic not found"); } if (!characteristic.properties.write && !characteristic.properties.writeWithoutResponse) { throw new Error("Characteristic does not support writing"); } if (characteristic.properties.writeWithoutResponse) { await characteristic.writeValueWithoutResponse(command); } else { await characteristic.writeValue(command); } } this.logger.info("Hardware test command sent successfully"); } catch (error) { this.logger.error("Error sending hardware test command", error); throw error; } } }