import { Buffer } from "buffer" import { isAvailable as nativeBleAvailable } from "./nativeBle" import { BroadcastReceiver } from "react-native-broadcast-receiver" import { BleErrorCode, BleManager, Device, Subscription } from "react-native-ble-plx" import RNBluetoothClassic, { BluetoothDevice as ClassicDevice } from "react-native-bluetooth-classic" import useBleStore, { BleDevice, BleState, CharacteristicData, ConnectionProtocol, DeviceRole, ExtendedSubscription } from "./ExpoBleStore" import { BARCODE, IN_CHAR, IN_SER, INIT_DEVICE, NOTIFICATION, OUT_CHAR, RFID, START_RFID_SCAN, // BARCODE, INIT_DEVICE, NOTIFICATION, RFID, SETTING, START_RFID_SCAN } from "./csl/cs108/cslEnum" import { // combineUint8Arrays, formatValue, getHeader, getResponse, getTypeEnum, // hexStringToArray, hexStringToReversedUint8Array, validateCRC, } from "./csl/cs108/csl" import { shallow } from "zustand/shallow" // import { sendBroadcast } from "react-native-boardcast-sender" const manager = new BleManager() // Classic Bluetooth state const classicManager = RNBluetoothClassic // Classic Bluetooth scan and connect helpers (correct API) async function startClassicScan(onDeviceFound?: (d: any) => void) { try { const enabled = await classicManager.isBluetoothEnabled() if (!enabled) { await classicManager.requestBluetoothEnabled() } // Get bonded (paired) devices const devices = await classicManager.getBondedDevices() devices.forEach((device: ClassicDevice) => { onDeviceFound?.(device) }) // Optionally, discover unpaired devices (slower) // const unpaired = await classicManager.startDiscovery(); // unpaired.forEach((device: ClassicDevice) => onDeviceFound?.(device)); } catch (e) { console.warn("Classic scan error", e) } } function startClassicDataFlow(device: BleDevice, callback: CallableFunction) { // Listen for data from classic device const subscription = device.classicConnection?.onDataReceived((event: any) => { callback(event.data) }) return subscription } function ensureAvailable() { if (!nativeBleAvailable) { throw new Error("react-native-ble-plx is not installed in the host app. Install it to enable BLE features.") } } function decodeBase64ToUtf8(b64: string) { try { const G: any = globalThis as any if (G.Buffer) { return G.Buffer.from(b64, "base64").toString("utf8") } const binary = atob(b64) const bytes = [] as number[] for (let i = 0; i < binary.length; i++) bytes.push(binary.charCodeAt(i)) const percentEncoded = bytes.map((b) => "%" + ("00" + b.toString(16)).slice(-2)).join("") return decodeURIComponent(percentEncoded) } catch (e) { return undefined } } function cs108DataFlow(connectedDevice: BleDevice, callback: CallableFunction, characteristicBuffers: Map, acceptType?: string[], store?: any) { const sub = manager.monitorCharacteristicForDevice(connectedDevice.id, IN_SER, IN_CHAR, (err, update) => { if (err) { if (err.errorCode === BleErrorCode.DeviceDisconnected) { store.updateConnectedDevice(connectedDevice.id, { connected: false }) } } else if (update) { const base64ToBit = Buffer.from(update.value as any, "base64") const bufferToArray = Uint8Array.from(base64ToBit) if (bufferToArray[0] === 167 && bufferToArray[1] === 179) { const header = getHeader(bufferToArray) const response = getResponse(bufferToArray) if (header && response) { const dataLength = header[2] if (response.length === dataLength) { const crc = validateCRC(bufferToArray) if (crc) { const { type, completed, value } = formatValue(bufferToArray, "") if (!acceptType || acceptType.length === 0 || acceptType.includes(type)) { if (type !== "unknown") { if (!completed) { characteristicBuffers.set(type, value) } else { callback(value) characteristicBuffers.set(type, undefined) } } } } characteristicBuffers.set("tmp", undefined) } else { characteristicBuffers.set("tmp", { base64: update.value, bufferToArray: bufferToArray, }) } } } else { const currentBuffer = characteristicBuffers.get("tmp") if (currentBuffer) { let arrayOne = currentBuffer.bufferToArray let arrayTwo = bufferToArray let mergedArray = new Uint8Array(arrayOne.length + arrayTwo.length) mergedArray.set(arrayOne) mergedArray.set(arrayTwo, arrayOne.length) const mergedBase64 = currentBuffer.base64 + "," + update.value const header = getHeader(mergedArray) const response = getResponse(mergedArray) if (header && response) { const dataLength = header[2] if (response.length === dataLength) { const crc = validateCRC(mergedArray) if (crc) { const typeEnum = getTypeEnum(mergedBase64) const prev = characteristicBuffers.get(typeEnum) const { type, completed, value } = formatValue(mergedBase64, prev) if (!acceptType || acceptType.length === 0 || acceptType.includes(type)) { if (type !== "unknown") { if (!completed) { characteristicBuffers.set(type, value) } else { callback(value) characteristicBuffers.set(type, undefined) } } } } characteristicBuffers.set("tmp", undefined) } else { characteristicBuffers.set("tmp", { base64: mergedBase64, bufferToArray: mergedArray, }) } } } } } }) return sub } function barcodeDataFlow(connectedDevice: BleDevice, subscriptionList: Subscription[], callback: CallableFunction): Subscription[] | undefined { const characteristics = Object.values(connectedDevice.characteristics) characteristics.forEach((c) => { const sub = manager.monitorCharacteristicForDevice(connectedDevice.id, c.serviceUUID, c.uuid, (err, update) => { if (update) { const decodedBuffer = Buffer.from(update.value ?? "", "base64") let decodedString = decodedBuffer.toString("utf-8") decodedString = decodedString.replace(/(\r\n|\n|\r)/gm, "") callback(decodedString) } }) subscriptionList.push(sub) }) return subscriptionList } function handheldDataFlow(callback: CallableFunction, acceptType?: string[]): Subscription[] | undefined { const sub = BroadcastReceiver.addEventListner((d) => { if (acceptType && acceptType?.length > 0 && acceptType.includes("barcode")) { if (d.action === "com.speedata.showdecodedata") { const data = d.data as HandheldBarcodeData callback(data.message) } } if (acceptType && acceptType?.length > 0 && acceptType.includes("rfid")) { if (d.action === "com.spd.action.data") { const data = d.data as HandheldRFIDData callback({ epc: data.epc.toLowerCase(), rssi: Number(data.rssi) }) } } if (acceptType && acceptType?.length > 0 && acceptType.includes("notification")) { if (d.action === "com.spd.action.KEY_UHF") { const data = d.data as HandheldTriggerData if (data.keyAction === "0") { callback({ value: "trigger_pushed", completed: true }) } else { callback({ value: "trigger_released", completed: true }) } } } }) return [sub] } async function initialCS108(connectedDevice: BleDevice | Device) { await writeDataToCS108Device(connectedDevice, INIT_DEVICE) .then((success) => {}) .finally(() => {}) } async function writeDataToCS108Device(connectedDevice: BleDevice | Device, dataArray: any[]) { let index = 0 if (connectedDevice) { if (connectedDevice.name?.includes("CS108")) { for (const element of dataArray) { const byteArray = new Uint8Array(element) const response = getResponse(byteArray) if (response) { byteArray[2] = response.length const buffer = Buffer.from(byteArray) const base64String = buffer.toString("base64") try { await manager.writeCharacteristicWithResponseForDevice(connectedDevice.id, IN_SER, OUT_CHAR, base64String) index++ // console.log("check", writeResponse, "index:", index) } catch (error) { console.error("Write error:", error) } } } } } return index === dataArray.length } export function createBleConnector(store: BleState) { return { async startScan(onDeviceFound?: (d: any) => void) { ensureAvailable() // Start BLE scan manager.startDeviceScan(null, null, (error: any, device: any) => { if (error) { // console.warn("BLE scan error", error) return } if (!device) return const currentStore = useBleStore.getState() const devices = Object.values(currentStore.devices) const connectedDevices = Object.values(currentStore.connectedDevices) // Add or update device in store const existing = devices.find((d: any) => d.id === device.id) if (!existing) { currentStore.addDevice({ id: device.id, name: device.name ?? device.localName ?? null, connected: false, role: "unknown", rssi: device.rssi ?? null, }) onDeviceFound?.(device) } // Attempt reconnection if previously connected and not manually disconnected const previouslyConnected = connectedDevices.find((d: any) => d.id === device.id) if ( previouslyConnected && previouslyConnected.connected === false && !previouslyConnected.manuallyDisconnected && previouslyConnected.connectionProtocol !== "classic" ) { ExpoBleConnector.connect(device.id, previouslyConnected.role).catch((e) => { // console.warn("Reconnection failed", e) }) } }) // Start classic Bluetooth scan await startClassicScan((classicDevice) => { // Add classic device to store if not present const currentStore = useBleStore.getState() const devices = Object.values(currentStore.devices) const existing = devices.find((d: any) => d.id === classicDevice.id) if (!existing) { currentStore.addDevice({ id: classicDevice.id, name: classicDevice.name ?? null, connected: false, connectionProtocol: "classic", role: "unknown", rssi: classicDevice.rssi ?? null, }) onDeviceFound?.(classicDevice) } }) }, stopScan() { if (!nativeBleAvailable) return manager.stopDeviceScan() }, onStateChange(callback: (state: string) => void) { ensureAvailable() return manager.onStateChange((state) => { callback(state) }, true) }, async connect(id: string, role: DeviceRole = "unknown", connectionProtocol: ConnectionProtocol = "ble") { ensureAvailable() try { if (connectionProtocol === "classic") { // Classic Bluetooth connect const devices = await classicManager.getBondedDevices() const device = devices.find((d: ClassicDevice) => d.id === id) if (!device) throw new Error("Classic device not found") const connection = await classicManager.connectToDevice(device.id) const is = await connection.isConnected() if (!is) { throw new Error("Classic device connection failed") } const deviceData = { id, name: device.name ?? null, connected: true, role, connectionProtocol: "classic" as ConnectionProtocol, serviceUUIDs: [] as string[], characteristics: {} as Record, manuallyDisconnected: false, // Default to false when connecting classicConnection: connection, } // Log deviceData only after all characteristics are discovered store.addConnectedDevice(deviceData) return device } else { // BLE connect (original logic) let device = await manager.connectToDevice(id) device = await device.discoverAllServicesAndCharacteristics() await device.requestMTU(517) let characteristic: Record = {} as Record const services = await device.services() if (device.name?.includes("CS108")) { await initialCS108(device) } for (const s of services) { const characteristics = await s.characteristics() for (const c of characteristics) { const raw = c.value const decoded = raw ? decodeBase64ToUtf8(raw) : undefined characteristic[c.uuid] = { uuid: c.uuid, serviceUUID: c.serviceUUID, value: raw ?? undefined, rawBase64: raw ?? undefined, decoded } } } // Log connected characteristic after all characteristics are processed const deviceData = { id, name: device.name ?? device.localName ?? null, connected: true, connectionProtocol: "ble" as ConnectionProtocol, role, serviceUUIDs: device.serviceUUIDs, characteristics: characteristic, manuallyDisconnected: false, // Default to false when connecting } // Log deviceData only after all characteristics are discovered store.addConnectedDevice(deviceData) // Listen for disconnection events device.onDisconnected(() => { store.updateConnectedDevice(id, { connected: false }) }) // await startMonitoringAllCharacteristicsForDevice(device, store.updateConnectedDevice) return device } } catch (e) { console.warn("connect error", e) manager.cancelDeviceConnection(id).catch((err) => { console.warn("Error cancelling connection after failed connect:", err) }) throw e } }, async writeToClassic(id: string | string[], data: string) { const ids = Array.isArray(id) ? id : [id] const results: { id: string; success: boolean; error?: any }[] = [] for (const deviceId of ids) { const entry = useBleStore.getState().connectedDevices[deviceId] if (!entry) { console.warn(`writeToClassic: Device with id ${deviceId} not found in connectedDevices`) results.push({ id: deviceId, success: false, error: "Device not found" }) continue } if (!entry.classicConnection) { console.warn(`writeToClassic: No classicConnection for device ${deviceId}`) results.push({ id: deviceId, success: false, error: "No classicConnection" }) continue } try { const isConnected = await entry.classicConnection.isConnected() if (!isConnected) { console.warn(`writeToClassic: classicConnection for device ${deviceId} is not connected`) results.push({ id: deviceId, success: false, error: "Not connected" }) continue } console.warn(`writeToClassic: Writing data to device ${deviceId} onDataReceived`) entry.classicConnection.onDataReceived((v) => console.warn(v)) console.warn(`writeToClassic: Writing data to device ${deviceId} onDataReceived`) await entry.classicConnection.write(data).then((v) => console.warn("write result", v)) results.push({ id: deviceId, success: true }) } catch (err) { console.error(`writeToClassic: Error writing to device ${deviceId}`, err) results.push({ id: deviceId, success: false, error: err }) } } return results }, async readFromClassic(id: string) { const entry = useBleStore.getState().connectedDevices[id] if (entry?.classicConnection && (await entry.classicConnection.isConnected())) { await entry.classicConnection.read().then((data) => { console.log("readFrom", data) }) } else { console.warn(`Device ${id} not connected`) } }, async enableBluetooth() { // const a = await manager // .enable() // .catch((e) => { // console.log(e) // }) // .then(() => { // console.log("Bluetooth enabled") // }) }, async disconnect(id: string) { const entry = store.connectedDevices[id] if (!entry) return if (entry.connectionProtocol === "classic") { // Classic Bluetooth disconnect await classicManager.disconnectFromDevice(id) store.updateConnectedDevice(id, { connected: false, manuallyDisconnected: true, classicConnection: undefined, }) } else { // BLE disconnect if (!nativeBleAvailable) { store.updateConnectedDevice(id, { connected: false, manuallyDisconnected: true }) return } try { manager.isDeviceConnected(id) await manager.cancelDeviceConnection(id) } finally { store.updateConnectedDevice(id, { connected: false, manuallyDisconnected: true }) } } }, async sendData(id: string, serviceUUID: string, charUUID: string, data: string) { ensureAvailable() const devices = await manager.devices([id]) const device = devices[0] if (!device) throw new Error("device not found") const char = await device.writeCharacteristicWithResponseForService(serviceUUID, charUUID, data) return char }, async readCharacteristic(id: string, serviceUUID: string, charUUID: string) { ensureAvailable() const devices = await manager.devices([id]) const device = devices[0] if (!device) throw new Error("device not found") const char = await device.readCharacteristicForService(serviceUUID, charUUID) const rawBase64 = char?.value const decoded = rawBase64 ? decodeBase64ToUtf8(rawBase64) : undefined store.updateConnectedDevice(id, { characteristics: { [charUUID]: { serviceUUID: char.serviceUUID, uuid: charUUID, value: rawBase64 ?? undefined, rawBase64: rawBase64 ?? undefined, decoded }, }, }) return { rawBase64, decoded, char } }, monitorCharacteristic(device: any, charUUID: string, onValue: (value?: string) => void) { device.services().then((services: any) => { services.forEach(async (s: any) => { const characteristics = await s.characteristics() const target = characteristics.find((c: any) => c.uuid.toLowerCase().includes(charUUID.toLowerCase())) if (target) { target.monitor((error: any, c: any) => { if (error) { console.warn("monitor error", error) return } const decoded = c?.value ?? undefined onValue(decoded) }) } }) }) }, isAvailable() { return nativeBleAvailable }, startDataFlow(callback: CallableFunction, acceptType?: string[]) { const transactionId = Date.now() // Generate a unique transaction ID // Subscribe to changes in connectedDevices using Zustand's subscribeWithSelector const unsubscribe = useBleStore.subscribe( (state) => state.connectedDevices, async (connectedDevices) => { const devices = Object.values(connectedDevices) if (devices.length === 0) return // Cleanup old subscriptions const existingSubscriptions = useBleStore.getState().subscriptions for (const sub of existingSubscriptions) { if (sub.transactionId === transactionId && sub.remove) { await sub.remove() } } useBleStore.setState((state) => ({ subscriptions: state.subscriptions.filter((sub) => sub.transactionId !== transactionId), })) // Create new subscriptions const newSubscriptions: ExtendedSubscription[] = [] for (const device of devices) { if (!device.connected) continue if (device.role === "scanner" && device.name?.includes("CS108")) { const buffers = new Map() newSubscriptions.push(cs108DataFlow(device, callback, buffers, acceptType, useBleStore)) } else if (device.role === "scanner") { if (acceptType && acceptType?.length > 0 && acceptType.includes("barcode")) { const barcodeSubs = barcodeDataFlow(device, [], callback) if (barcodeSubs) newSubscriptions.push(...barcodeSubs) } } else if (device.connectionProtocol === "classic") { if (acceptType && acceptType?.length > 0 && acceptType.includes("classic")) { const classicSub = startClassicDataFlow(device, callback) newSubscriptions.push(classicSub as Subscription) } } } const handheldSubs = handheldDataFlow(callback, acceptType) if (handheldSubs) newSubscriptions.push(...handheldSubs) useBleStore.setState((state) => { const newSubscriptionsWithId: ExtendedSubscription[] = newSubscriptions.map((sub) => ({ ...sub, transactionId })) return { subscriptions: [...state.subscriptions, ...newSubscriptionsWithId] } }) }, { equalityFn: shallow, fireImmediately: true } ) return () => { unsubscribe() // Cleanup subscriptions when data flow is stopped const existingSubscriptions = useBleStore.getState().subscriptions for (const sub of existingSubscriptions) { if (sub.transactionId === transactionId && sub.remove) { sub.remove() } } useBleStore.setState((state) => ({ subscriptions: state.subscriptions.filter((sub) => sub.transactionId !== transactionId), })) } }, startRFID: () => { const connectedDevices = Object.values(useBleStore.getState().connectedDevices) connectedDevices.forEach((connectedDevice) => { const isCS108 = connectedDevice?.name?.includes("CS108") if (connectedDevice) { if (isCS108) { writeDataToCS108Device(connectedDevice, [RFID.START_RFID_READER, NOTIFICATION.ENABLE_TRIGGER_INVOKE_RFID_ABORT, RFID.ABORT]).then((success) => { writeDataToCS108Device(connectedDevice, [RFID.ABORT]) }) // useEffect(() => { // if (connectedDevice) { // if (rfidReady) { // if (trigger) { // writeDataToDevice(connectedDevice, START_RFID_SCAN) // } else { // writeDataToDevice(connectedDevice, [RFID.ABORT]) // } // } // } // }, [connectedDevice, rfidReady, trigger, writeDataToDevice]) } } }) store.setRfidReady(true) }, stopRFID: () => { const connectedDevices = Object.values(useBleStore.getState().connectedDevices) connectedDevices.forEach((connectedDevice) => { const isCS108 = connectedDevice?.name?.includes("CS108") if (connectedDevice) { if (isCS108) { } } }) store.setRfidReady(false) }, startRFIDTrigger: (start: boolean) => { const connectedDevices = Object.values(useBleStore.getState().connectedDevices) const enable = useBleStore.getState().rfidReady if (!enable) return connectedDevices.forEach((connectedDevice) => { const isCS108 = connectedDevice?.name?.includes("CS108") if (connectedDevice) { if (isCS108) { if (start) { writeDataToCS108Device(connectedDevice, START_RFID_SCAN) } else { writeDataToCS108Device(connectedDevice, [RFID.ABORT]) } } else { // sendBroadcast("com.pda.uhfdemo.read", { key: "clear", value: "clear" }) } } }) store.setRfidReady(true) }, startBarcode: () => { const connectedDevices = Object.values(useBleStore.getState().connectedDevices) connectedDevices.forEach((connectedDevice) => { const isCS108 = connectedDevice?.name?.includes("CS108") if (connectedDevice) { if (isCS108) { writeDataToCS108Device(connectedDevice, [NOTIFICATION.ENABLE_TRIGGER_BARCODE_SCANNING, BARCODE.START_BARCODE_READER]) } } }) store.setBarcodeReady(true) }, stopBarcode: () => { const connectedDevices = Object.values(useBleStore.getState().connectedDevices) connectedDevices.forEach((connectedDevice) => { const isCS108 = connectedDevice?.name?.includes("CS108") if (connectedDevice) { if (isCS108) { writeDataToCS108Device(connectedDevice, [NOTIFICATION.DISABLE_TRIGGER_BARCODE_SCANNING, BARCODE.STOP_BARCODE_READER]) } } }) store.setBarcodeReady(false) }, } } export const ExpoBleConnector = createBleConnector(useBleStore.getState()) export default ExpoBleConnector type HandheldBarcodeData = { message: string messagetype: string se4500_byte: string } type HandheldRFIDData = { epc: string rssi: string tid: string } type HandheldTriggerData = { keyAction: string keyRepeatCount: string keyTime: string }