/** * Device Manager - Handles device discovery, connection, and provisioning */ import { Subscription } from 'react-native-ble-plx'; import EventEmitter from 'eventemitter3'; import type { DiscoveredDevice, ConnectedDevice, DeviceStatus, ScanOptions, ReconnectOptions, Environment, RecordingState, StartRecordingOptions, StopRecordingOptions, WiFiConfigGrant, WiFiCredentials, WiFiConfigResult, WiFiStatusInfo, DeviceWiFiScanResult, DeviceConnectionSettings } from '../models/Device'; import type { DeviceManagerEvents } from '../models/Status'; /** * Async fetcher invoked by {@link DeviceManager.requestStartRecording} and * {@link DeviceManager.requestStopRecording} when called with the fetcher * shape (instead of a pre-fetched grant blob). The SDK reads the device's * P6 session nonce, then calls this with the nonce so the caller can bind * the backend-issued grant to the current BLE session. * * @param nonce_d 16-byte hex session nonce, or null on pre-P6 firmware. * @returns Base64-encoded 171-byte HPKE grant blob. */ export type RecordingGrantFetcher = (nonce_d: string | null) => Promise; /** Options for {@link DeviceManager.provision}. */ export interface ProvisionOptions { /** * Optional fetcher invoked when the device rejects the token write with * ALREADY_PAIRED — the device is still paired with a stale token from a * previous owner. The fetcher should call the backend's deprovision-grant * endpoint (e.g. POST /devices/{id}/grant with scope=deprovision) and * return the base64 grant blob. The SDK then performs an opcode-0x05 BLE * deprovision (no reboot) and retries the token write on the same connection. * * @param nonce_d Hex-encoded P6 session nonce read from the device, or null * on pre-P6 firmware. Pass through to the backend so it can bind the grant * to the current BLE session. */ fetchDeprovisionGrant?: (nonce_d: string | null) => Promise; } /** * Device Manager class */ export declare class DeviceManager extends EventEmitter { private bleManager; private connectedDevices; private statusSubscriptions; private nonceSubscriptions; private nonceCache; private reconnectRegistry; private recordingStateCache; private recordingStatePending; private isInitialized; private autoReconnectEnabled; private autoReconnectSerial; private autoReconnectTimer; private autoReconnectAttempting; private userDisconnected; constructor(); /** * Initialize the device manager */ initialize(): Promise; /** * Set up Bluetooth event listeners */ private setupBleListeners; /** * Start scanning for Bota devices */ startScan(options?: ScanOptions): Promise; /** * Stop scanning for devices */ stopScan(): void; /** * Get list of discovered devices */ getDiscoveredDevices(): DiscoveredDevice[]; /** * Get list of connected devices */ getConnectedDevices(): ConnectedDevice[]; /** * Connect to a discovered device */ connect(device: DiscoveredDevice): Promise; /** * Disconnect from a device (user-initiated) */ disconnect(device: ConnectedDevice): Promise; /** * Enable auto-reconnect for a device by serial number. * When enabled, the SDK will automatically attempt to reconnect when: * - The device disconnects unexpectedly (out of range, power loss) * - Bluetooth is toggled off and back on * * Auto-reconnect is paused when disconnect() is called (user-initiated). * Call enableAutoReconnect() again or reconnect() to resume. */ enableAutoReconnect(serialNumber: string): void; /** * Disable auto-reconnect */ disableAutoReconnect(): void; private startAutoReconnectLoop; private stopAutoReconnectLoop; /** * Reconnect to a previously paired device by serial number. * * The SDK stores the Bluetooth name and peripheral ID from the initial pairing. * This method scans for nearby devices, matches by stored Bluetooth name, * stored peripheral ID, or Bota-prefix fallback, then connects. * * @param serialNumber - Serial number of the device to reconnect to * @param options - Optional reconnection options * @returns The connected device * @throws DeviceError.notFound if no matching device is found during scan */ reconnect(serialNumber: string, options?: ReconnectOptions): Promise; /** * @deprecated Firmware ≥ P5 rejects the unauthenticated BLE factory-reset opcode (0x01). * Use the backend unbind/delete flow instead: `DELETE /v1/projects/{projectId}/devices/{id}` * or `POST /v1/projects/{projectId}/devices/{id}/unbind`. The backend emits a `factory_reset` * heartbeat command that the device processes over WiFi/4G. * * This method will fail silently on P5+ firmware (device returns INVALID_TOKEN). */ factoryReset(device: ConnectedDevice): Promise; /** * Check if a device is connected */ isConnected(deviceId: string): boolean; /** * Provision a device with a token */ provision(device: ConnectedDevice, deviceToken: string, environment?: Environment, options?: ProvisionOptions): Promise; /** * Single provisioning attempt: writes API endpoint + token, waits for result, * throws a typed ProvisioningError on failure. No retry. */ private writeProvisioningOnce; /** * Check if a device is provisioned */ isProvisioned(device: ConnectedDevice): Promise; /** * Read the device's Ed25519 public key (PK_D) from the Auth service. * Returns the 32-byte key as a 64-char lowercase hex string, or null if * the firmware does not expose the Auth service (legacy devices). */ readPublicKey(device: ConnectedDevice): Promise; /** * Subscribe to P6 session nonce notifications and cache the value. * Called once per connect. Handles the case where CHAR_AUTH_NONCE is NOTIFY-only * and the firmware sends the nonce when the CCC subscription is enabled. * * Also runs an explicit READ probe at subscription time so the logs always show * which transport (READ vs NOTIFY) the firmware actually serves, and at what length. */ private subscribeToNonce; /** * Read the P6 session nonce from the device's AUTH_NONCE characteristic. * * P6 firmware generates a fresh 16-byte nonce on every BLE connection. * Pass this value as `nonce_d` to the backend grant endpoint so the grant * is cryptographically bound to this session. Returns null on pre-P6 firmware * (characteristic absent) — fall back to legacy grant flow without nonce. * * Checks the notification cache first (populated by subscribeToNonce on connect), * then falls back to a direct BLE read for firmware that exposes the nonce as READ. */ readAuthNonce(device: ConnectedDevice): Promise; /** * Get device status */ getStatus(device: ConnectedDevice): Promise; /** * Subscribe to device status updates */ subscribeToStatus(device: ConnectedDevice, callback: (status: DeviceStatus) => void): () => void; /** * Sync time to device */ syncTime(deviceId: string): Promise; /** * Read connection settings from device via Bluetooth DEVICE_SETTINGS characteristic. * Returns parsed settings (enabled connections + network preference). */ readConnectionSettings(device: ConnectedDevice): Promise; /** * Write connection settings to device via Bluetooth DEVICE_SETTINGS characteristic. * Serializes settings to 8-byte binary format. */ writeConnectionSettings(device: ConnectedDevice, settings: DeviceConnectionSettings): Promise; private readSerialNumber; private readFirmwareVersion; private readHardwareRevision; private readPairingState; setApiEndpoint(device: ConnectedDevice, environment: Environment): Promise; private writeApiEndpoint; /** * P4: Deliver the per-device X.509 leaf certificate + RSA-2048 private * key issued by the Bota Device CA at bind time. The device persists * both in syscfg and presents the cert on every WiFi/4G TLS handshake * (mTLS) — the API gateway authenticates by chain validation against * the Bota Device Root CA. * * Both PEMs are concatenated as a single payload separated by a newline: * * \n * * The firmware splits on the first `-----BEGIN ENCRYPTED|RSA|PRIVATE` * marker. Chunked over the same chunk-header protocol used for the * device token (max chunk = MTU-5; chunks prefixed with [index, total]). * * Call once after a successful `provision()`, before the first WiFi/4G * upload from the device. * * @param device - Connected device * @param certPem - PEM-encoded leaf certificate from bind response * @param privkeyPem - PEM-encoded RSA private key from bind response */ deliverCert(device: ConnectedDevice, certPem: string, privkeyPem: string): Promise; /** * P10: Write the backend's X25519 BLE-e2e public key (32 raw bytes) to the * device's `CHAR_BACKEND_PUBKEY`. The device persists it in syscfg and uses * it to hybrid-encrypt audio chunks before BLE transfer; the app then * relays ciphertext it cannot read to `POST /v1/recordings/{id}/upload-relay`. * * Call this once after a successful `provision()` and again after any * pubkey rotation (re-fetch from `GET /v1/ble-pubkey`). * * @param device - Connected device * @param pubkey - 32-byte raw X25519 public key (Buffer or 32-byte Uint8Array) */ deliverBackendPubkey(device: ConnectedDevice, pubkey: Uint8Array): Promise; private writeDeviceToken; private waitForProvisioningResult; /** * Write an HPKE grant blob to the device's CHAR_DEVICE_COMMAND characteristic. * The device decrypts and verifies the blob; subsequent recording commands within * the grant TTL are honoured. * * @param device - Connected device * @param grantBlob - Base64-encoded 171-byte HPKE grant blob from backend */ writeGrant(device: ConnectedDevice, grantBlob: string): Promise; /** * Request to start recording on a device remotely. * * Two call shapes: * 1. `requestStartRecording(device, grantBlob)` — caller already fetched the * grant blob (legacy / explicit usage). * 2. `requestStartRecording(device, fetchGrant)` — pass an async fetcher and * the SDK runs the full P6 atomic sequence: read CHAR_AUTH_NONCE → invoke * `fetchGrant(nonce)` → write grant → send opcode. This keeps the nonce * read and grant fetch on a single BLE connection (avoids races where the * nonce rotates between read and grant write). * * @param device - Connected device * @param grantOrFetcher - Either a base64-encoded 171-byte grant blob, or an * async fetcher invoked with the device's session * nonce (16-byte hex, or null on pre-P6 firmware) * that returns the grant blob. * @param _options - Reserved for future use * @returns Recording command result */ requestStartRecording(device: ConnectedDevice, grantOrFetcher: string | RecordingGrantFetcher, _options?: StartRecordingOptions): Promise<{ success: boolean; error?: string; }>; /** Read the device session nonce, then invoke the caller's fetcher. * Encapsulates the P6 nonce-read-then-fetch sequence so callers can pass a * one-line fetcher to {@link requestStartRecording} / {@link requestStopRecording}. */ private fetchGrantWithNonce; /** * Request to stop recording on a device remotely. * * Same two call shapes as {@link requestStartRecording} — accepts either a * pre-fetched grant blob or a fetcher that the SDK invokes after reading * the device's session nonce. * * @param device - Connected device * @param grantOrFetcher - Base64 grant blob OR a fetcher (see {@link RecordingGrantFetcher}) * @param _options - Reserved for future use * @returns Recording command result */ requestStopRecording(device: ConnectedDevice, grantOrFetcher: string | RecordingGrantFetcher, _options?: StopRecordingOptions): Promise<{ success: boolean; error?: string; }>; /** * Deprovision a device via a grant-gated BLE command (P5.B). * * Writes the recording grant blob, then sends opcode 0x05 to CHAR_DEVICE_COMMAND. * The device verifies the grant and clears its pairing state + token, allowing * re-provisioning without a physical firmware reflash. * * Call this BEFORE revoking the device token on the backend. Once the token is * revoked the backend can no longer issue a grant for this device. * * Falls back gracefully on old firmware (no 0x05 handler): the device ignores * the opcode and the result notification times out, which we treat as a soft * failure — the caller should still proceed with server-side unbind and rely on * the heartbeat factory_reset path for credential wipe. */ deprovision(device: ConnectedDevice, grantBlob: string): Promise<{ success: boolean; error?: string; }>; /** * Full BLE factory reset (opcode 0x06). Requires a valid deprovision grant. * * Clears token, pairing state, all stored WiFi credentials, and conn_policy on * the device, then reboots it. Use this for Reset/Delete flows where the device * may not have WiFi/4G to receive a cloud-channel factory_reset command. * * The device reboots ~500ms after sending the success notification, so the BLE * connection will drop shortly after a successful call. * * Falls back gracefully on firmware without 0x06: times out on the result * notification — caller should still proceed with server-side reset. */ bleFactoryReset(device: ConnectedDevice, grantBlob: string): Promise<{ success: boolean; error?: string; }>; /** * Get current recording state from device */ getRecordingState(device: ConnectedDevice): Promise; /** * Subscribe to recording state changes */ subscribeToRecordingState(device: ConnectedDevice, callback: (state: RecordingState) => void): () => void; /** * Wait for recording control result from device */ private waitForRecordingResult; /** * Parse recording state from Bluetooth data */ private parseRecordingState; /** * Configure WiFi credentials on device via BLE. * Requires a WiFi configuration grant from backend. * * @param deviceId - Connected device ID * @param credentials - WiFi network credentials * @param grant - WiFi config grant from backend * @returns Configuration result * * @example * ```typescript * // 1. Get grant from backend * const grant = await api.createWiFiConfigGrant(deviceId); * * // 2. Configure device via BLE * const result = await deviceManager.configureWiFi( * deviceId, * { ssid: 'MyNetwork', password: 'password123', securityType: 'WPA2' }, * grant * ); * * if (result.success) { * console.log('WiFi configured successfully'); * } * ``` */ configureWiFi(deviceId: string, credentials: WiFiCredentials, grant: WiFiConfigGrant): Promise; /** * Disconnect WiFi on device and forget stored credentials. * * @param deviceId - Connected device ID * @returns Configuration result */ disconnectWiFi(deviceId: string): Promise; /** * Get WiFi connection status from device. * * @param deviceId - Connected device ID * @returns WiFi status information * * @example * ```typescript * const status = await deviceManager.getWiFiStatus(deviceId); * console.log('WiFi status:', status.status); * if (status.status === 'connected') { * console.log('Connected to:', status.ssid); * console.log('Signal strength:', status.signalStrength); * } * ``` */ getWiFiStatus(deviceId: string): Promise; /** * Subscribe to WiFi status updates from device. * * @param deviceId - Connected device ID * @param callback - Callback function for status updates * @returns Subscription object (call .remove() to unsubscribe) * * @example * ```typescript * const subscription = deviceManager.subscribeToWiFiStatus(deviceId, (status) => { * console.log('WiFi status update:', status.status); * if (status.status === 'connected') { * console.log('Connected to:', status.ssid); * } else if (status.status === 'failed') { * console.error('Connection failed:', status.lastError); * } * }); * * // Later: unsubscribe * subscription.remove(); * ``` */ subscribeToWiFiStatus(deviceId: string, callback: (status: WiFiStatusInfo) => void): Subscription; /** * Scan for WiFi networks using the device's WiFi radio. * Sends a scan command via Bluetooth and waits for the device to report results. * * @param device - Connected device with WiFi capability * @returns Scan result with networks sorted by signal quality */ scanWiFiNetworks(device: ConnectedDevice): Promise; /** * Wait for WiFi configuration result from device. */ private waitForWiFiConfigResult; private loadReconnectRegistry; private saveReconnectRegistry; /** * Clean up resources */ destroy(): void; } //# sourceMappingURL=DeviceManager.d.ts.map