/** * 经典蓝牙工具(Android APP-PLUS) * 兼容官方 UniApp SDK 经典蓝牙实现,并补充当前项目需要的简化调用: * - getPairedDevices() * - startClassicDiscovery(onDeviceFound, onDiscoveryFinished) * - cancelClassicDiscovery() * - connDevice(address, callback) * - disConnDevice() * - sendByteData(byteData) */ // #ifdef APP-PLUS let BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter') let Intent = plus.android.importClass('android.content.Intent') let IntentFilter = plus.android.importClass('android.content.IntentFilter') let BluetoothDevice = plus.android.importClass('android.bluetooth.BluetoothDevice') let UUID = plus.android.importClass('java.util.UUID') let Toast = plus.android.importClass('android.widget.Toast') let Thread = plus.android.importClass('java.lang.Thread') let MY_UUID = UUID.fromString('00001101-0000-1000-8000-00805F9B34FB') let invoke = plus.android.invoke let btAdapter = BluetoothAdapter.getDefaultAdapter() let activity = plus.android.runtimeMainActivity() let btSocket = null let btInStream = null let btOutStream = null let setIntervalId = 0 let btFindReceiver = null let btStatusReceiver = null // #endif function normalizeDeviceType (deviceType) { if (deviceType === 1) return 'classic' if (deviceType === 2) return 'ble' if (deviceType === 3) return 'dual' return 'unknown' } function fallbackRfcommSocket (device, insecure = false) { try { const cls = invoke(device, 'getClass') const intClass = plus.android.importClass('java.lang.Integer').TYPE const methodName = insecure ? 'createInsecureRfcommSocket' : 'createRfcommSocket' const method = invoke(cls, 'getMethod', methodName, intClass) return invoke(method, 'invoke', device, 1) } catch (e) { console.error('RFCOMM fallback failed:', e) return null } } function getErrorMessage (error) { if (!error) return 'Unknown error' if (typeof error === 'string') return error return String(error.message || error.errMsg || error) } function normalizeWriteByte (value) { const byte = Number(value) || 0 return byte & 0xff } function toSignedWriteByte (value) { const byte = normalizeWriteByte(value) return byte >= 128 ? byte - 256 : byte } function normalizeWriteChunk (byteData, start, end) { const out = [] for (let i = start; i < end; i++) { out.push(toSignedWriteByte(byteData[i])) } return out } function isConnectionStateConnected (state) { return String(state || '').trim().toLowerCase() === 'connected' } function runOnUiThread (fn) { // #ifdef APP-PLUS try { if (activity && typeof activity.runOnUiThread === 'function') { const runnable = plus.android.implements('java.lang.Runnable', { run: function () { try { fn && fn() } catch (e) { console.error('runOnUiThread callback failed:', e) } }, }) activity.runOnUiThread(runnable) return } } catch (e) { console.error('runOnUiThread failed:', e) } // #endif try { setTimeout(() => { try { fn && fn() } catch (e) { console.error('setTimeout callback failed:', e) } }, 0) } catch (e) { console.error('async callback fallback failed:', e) } } function startBackgroundTask (task) { // #ifdef APP-PLUS const runnable = plus.android.implements('java.lang.Runnable', { run: function () { try { task && task() } catch (e) { console.error('Background task failed:', e) } }, }) const thread = new Thread(runnable) thread.start() return // #endif try { task && task() } catch (e) { console.error('Fallback task failed:', e) } } function createSocketCandidates (device) { return [ { name: 'secure-service-record', create: () => invoke(device, 'createRfcommSocketToServiceRecord', MY_UUID), }, { name: 'insecure-service-record', create: () => invoke(device, 'createInsecureRfcommSocketToServiceRecord', MY_UUID), }, { name: 'secure-channel-1', create: () => fallbackRfcommSocket(device, false), }, { name: 'insecure-channel-1', create: () => fallbackRfcommSocket(device, true), }, ] } var blueToothTool = { state: { bluetoothEnable: false, bluetoothState: '', discoveryDeviceState: false, readThreadState: false, connectionState: 'idle', lastAddress: '', lastSocketStrategy: '', lastError: '', lastSendError: '', outputReady: false, lastSendMode: 'idle', isSending: false, }, options: { listenBTStatusCallback: function (state) {}, discoveryDeviceCallback: function (newDevice) {}, discoveryFinishedCallback: function () {}, readDataCallback: function (dataByteArr) {}, connExceptionCallback: function (e) {}, }, init (setOptions) { Object.assign(this.options, setOptions || {}) this.state.bluetoothEnable = this.getBluetoothStatus() this.listenBluetoothStatus() }, setErrorState (message, type = 'general') { const text = String(message || '') this.state.lastError = text if (type === 'send') { this.state.lastSendError = text } }, clearErrorState () { this.state.lastError = '' this.state.lastSendError = '' }, shortToast (msg) { // #ifdef APP-PLUS try { if (activity) Toast.makeText(activity, String(msg), Toast.LENGTH_SHORT).show() } catch (_) {} // #endif }, isSupportBluetooth () { // #ifdef APP-PLUS return btAdapter != null // #endif return false }, getBluetoothStatus () { // #ifdef APP-PLUS return btAdapter != null && btAdapter.isEnabled() // #endif return false }, turnOnBluetooth () { // #ifdef APP-PLUS if (btAdapter == null) { this.shortToast('Bluetooth not available') return } if (!btAdapter.isEnabled()) { if (activity == null) { this.shortToast('Activity not available') return } let intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) activity.startActivityForResult(intent, 1) return } this.shortToast('Bluetooth already enabled') // #endif }, turnOffBluetooth () { // #ifdef APP-PLUS if (btAdapter != null && btAdapter.isEnabled()) { btAdapter.disable() } if (btFindReceiver != null) { try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} btFindReceiver = null } this.state.bluetoothEnable = false this.cancelDiscovery() this.closeBtSocket() // #endif }, getPairedDevices () { // #ifdef APP-PLUS let pairedDevices = [] try { if (btAdapter == null || !btAdapter.isEnabled()) return pairedDevices let pairedDevicesAndroid = btAdapter.getBondedDevices() if (!pairedDevicesAndroid) return pairedDevices let it = invoke(pairedDevicesAndroid, 'iterator') while (invoke(it, 'hasNext')) { let device = invoke(it, 'next') let deviceType = invoke(device, 'getType') let deviceId = invoke(device, 'getAddress') let deviceName = invoke(device, 'getName') pairedDevices.push({ name: deviceName != null ? String(deviceName).trim() || 'Unknown Device' : 'Unknown Device', deviceId: String(deviceId || ''), type: normalizeDeviceType(deviceType), }) } } catch (e) { console.error('getPairedDevices error:', e) } return pairedDevices // #endif return [] }, discoveryNewDevice () { // #ifdef APP-PLUS if (btAdapter == null || !btAdapter.isEnabled() || activity == null) return if (btFindReceiver != null) { try { activity.unregisterReceiver(btFindReceiver) } catch (e) { console.error(e) } btFindReceiver = null this.cancelDiscovery() } let options = this.options btFindReceiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', { onReceive: function (context, intent) { plus.android.importClass(context) plus.android.importClass(intent) let action = intent.getAction() if (BluetoothDevice.ACTION_FOUND === action) { let device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) if (!device) return let deviceType = invoke(device, 'getType') let deviceId = invoke(device, 'getAddress') let deviceName = invoke(device, 'getName') let newDevice = { name: deviceName != null ? String(deviceName).trim() || 'Unknown Device' : 'Unknown Device', deviceId: String(deviceId || ''), type: normalizeDeviceType(deviceType), } options.discoveryDeviceCallback && options.discoveryDeviceCallback(newDevice) } if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED === action) { blueToothTool.cancelDiscovery() options.discoveryFinishedCallback && options.discoveryFinishedCallback() } }, }) let filter = new IntentFilter() filter.addAction(BluetoothDevice.ACTION_FOUND) filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) activity.registerReceiver(btFindReceiver, filter) btAdapter.startDiscovery() this.state.discoveryDeviceState = true // #endif }, startClassicDiscovery (onDeviceFound, onDiscoveryFinished) { this.init({ discoveryDeviceCallback: onDeviceFound || function () {}, discoveryFinishedCallback: onDiscoveryFinished || function () {}, }) this.discoveryNewDevice() }, listenBluetoothStatus () { // #ifdef APP-PLUS if (activity == null) return if (btStatusReceiver != null) { try { activity.unregisterReceiver(btStatusReceiver) } catch (e) { console.error(e) } btStatusReceiver = null } btStatusReceiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', { onReceive: (context, intent) => { plus.android.importClass(context) plus.android.importClass(intent) let action = intent.getAction() if (action === BluetoothAdapter.ACTION_STATE_CHANGED) { let blueState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0) let stateStr = '' switch (blueState) { case BluetoothAdapter.STATE_TURNING_ON: stateStr = 'STATE_TURNING_ON' break case BluetoothAdapter.STATE_ON: stateStr = 'STATE_ON' this.state.bluetoothEnable = true break case BluetoothAdapter.STATE_TURNING_OFF: stateStr = 'STATE_TURNING_OFF' break case BluetoothAdapter.STATE_OFF: stateStr = 'STATE_OFF' this.state.bluetoothEnable = false break } this.state.bluetoothState = stateStr this.options.listenBTStatusCallback && this.options.listenBTStatusCallback(stateStr) } }, }) let filter = new IntentFilter() filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED) activity.registerReceiver(btStatusReceiver, filter) if (this.state.bluetoothEnable) { this.options.listenBTStatusCallback && this.options.listenBTStatusCallback('STATE_ON') } // #endif }, connDevice (address, callback) { // #ifdef APP-PLUS this.cancelDiscovery() if (btSocket != null) this.closeBtSocket() this.state.readThreadState = false this.state.connectionState = 'connecting' this.state.lastAddress = String(address || '') this.state.outputReady = false this.clearErrorState() if (btAdapter == null) { this.shortToast('Bluetooth not available') this.state.connectionState = 'error' this.setErrorState('Bluetooth not available') callback && callback(false) return false } try { let device = invoke(btAdapter, 'getRemoteDevice', address) const candidates = createSocketCandidates(device) let socket = null let lastError = null for (let i = 0; i < candidates.length; i++) { const candidate = candidates[i] try { socket = candidate.create() if (socket) { this.state.lastSocketStrategy = candidate.name break } } catch (e) { lastError = e console.warn('create socket failed:', candidate.name, e) } } btSocket = socket if (!btSocket) { this.state.connectionState = 'error' this.setErrorState(lastError ? getErrorMessage(lastError) : 'Unable to create Bluetooth socket') callback && callback(false) return false } } catch (e) { console.error(e) this.state.connectionState = 'error' this.setErrorState(getErrorMessage(e)) callback && callback(false) return false } try { invoke(btSocket, 'connect') const streamReady = this.readData() if (!streamReady) { throw new Error(this.state.lastError || 'Bluetooth output stream not ready') } this.state.connectionState = 'connected' this.shortToast('Classic Bluetooth connected') callback && callback(true) } catch (e) { console.error(e) this.state.connectionState = 'error' this.setErrorState(getErrorMessage(e)) callback && callback(false) try { btSocket.close() btSocket = null } catch (e1) { console.error(e1) } btInStream = null btOutStream = null return false } return true // #endif callback && callback(false) return false }, disConnDevice () { // #ifdef APP-PLUS if (btSocket != null) this.closeBtSocket() this.state.readThreadState = false // #endif }, closeBtSocket () { // #ifdef APP-PLUS this.state.readThreadState = false this.state.outputReady = false this.state.connectionState = 'idle' clearInterval(setIntervalId) setIntervalId = 0 if (!btSocket) return try { btSocket.close() } catch (e) { console.error(e) } btSocket = null btInStream = null btOutStream = null // #endif }, cancelDiscovery () { // #ifdef APP-PLUS if (btAdapter != null && btAdapter.isDiscovering()) { btAdapter.cancelDiscovery() } if (btFindReceiver != null && activity != null) { try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} btFindReceiver = null } this.state.discoveryDeviceState = false // #endif }, cancelClassicDiscovery () { this.cancelDiscovery() }, readData () { // #ifdef APP-PLUS if (!btSocket) { this.shortToast('Please connect Bluetooth device first.') this.state.connectionState = 'error' this.setErrorState('Please connect Bluetooth device first.') return false } try { btInStream = invoke(btSocket, 'getInputStream') btOutStream = invoke(btSocket, 'getOutputStream') } catch (e) { console.error(e) this.setErrorState(getErrorMessage(e)) this.closeBtSocket() return false } this.read() this.state.readThreadState = true this.state.outputReady = !!btOutStream return true // #endif return false }, read () { // #ifdef APP-PLUS clearInterval(setIntervalId) setIntervalId = setInterval(() => { if (this.state.readThreadState) { let start = new Date().getTime() let dataArr = [] try { while (btInStream && invoke(btInStream, 'available') !== 0) { let data = invoke(btInStream, 'read') dataArr.push(data) let current = new Date().getTime() if (current - start > 20) break } } catch (e) { this.state.readThreadState = false this.state.connectionState = 'error' this.setErrorState(getErrorMessage(e)) this.options.connExceptionCallback && this.options.connExceptionCallback(e) } if (dataArr.length > 0) { this.options.readDataCallback && this.options.readDataCallback(dataArr) } } }, 40) // #endif }, sendData (dataStr) { // #ifdef APP-PLUS if (!btOutStream) { this.shortToast('Output stream not ready') return false } let bytes = invoke(dataStr, 'getBytes', 'gb18030') try { this.sendByteData(bytes) } catch (e) { return false } return true // #endif return false }, /** * @param {any} byteData * @param {(percent0to99: number) => void} [onProgress] 按已写字节占比回调 0–99(完成由 sendByteDataAsync 再报 100) */ sendByteData (byteData, onProgress) { // #ifdef APP-PLUS if (!btOutStream) { this.state.outputReady = false this.state.connectionState = 'error' this.setErrorState('Classic Bluetooth output stream not ready', 'send') this.shortToast('Not connected') return false } const socketConnected = this.isSocketConnected() const connectionUsable = socketConnected || isConnectionStateConnected(this.state.connectionState) if (!connectionUsable) { this.state.connectionState = 'error' this.setErrorState('Classic Bluetooth connection is not ready', 'send') this.shortToast('Not connected') return false } try { const CHUNK_SIZE = 4096 const total = byteData && byteData.length ? byteData.length : 0 this.state.lastSendMode = 'chunk-write' this.state.lastSendError = '' for (let i = 0; i < byteData.length; i += CHUNK_SIZE) { const chunk = byteData.slice(i, Math.min(i + CHUNK_SIZE, byteData.length)) try { btOutStream.write(chunk) } catch (writeChunkError) { this.state.lastSendMode = 'byte-write-fallback' const signedChunk = normalizeWriteChunk(byteData, i, Math.min(i + CHUNK_SIZE, byteData.length)) for (let j = 0; j < chunk.length; j++) { invoke(btOutStream, 'write', normalizeWriteByte(signedChunk[j])) } } if (typeof onProgress === 'function' && total > 0) { const sent = Math.min(total, i + chunk.length) const pct = Math.min(99, Math.floor((sent / total) * 100)) try { onProgress(pct) } catch (pe) { console.error('sendByteData onProgress failed:', pe) } } } return true } catch (e) { const message = getErrorMessage(e) console.error('sendByteData failed:', e) this.setErrorState(message, 'send') this.state.connectionState = 'error' return false } // #endif return false }, /** * @param {(ok: boolean, errorMessage?: string) => void} callback * @param {(percent0to99: number) => void} [onProgress] 发送过程中 0–99 */ sendByteDataAsync (byteData, callback, onProgress) { // #ifdef APP-PLUS if (!btOutStream) { this.state.outputReady = false this.state.connectionState = 'error' this.setErrorState('Classic Bluetooth output stream not ready', 'send') runOnUiThread(() => { callback && callback(false, this.getLastError()) }) return false } const socketConnected = this.isSocketConnected() const connectionUsable = socketConnected || isConnectionStateConnected(this.state.connectionState) if (!connectionUsable) { this.state.connectionState = 'error' this.setErrorState('Classic Bluetooth connection is not ready', 'send') runOnUiThread(() => { callback && callback(false, this.getLastError()) }) return false } this.state.isSending = true this.state.lastSendError = '' startBackgroundTask(() => { let ok = false let errorMessage = '' try { ok = this.sendByteData(byteData, onProgress) if (!ok) { errorMessage = this.getLastError() || 'Classic Bluetooth send failed' } } catch (e) { errorMessage = getErrorMessage(e) this.setErrorState(errorMessage, 'send') this.state.connectionState = 'error' } finally { this.state.isSending = false } runOnUiThread(() => { callback && callback(ok, errorMessage) }) }) return true // #endif callback && callback(false, 'Classic Bluetooth is only available in the app.') return false }, isSocketConnected () { // #ifdef APP-PLUS if (!btSocket) return false try { return !!invoke(btSocket, 'isConnected') } catch (_) { return false } // #endif return false }, ensureConnection (address) { // #ifdef APP-PLUS const targetAddress = String(address || this.state.lastAddress || '') if (targetAddress) { this.state.lastAddress = targetAddress } if (btOutStream && (this.isSocketConnected() || isConnectionStateConnected(this.state.connectionState))) { this.state.outputReady = true return true } if (!targetAddress) { this.setErrorState('Bluetooth address missing') return false } let connected = false this.connDevice(targetAddress, (ok) => { connected = !!ok }) return connected && !!btOutStream // #endif return false }, getLastError () { return this.state.lastSendError || this.state.lastError || '' }, getDebugState () { return { ...this.state, socketConnected: this.isSocketConnected(), outputReady: !!btOutStream, inputReady: !!btInStream, } }, } export default blueToothTool