UNPKG

react-native-capture

Version:
580 lines (525 loc) 22.7 kB
/* eslint-disable no-bitwise */ // src/CaptureHelperDevice.ts import { CaptureDeviceType, CapturePropertyIds, CapturePropertyTypes, CaptureProperty, CaptureDataSourceStatus, CaptureDataSourceFlags, Disconnect, type Trigger, type DataFormat, type StandConfig, type LocalDecodeAction, type DeviceDataAcknowledgment, type DataConfirmationLed, type DataConfirmationBeep, type DataConfirmationRumble, type Notifications, type PowerState, type Timer, } from 'socketmobile-capturejs'; // Type-only import to avoid circular runtime dependency. import type { CaptureRn } from 'react-native-capture'; /** Internal interface — only what CaptureHelperDevice needs from a capture instance */ interface CaptureAPI { getProperty(property: CaptureProperty<any>): Promise<CaptureProperty<any>>; setProperty(property: CaptureProperty<any>): Promise<CaptureProperty<unknown>>; } export interface CaptureHelperDeviceInfo { name: string; guid: string; type: number; handle: number; } /** * Wraps a connected device's CaptureRn with typed property methods. * Created automatically by CaptureHelper — never instantiated by developers. * * All methods throw on error; wrap calls in try/catch. */ export class CaptureHelperDevice { readonly name: string; readonly guid: string; readonly type: number; readonly handle: number; /** * The underlying device capture — exposed for SocketCam component compatibility. * Prefer using the typed methods on this class instead of calling devCapture directly. */ readonly devCapture: CaptureAPI; constructor(info: CaptureHelperDeviceInfo, capture: CaptureRn) { this.name = info.name; this.guid = info.guid; this.type = info.type; this.handle = info.handle; this.devCapture = capture as unknown as CaptureAPI; } /** Multi-interface device families: each physical device appears as multiple virtual devices */ private static readonly _multiInterfaceFamilies: number[][] = [ [CaptureDeviceType.ScannerS370, CaptureDeviceType.NFCS370], ]; /** * Returns the multi-interface family (array of device types) that the given * device type belongs to, or undefined if it is a single-interface device. */ static getMultiInterfaceFamily(deviceType: number): number[] | undefined { return CaptureHelperDevice._multiInterfaceFamilies.find( family => family.includes(deviceType) ); } // ─── BLE timer device categories ──────────────────────────────────────────── private static readonly _bluetoothLowEnergyDeviceTypes: number[] = [ CaptureDeviceType.ScannerS550, CaptureDeviceType.DeviceD751, CaptureDeviceType.ScannerS370, CaptureDeviceType.NFCS370, CaptureDeviceType.ScannerS320, CaptureDeviceType.DeviceS721, CaptureDeviceType.DeviceS741, ]; private _isBluetoothLowEnergy(): boolean { return CaptureHelperDevice._bluetoothLowEnergyDeviceTypes.includes(this.type); } // BLE single-byte timer encoding: // 0x00 = disabled, 0x01–0x7F = 1–127 min, 0x80–0xFF = (v & 0x7F) × 15 min private static _decodeBleTimer(raw: number): number { if (raw & 0x80) { return (raw & 0x7F) * 15; } return raw; } private static _encodeBleTimer(minutes: number): number { if (minutes === 0) return 0x00; if (minutes <= 127) return minutes; const quarterHours = Math.round(minutes / 15); return 0x80 | (Math.min(quarterHours, 0x7F) & 0x7F); } // ─── Private helpers ─────────────────────────────────────────────────────── private async _get( id: CapturePropertyIds, type: CapturePropertyTypes = CapturePropertyTypes.None, value: any = {} ): Promise<any> { const property = new CaptureProperty(id, type, value); const result = await this.devCapture.getProperty(property); return result.value; } private async _set( id: CapturePropertyIds, type: CapturePropertyTypes, value: any ): Promise<void> { const property = new CaptureProperty(id, type, value); await this.devCapture.setProperty(property); } // ─── Device Info ─────────────────────────────────────────────────────────── /** * Get the device's friendly (display) name. * The friendly name is the name that appears in Bluetooth settings. * * Tip: You can also read `device.name` directly to avoid an async call, * but that value is only set at connection time and won't reflect changes * made by another app or command barcode. * * @returns The friendly name string (max 31 UTF-8 characters) */ async getFriendlyName(): Promise<string> { return this.name; } /** * Set the device's friendly name. * The friendly name is the name that appears in Bluetooth settings. * * @param name - The new friendly name (max 31 UTF-8 characters) */ async setFriendlyName(name: string): Promise<void> { return this._set(CapturePropertyIds.FriendlyNameDevice, CapturePropertyTypes.String, name); } /** * Get the Bluetooth address of the device. * * @returns The Bluetooth MAC address as a 6-byte array */ async getBluetoothAddress(): Promise<number[]> { return this._get(CapturePropertyIds.BluetoothAddressDevice); } /** * Get the model of the device (see `CaptureDeviceType` enum). * * Tip: You can also read `device.type` directly to avoid an async call, * but that value is only set at connection time. * * @returns A `CaptureDeviceType` value identifying the device model */ async getDeviceType(): Promise<number> { return this.type; } /** * Get the firmware version of the device. * * @returns An object with version components: `major`, `middle`, `minor`, `build`, * and optionally `year`, `month`, `day` for the firmware build date */ async getFirmwareVersion(): Promise<{ major: number; middle: number; minor: number; build: number; year?: number; month?: number; day?: number; }> { return this._get(CapturePropertyIds.VersionDevice); } // ─── Status ──────────────────────────────────────────────────────────────── /** * Get the current battery level of the device. * * Tip: Consider using {@link CaptureHelper} `onBatteryLevel` callback with * `setNotifications` to subscribe to battery level change events instead of polling. * * @returns Battery percentage (0–100) */ async getBatteryLevel(): Promise<number> { const raw = await this._get(CapturePropertyIds.BatteryLevelDevice); return (raw >> 8) & 0xff; } /** * Get the current power state of the device (on battery, on cradle, on AC, etc.). * * Tip: Consider using {@link CaptureHelper} `onPowerState` callback with * `setNotifications` to subscribe to power state events instead of polling. * * @returns A `PowerState` value */ async getPowerState(): Promise<PowerState> { const raw = await this._get(CapturePropertyIds.PowerStateDevice); return raw & 0xff; } /** * Get the current state of each button on the device (pressed/released). * * Tip: Consider using {@link CaptureHelper} `onButtons` callback with * `setNotifications` to subscribe to button press/release events instead of polling. * * @returns A `Notifications` bitmask describing the button states */ async getButtonsState(): Promise<Notifications> { return this._get(CapturePropertyIds.ButtonsStatusDevice); } // ─── Scan Control ────────────────────────────────────────────────────────── /** * Set the trigger of the device — can start or stop a read and enable or * disable the physical trigger button on the device. * * @param trigger - One of the `Trigger` values: * - `Trigger.Start` — programmatically fire a scan * - `Trigger.Stop` — abort an ongoing scan * - `Trigger.Enable` — enable the physical trigger button * - `Trigger.Disable` — disable (lock) the physical trigger button * - `Trigger.ContinuousScan` — start continuous scanning mode */ async setTrigger(trigger: Trigger): Promise<void> { return this._set(CapturePropertyIds.TriggerDevice, CapturePropertyTypes.Byte, trigger); } /** * Get the status of a data source (symbology or NFC tag type). * A data source describes a barcode symbology (e.g. EAN-13, QR Code) or * an NFC tag type that the device can decode. * * @param dataSourceId - A `CaptureDataSourceID` constant identifying the symbology or tag type * @returns An object with `id`, `name`, and `status` (`CaptureDataSourceStatus.Enabled` or `Disabled`) */ async getDataSource(dataSourceId: number): Promise<{ id: number; name: string; status: CaptureDataSourceStatus }> { return this._get( CapturePropertyIds.DataSourceDevice, CapturePropertyTypes.DataSource, { id: dataSourceId, flags: CaptureDataSourceFlags.Status } ); } /** * Enable or disable a data source (symbology or NFC tag type) on the device. * * @param dataSourceId - A `CaptureDataSourceID` constant identifying the symbology or tag type * @param status - `CaptureDataSourceStatus.Enabled` or `CaptureDataSourceStatus.Disabled` */ async setDataSource(dataSourceId: number, status: CaptureDataSourceStatus): Promise<void> { return this._set(CapturePropertyIds.DataSourceDevice, CapturePropertyTypes.DataSource, { id: dataSourceId, flags: CaptureDataSourceFlags.Status, status: status } ); } /** * Get the local decode action of the device. * The decode action determines how decoded data is acknowledged locally — * with a beep, rumble, flash, or some combination of all three. * * @returns A `LocalDecodeAction` bitmask value */ async getDecodeAction(): Promise<LocalDecodeAction> { return this._get(CapturePropertyIds.LocalDecodeActionDevice); } /** * Set the local decode action of the device. * Controls the beep, rumble, and/or flash feedback when data is decoded. * * @param action - A `LocalDecodeAction` bitmask combining the desired feedback */ async setDecodeAction(action: LocalDecodeAction): Promise<void> { return this._set(CapturePropertyIds.LocalDecodeActionDevice, CapturePropertyTypes.Byte, action); } /** * Get the local device acknowledgment mode. * When enabled, the device acknowledges decoded data as soon as it is decoded. * When disabled, the device waits for the host to acknowledge decoded data and * the trigger will be locked until acknowledgment is received or the trigger lock * timeout has elapsed. * * @returns A `DeviceDataAcknowledgment` value */ async getDataAcknowledgment(): Promise<DeviceDataAcknowledgment> { return this._get(CapturePropertyIds.LocalAcknowledgmentDevice); } /** * Set the local device acknowledgment mode. * When enabled, the device acknowledges decoded data immediately. * When disabled, the device waits for the host to acknowledge via * {@link setDataConfirmation} and the trigger locks until acknowledgment * is received or the trigger lock timeout elapses. * * @param mode - A `DeviceDataAcknowledgment` value (`On` or `Off`) */ async setDataAcknowledgment(mode: DeviceDataAcknowledgment): Promise<void> { return this._set(CapturePropertyIds.LocalAcknowledgmentDevice, CapturePropertyTypes.Byte, mode); } /** * Send an acknowledgment to the device. Acknowledgment can either be * positive (good scan) or negative (bad scan). * * Required when `DataConfirmationMode` is set to `ModeApp` on the * {@link CaptureHelper} — otherwise the trigger stays locked after a scan. * Can also be called at any time to give visual/audio/haptic feedback * (e.g. red LED + error beep for an invalid barcode). * * @param led - LED feedback: `DataConfirmationLed.Green`, `.Red`, or `.None` * @param beep - Beep feedback: `DataConfirmationBeep.Good`, `.Bad`, or `.None` * @param rumble - Rumble feedback: `DataConfirmationRumble.Good`, `.Bad`, or `.None` */ async setDataConfirmation( led: DataConfirmationLed, beep: DataConfirmationBeep, rumble: DataConfirmationRumble ): Promise<void> { const value = (rumble << 4) + (beep << 2) + led; return this._set(CapturePropertyIds.DataConfirmationDevice, CapturePropertyTypes.Ulong, value); } /** * Get the current notification subscriptions for the device. * Notifications allow subscribing to events such as trigger press/release, * power button press/release, power state change, and battery level change. * * @returns A `Notifications` bitmask of the currently subscribed events */ async getNotifications(): Promise<Notifications> { return this._get(CapturePropertyIds.NotificationsDevice); } /** * Set which events the device reports to the host. * Subscribable events include trigger press/release, power button press/release, * power state change, and battery level change. * * @param flags - A `Notifications` bitmask combining the desired event subscriptions. * Example: `Notifications.BatteryLevelChange | Notifications.PowerState` */ async setNotifications(flags: Notifications): Promise<void> { return this._set(CapturePropertyIds.NotificationsDevice, CapturePropertyTypes.Ulong, flags); } /** * Get the decoded data format of the device. * The data format controls how scanned data is structured in the `onDecodedData` callback. * * @returns A `DataFormat` value (e.g. `DataFormat.Raw`, `DataFormat.TagTypeAndData`) */ async getDataFormat(): Promise<DataFormat> { return this._get(CapturePropertyIds.DataFormatDevice); } /** * Set the decoded data format of the device. * For NFC devices, use `DataFormat.TagTypeAndData` to receive both the tag type * and the payload in the `onDecodedData` callback. * * @param format - A `DataFormat` value */ async setDataFormat(format: DataFormat): Promise<void> { return this._set(CapturePropertyIds.DataFormatDevice, CapturePropertyTypes.Byte, format); } /** * Send an arbitrary get command to the device and receive the response. * This is a low-level API for sending proprietary commands that are not * exposed as typed methods. Use only when you know the device's command set. * * @param command - A byte array containing the raw command to send * @returns The device's raw response as a byte array */ async getDeviceSpecificCommand(command: number[]): Promise<number[]> { return this._get(CapturePropertyIds.DeviceSpecific, CapturePropertyTypes.Array, command); } // ─── Configuration ───────────────────────────────────────────────────────── /** * Get the stand (cradle) configuration of the device. * Controls the scanning behavior when the device is placed in a stand/cradle. * * @returns A `StandConfig` value */ async getStandConfig(): Promise<StandConfig> { return this._get(CapturePropertyIds.StandConfigDevice); } /** * Set the stand (cradle) configuration of the device. * Controls whether the device auto-scans when placed in a stand. * * @param config - A `StandConfig` value (e.g. `StandConfig.DetectMode`, `StandConfig.MobileMode`) */ async setStandConfig(config: StandConfig): Promise<void> { return this._set(CapturePropertyIds.StandConfigDevice, CapturePropertyTypes.Ulong, config); } /** * Get the timers for a scanner. * * Returns the timers for powering off when the device is connected or * disconnected without any activity, and the trigger lock timeout used * with remote acknowledgment. * * The byte layout of the response differs by device family: * - **Bluetooth Classic**: 8 bytes — mask(2) + triggerLock(2) + disconnected(2) + connected(2) * - **Bluetooth LE devices (S550, S370, D751, S721, S741)**: 4 bytes — padding(2) + disconnected(1) + connected(1) * * For Bluetooth LE devices, the single-byte encoding is: * - 0x00 = disabled (always on) * - 0x01–0x7F = 1–127 minutes * - 0x80–0xFF = (value & 0x7F) × 15 minutes (quarter-hour increments, up to 32 hours) * * Returned values are always normalized to minutes regardless of device family. * * @returns An object with: * - `mask` — a `Timer` bitmask indicating which timers are active (0 for BLE devices) * - `triggerLockTimer` — timeout in 1/4 second units for locking the trigger (0 for BLE devices) * - `connectedPowerOffTimer` — timeout in minutes before powering off when connected to a host * - `disconnectedPowerOffTimer` — timeout in minutes before powering off when not connected to any host */ async getTimers(): Promise<{ mask: number; triggerLockTimer: number; connectedPowerOffTimer: number; disconnectedPowerOffTimer: number; }> { const data = await this._get(CapturePropertyIds.TimersDevice); if (!Array.isArray(data)) { throw new Error(`Expected an array for timers, got ${data}`); } if (this._isBluetoothLowEnergy()) { // no mask - disconnected (1 byte) + connected(1 byte) if (data.length < 8) { throw new Error(`Expected at least 8 bytes for BluetoothLE timers response, got ${data.length}`); } const disconnectedPowerOffTimer = CaptureHelperDevice._decodeBleTimer(data[6] & 0xFF); const connectedPowerOffTimer = CaptureHelperDevice._decodeBleTimer(data[7] & 0xFF); return { mask: 0, triggerLockTimer: 0, connectedPowerOffTimer, disconnectedPowerOffTimer }; } // Bluetooth Classic: 8 bytes — mask(2) + triggerLock(2) + disconnected(2) + connected(2) if (data.length < 8) { throw new Error(`Expected at least 8 bytes for Classic timers, got ${data.length}`); } const mask = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF); const triggerLockTimer = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF); const disconnectedPowerOffTimer = ((data[4] & 0xFF) << 8) | (data[5] & 0xFF); const connectedPowerOffTimer = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF); return { mask, triggerLockTimer, connectedPowerOffTimer, disconnectedPowerOffTimer }; } /** * Set the timers for a scanner. The scanner needs to be powered cycle to take effect after setting timers. * * The byte layout sent to the device differs by device family: * - **Bluetooth Classic**: 8 bytes — mask(2) + triggerLock(2) + disconnected(2) + connected(2) * - **Bluetooth LE devices (S550, S370, D751, S721, S741)**: 4 bytes — pad(1) + disconnected(1) + pad(1) + connected(1) * * For Bluetooth Classic, the mask determines which values should be applied. * NOTE: on the 700 series (excluding 750) the trigger lock out timer cannot * be set at the same time as the connected or disconnected power off timer. * * For Bluetooth LE devices, the single-byte encoding is: * - 0x00 = disabled (always on) * - 0x01–0x7F = 1–127 minutes * - 0x80–0xFF = (value & 0x7F) × 15 minutes (quarter-hour increments, up to 32 hours) * * For Bluetooth LE devices, the mask and triggerLockTimer parameters are ignored. * All values are in minutes. For Bluetooth LE devices, values >127 min are encoded * as quarter-hour increments automatically. * * @param mask - A `Timer` bitmask selecting which timers to apply (Classic only) * @param connectedPowerOffTimer - Timeout in minutes before powering off when connected to a host * @param disconnectedPowerOffTimer - Timeout in minutes before powering off when not connected to any host * @param triggerLockTimer - Timeout in 1/4 second units for locking the trigger (Classic only) */ async setTimers( mask: Timer, connectedPowerOffTimer: number, disconnectedPowerOffTimer: number, triggerLockTimer: number ): Promise<void> { let data: number[]; if (this._isBluetoothLowEnergy()) { data = [ 0, CaptureHelperDevice._encodeBleTimer(disconnectedPowerOffTimer), 0, CaptureHelperDevice._encodeBleTimer(connectedPowerOffTimer), ]; } else { // Bluetooth Classic data = [ (mask >> 8) & 0xFF, mask & 0xFF, (triggerLockTimer >> 8) & 0xFF, triggerLockTimer & 0xFF, (disconnectedPowerOffTimer >> 8) & 0xFF, disconnectedPowerOffTimer & 0xFF, (connectedPowerOffTimer >> 8) & 0xFF, connectedPowerOffTimer & 0xFF, ]; } return this._set(CapturePropertyIds.TimersDevice, CapturePropertyTypes.Array, data); } // ─── BLE Device Lifecycle ────────────────────────────────────────────────── /** * Instruct the device to drop its Bluetooth connection. * The device becomes available for reconnection. * * Note: After sending this command, the host will be unable to send any * subsequent commands to this device until it reconnects. */ async setDisconnect(): Promise<void> { return this._set( CapturePropertyIds.DisconnectDevice, CapturePropertyTypes.Byte, Disconnect.MakeAvailable ); } /** * Reset all the settings on the device to their factory default values. * This includes symbology settings, preamble, postamble, friendly name, timers, etc. */ async setFactoryReset(): Promise<void> { return this._set(CapturePropertyIds.RestoreFactoryDefaultsDevice, CapturePropertyTypes.None, {}); } /** * Power cycle a Bluetooth LE device. * When the device is on a power source (e.g. charging), it reboots. * Otherwise it simply powers off. */ async setReset(): Promise<void> { return this._set(CapturePropertyIds.ResetDevice, CapturePropertyTypes.None, {}); } /** * Turn the device off. * The device will need to be manually powered on again to reconnect. */ async setPowerOff(): Promise<void> { return this._set(CapturePropertyIds.SetPowerOffDevice, CapturePropertyTypes.None, {}); } }