uni-app 连接 JDY-31 蓝牙串口模块实践

最近在做一个蓝牙 App,需要连接 JDY-31 模块,实现手机和单片机之间的数据通信。刚开始我按照 uni-app 常见的 BLE 蓝牙流程去写,使用了 openBluetoothAdapter、startBluetoothDevicesDiscovery、createBLEConnection、getBLEDeviceServices、notifyBLECharacteristicValueChange 等接口。

结果发现一个问题:手机上的蓝牙串口助手可以搜索到 JDY-31,但自己写的 App 却一直搜不到。

后来排查后发现,问题并不是代码写错了,而是 JDY-31 和 BLE 蓝牙不是同一种协议

一、JDY-31 不是 BLE 蓝牙

uni-app 官方提供的蓝牙接口主要是针对 BLE 低功耗蓝牙 的,比如 JDY-23、HM-10、AT-09 这类模块。

而 JDY-31 属于 经典蓝牙 SPP 串口模块,更接近 HC-05、HC-06 这一类。它走的是经典蓝牙串口协议,不存在 BLE 里的服务 UUID、特征值 UUID、notify 监听这些概念。

所以如果使用 BLE 接口去搜索 JDY-31,就会出现:

蓝牙串口助手能搜到 uni-app BLE 接口搜不到

二、解决思路

既然 JDY-31 是经典蓝牙 SPP,就不能再走 BLE 流程,而是需要在 Android App 端调用原生蓝牙能力,通过 BluetoothSocket 建立串口连接。

整体流程如下:

  1. 申请蓝牙和定位权限
  2. 获取 Android 蓝牙适配器
  3. 搜索附近经典蓝牙设备
  4. 读取已经配对过的蓝牙设备
  5. 筛选名称中包含 JDY、JDY31 或 JDY-31 的设备
  6. 点击设备后,如果未配对,先触发系统配对
  7. 配对成功后,通过 SPP UUID 创建 socket 连接
  8. 获取输入流和输出流
  9. 定时读取串口数据并显示到页面

SPP 常用 UUID 是:00001101-0000-1000-8000-00805F9B34FB

三.关键权限

经典蓝牙在 Android 上需要配置相关权限,尤其是 Android 12 以后,蓝牙权限更加严格。

manifest.json 中需要加入类似权限:

cpp 复制代码
"<uses-permission android:name=\"android.permission.BLUETOOTH\"/>",
"<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\"/>",
"<uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\"/>",
"<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>"

四.接口说明

1. 搜索附近和已配对设备

入口方法:

searchJdy31()

它会做几件事:

this.deviceList = [] this.showDeviceList = true this.searching = true this.statusText = '正在搜索JDY31...'

然后申请权限:

requestBluetoothPermission()

权限包括:

ACCESS_FINE_LOCATION BLUETOOTH BLUETOOTH_ADMIN BLUETOOTH_SCAN BLUETOOTH_CONNECT

之后调用:

startClassicSearch()

这个方法会:

  • 获取手机蓝牙适配器 BluetoothAdapter
  • 读取已配对设备 getBondedDevices()
  • 开始搜索附近设备 startDiscovery()
  • 通过广播接收附近发现的蓝牙设备

2. 筛选 JDY31

搜索到设备后,会进入:

addJdy31Device(name, address, bondState)

内部会判断设备名:

isJdy31Name(name)

目前只显示名称包含这些字段的设备:

JDY JDY31 JDY-31

如果你的模块改过名字,比如叫 BT05,这里就需要把过滤条件加进去。

3. 连接 JDY31

点击设备后调用:

connectJdy31(item)

如果设备已经配对,直接连接。

如果设备没有配对,会先调用:

device.createBond()

这时手机会弹出系统配对框,JDY31 常见密码是:

1234

或:

0000

配对成功后,广播监听会自动继续连接。

4. 打开串口 Socket

真正建立串口连接的是:

openJdy31Socket(device, adapter)

这里使用经典蓝牙 SPP UUID:

const sppUuid = UUID.fromString('00001101-0000-1000-8000-00805F9B34FB')

然后创建 socket:

this.socket = device.createRfcommSocketToServiceRecord(sppUuid) this.socket.connect()

连接成功后获取输入输出流:

this.inputStream = this.socket.getInputStream() this.outputStream = this.socket.getOutputStream()

这两个流就是后面接收和发送串口数据的核心。

5. 接收数据

连接成功后调用:

listenJdy31()

它内部用定时器轮询串口输入流:

const count = this.inputStream.available()

如果有数据,就逐字节读取:

data.push(this.inputStream.read() & 0xff)

然后显示两种格式:

this.messageHex = data.map(item => ('00' + item.toString(16)).slice(-2)).join('') this.message = String.fromCharCode.apply(null, data)

所以页面上能看到:

  • 普通字符内容
  • 十六进制内容

6. 发送字符

新增按钮直接调用:

sendJdy31('A') sendJdy31('B')

发送函数是:

sendJdy31(msg) { for (let i = 0; i < msg.length; i++) { this.outputStream.write(msg.charCodeAt(i)) } this.outputStream.flush() }

发送 'A' 时,实际发出的字节是:

0x41

发送 'B' 时,实际发出的字节是:

0x42

STM32 或其他单片机串口端可以按普通串口字符接收:

'A' 'B'

7. 断开连接

断开按钮调用:

disconnectJdy31()

它会关闭:

inputStream outputStream socket readTimer

核心清理方法是:

closeSocket()

断开后状态显示:

已断开连接

8. 重新连接

重新连接按钮调用:

reconnectJdy31()

连接成功过一次后,会保存上次设备:

this.lastDevice = { deviceId: data.deviceId, name: data.name || data.deviceId, bondState: data.bondState || 12, stateText: data.stateText || '已配对' }

重新连接时不重新搜索,直接用上一次地址:

this.connectJdy31(this.lastDevice)

五.完整代码

cpp 复制代码
<template>
    <view class="page">
        <view class="top">
            <view class="title">JDY31连接</view>
            <view class="status" :class="{ connected: connected }">{{ statusText }}</view>
        </view>

        <view class="search-box">
            <button class="search-btn" :loading="searching" @click="searchJdy31">
                {{ searching ? '正在搜索JDY31...' : '搜索附近/已配对JDY31' }}
            </button>
            <view class="action-row">
                <button class="action-btn disconnect" :disabled="!connected" @click="disconnectJdy31">断开连接</button>
                <button class="action-btn reconnect" :loading="connecting" :disabled="!lastDevice" @click="reconnectJdy31">重新连接</button>
            </view>
            <view class="action-row">
                <button class="action-btn send-a" :disabled="!connected" @click="sendJdy31('A')">发送字符A</button>
                <button class="action-btn send-b" :disabled="!connected" @click="sendJdy31('B')">发送字符B</button>
            </view>
            <view class="selected" v-if="connected && connectedDeviceName">
                已连接:{{ connectedDeviceName }}
            </view>
        </view>

        <view class="msg_x">
            <view class="msg_txt">
                监听到的内容:{{ message || '--' }}
            </view>
            <view class="msg_hex">
                监听到的内容(十六进制):{{ messageHex || '--' }}
            </view>
        </view>

        <view class="device-mask" v-if="showDeviceList" @click="closeDeviceList">
            <view class="device-panel" @click.stop>
                <view class="panel-head">
                    <text>JDY31设备</text>
                    <text class="panel-tip">{{ searching ? '搜索中' : '已停止' }}</text>
                </view>

                <scroll-view scroll-y class="device-list">
                    <view
                        class="device-item"
                        v-for="item in deviceList"
                        :key="item.deviceId"
                        @click="connectJdy31(item)"
                    >
                        <view class="device-line">
                            <text class="device-name">{{ item.name || 'JDY31' }}</text>
                            <text class="device-type">{{ item.stateText }}</text>
                        </view>
                        <view class="device-id">{{ item.deviceId }}</view>
                    </view>

                    <view class="empty" v-if="deviceList.length === 0">
                        没有发现JDY31,请确认模块已上电并处于可连接状态
                    </view>
                </scroll-view>
            </view>
        </view>
    </view>
</template>

<script>
export default {
    data() {
        return {
            deviceList: [],
            deviceId: '',
            message: '',
            messageHex: '',
            showDeviceList: false,
            searching: false,
            connecting: false,
            connected: false,
            connectedDeviceName: '',
            lastDevice: null,
            statusText: '未连接',
            adapter: null,
            receiver: null,
            bondReceiver: null,
            pendingConnectDevice: null,
            socket: null,
            inputStream: null,
            outputStream: null,
            readTimer: null
        }
    },
    onUnload() {
        this.stopSearch()
        this.unregisterBondReceiver()
        this.closeSocket()
    },
    methods: {
        searchJdy31() {
            this.deviceList = []
            this.showDeviceList = true
            this.searching = true
            this.statusText = '正在搜索JDY31...'

            this.requestBluetoothPermission(() => {
                this.startClassicSearch()
            })
        },

        requestBluetoothPermission(done) {
            if (typeof plus === 'undefined' || !plus.android || !plus.android.requestPermissions) {
                done()
                return
            }

            const permissions = [
                'android.permission.ACCESS_FINE_LOCATION',
                'android.permission.BLUETOOTH',
                'android.permission.BLUETOOTH_ADMIN',
                'android.permission.BLUETOOTH_SCAN',
                'android.permission.BLUETOOTH_CONNECT'
            ]

            plus.android.requestPermissions(permissions, () => {
                done()
            }, () => {
                done()
            })
        },

        startClassicSearch() {
            if (typeof plus === 'undefined' || !plus.android) {
                this.searching = false
                this.statusText = '经典蓝牙只支持Android App'
                uni.showToast({
                    title: '只支持Android App',
                    icon: 'none'
                })
                return
            }

            try {
                const BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter')
                const BluetoothDevice = plus.android.importClass('android.bluetooth.BluetoothDevice')
                const IntentFilter = plus.android.importClass('android.content.IntentFilter')
                const main = plus.android.runtimeMainActivity()

                this.adapter = BluetoothAdapter.getDefaultAdapter()
                if (!this.adapter) {
                    this.searching = false
                    this.statusText = '手机不支持蓝牙'
                    return
                }

                plus.android.importClass(this.adapter)
                if (!this.adapter.isEnabled()) {
                    this.adapter.enable()
                }

                this.addBondedJdy31Devices()
                this.unregisterReceiver()

                this.receiver = plus.android.implements('io.dcloud.feature.internal.reflect.BroadcastReceiver', {
                    onReceive: (context, intent) => {
                        plus.android.importClass(intent)
                        const action = intent.getAction()

                        if (action === BluetoothDevice.ACTION_FOUND) {
                            const device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                            plus.android.importClass(device)
                            this.addJdy31Device(device.getName(), device.getAddress(), device.getBondState())
                        }

                        if (action === BluetoothAdapter.ACTION_DISCOVERY_FINISHED && this.searching) {
                            this.searching = false
                            this.statusText = this.deviceList.length > 0 ? '请选择JDY31' : '未发现JDY31'
                        }
                    }
                })

                const filter = new IntentFilter()
                filter.addAction(BluetoothDevice.ACTION_FOUND)
                filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
                main.registerReceiver(this.receiver, filter)

                if (this.adapter.isDiscovering()) {
                    this.adapter.cancelDiscovery()
                }
                this.adapter.startDiscovery()
                console.log('开始搜索JDY31')
            } catch (err) {
                console.log('搜索JDY31失败')
                console.error(err)
                this.searching = false
                this.statusText = '搜索失败'
                uni.showToast({
                    title: '搜索失败',
                    icon: 'error'
                })
            }
        },

        addBondedJdy31Devices() {
            try {
                const bondedDevices = this.adapter.getBondedDevices()
                const iterator = bondedDevices.iterator()
                plus.android.importClass(iterator)

                while (iterator.hasNext()) {
                    const device = iterator.next()
                    plus.android.importClass(device)
                    this.addJdy31Device(device.getName(), device.getAddress(), device.getBondState())
                }
            } catch (err) {
                console.log('读取已配对设备失败')
                console.error(err)
            }
        },

        addJdy31Device(name, address, bondState) {
            if (!address || !this.isJdy31Name(name)) {
                return
            }

            const stateText = bondState === 12 ? '已配对' : '附近'
            const exists = this.deviceList.find(item => item.deviceId === address)
            if (exists) {
                exists.name = name || exists.name
                exists.bondState = bondState
                exists.stateText = stateText
            } else {
                this.deviceList.push({
                    deviceId: address,
                    name: name || 'JDY31',
                    bondState: bondState,
                    stateText: stateText
                })
            }
        },

        isJdy31Name(name) {
            if (!name) {
                return false
            }
            const upperName = String(name).toUpperCase()
            return upperName.indexOf('JDY') !== -1 || upperName.indexOf('JDY-31') !== -1 || upperName.indexOf('JDY31') !== -1
        },

        closeDeviceList() {
            this.showDeviceList = false
            this.stopSearch()
            if (!this.connected && !this.connecting) {
                this.statusText = '已停止搜索'
            }
        },

        connectJdy31(data) {
            if (this.connecting) {
                return
            }

            if (typeof plus === 'undefined' || !plus.android) {
                uni.showToast({
                    title: '只支持Android App',
                    icon: 'none'
                })
                return
            }

            try {
                console.log(data)
                this.connecting = true
                this.deviceId = data.deviceId
                this.connectedDeviceName = data.name || data.deviceId
                this.lastDevice = {
                    deviceId: data.deviceId,
                    name: data.name || data.deviceId,
                    bondState: data.bondState || 12,
                    stateText: data.stateText || '已配对'
                }
                this.showDeviceList = false
                this.stopSearch()
                this.statusText = '正在连接JDY31...'

                const BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter')
                const BluetoothDevice = plus.android.importClass('android.bluetooth.BluetoothDevice')
                const adapter = this.adapter || BluetoothAdapter.getDefaultAdapter()
                plus.android.importClass(adapter)

                const device = adapter.getRemoteDevice(data.deviceId)
                plus.android.importClass(device)
                adapter.cancelDiscovery()

                if (device.getBondState && device.getBondState() !== BluetoothDevice.BOND_BONDED) {
                    this.statusText = '请在弹窗中完成配对...'
                    this.pendingConnectDevice = data
                    this.registerBondReceiver(data)

                    const started = device.createBond()
                    if (!started) {
                        this.unregisterBondReceiver()
                        this.connecting = false
                        this.statusText = '配对失败'
                        uni.showToast({
                            title: '配对失败',
                            icon: 'error'
                        })
                    }
                    return
                }

                this.openJdy31Socket(device, adapter)
            } catch (err) {
                console.log('JDY31连接失败')
                console.error(err)
                this.closeSocket()
                this.connected = false
                this.connecting = false
                this.connectedDeviceName = ''
                this.statusText = '连接失败'
                uni.showToast({
                    title: '连接失败,请先配对',
                    icon: 'none'
                })
            }
        },

        registerBondReceiver(data) {
            this.unregisterBondReceiver()

            try {
                const BluetoothDevice = plus.android.importClass('android.bluetooth.BluetoothDevice')
                const IntentFilter = plus.android.importClass('android.content.IntentFilter')
                const BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter')
                const main = plus.android.runtimeMainActivity()

                this.bondReceiver = plus.android.implements('io.dcloud.feature.internal.reflect.BroadcastReceiver', {
                    onReceive: (context, intent) => {
                        plus.android.importClass(intent)
                        const action = intent.getAction()

                        if (action !== BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
                            return
                        }

                        const device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                        plus.android.importClass(device)
                        const address = device.getAddress()
                        const state = device.getBondState()

                        if (address !== data.deviceId) {
                            return
                        }

                        if (state === BluetoothDevice.BOND_BONDED) {
                            this.unregisterBondReceiver()
                            this.statusText = '配对成功,正在连接...'
                            const adapter = this.adapter || BluetoothAdapter.getDefaultAdapter()
                            plus.android.importClass(adapter)
                            this.openJdy31Socket(device, adapter)
                        } else if (state === BluetoothDevice.BOND_NONE) {
                            this.unregisterBondReceiver()
                            this.connecting = false
                            this.connectedDeviceName = ''
                            this.statusText = '配对失败'
                            uni.showToast({
                                title: '配对失败',
                                icon: 'error'
                            })
                        }
                    }
                })

                const filter = new IntentFilter()
                filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
                main.registerReceiver(this.bondReceiver, filter)
            } catch (err) {
                console.log('注册配对监听失败')
                console.error(err)
            }
        },

        unregisterBondReceiver() {
            if (!this.bondReceiver || typeof plus === 'undefined' || !plus.android) {
                return
            }

            try {
                const main = plus.android.runtimeMainActivity()
                main.unregisterReceiver(this.bondReceiver)
            } catch (err) {
                console.log('注销配对监听失败')
                console.error(err)
            }

            this.bondReceiver = null
            this.pendingConnectDevice = null
        },

        openJdy31Socket(device, adapter) {
            try {
                const UUID = plus.android.importClass('java.util.UUID')
                const sppUuid = UUID.fromString('00001101-0000-1000-8000-00805F9B34FB')

                this.closeSocket()
                this.socket = device.createRfcommSocketToServiceRecord(sppUuid)
                plus.android.importClass(this.socket)
                adapter.cancelDiscovery()
                this.socket.connect()

                this.inputStream = this.socket.getInputStream()
                this.outputStream = this.socket.getOutputStream()
                plus.android.importClass(this.inputStream)
                plus.android.importClass(this.outputStream)

                this.connected = true
                this.connecting = false
                this.connectedDeviceName = this.connectedDeviceName || this.deviceId
                this.statusText = 'JDY31已连接,监听中'
                uni.showToast({
                    title: '连接成功'
                })
                this.listenJdy31()
            } catch (err) {
                console.log('打开JDY31串口失败')
                console.error(err)
                this.closeSocket()
                this.connected = false
                this.connecting = false
                this.connectedDeviceName = ''
                this.statusText = '连接失败'
                uni.showToast({
                    title: '连接失败',
                    icon: 'error'
                })
            }
        },

        stopSearch() {
            if (!this.searching) {
                return
            }

            this.searching = false
            try {
                if (this.adapter) {
                    plus.android.importClass(this.adapter)
                    if (this.adapter.isDiscovering()) {
                        this.adapter.cancelDiscovery()
                    }
                }
            } catch (err) {
                console.log('停止搜索失败')
                console.error(err)
            }
            this.unregisterReceiver()
        },

        unregisterReceiver() {
            if (!this.receiver || typeof plus === 'undefined' || !plus.android) {
                return
            }

            try {
                const main = plus.android.runtimeMainActivity()
                main.unregisterReceiver(this.receiver)
            } catch (err) {
                console.log('注销广播失败')
                console.error(err)
            }
            this.receiver = null
        },

        listenJdy31() {
            if (this.readTimer) {
                clearInterval(this.readTimer)
                this.readTimer = null
            }

            this.readTimer = setInterval(() => {
                try {
                    if (!this.inputStream) {
                        return
                    }

                    const count = this.inputStream.available()
                    if (count <= 0) {
                        return
                    }

                    const data = []
                    for (let i = 0; i < count; i++) {
                        data.push(this.inputStream.read() & 0xff)
                    }

                    this.messageHex = data.map(item => ('00' + item.toString(16)).slice(-2)).join('')
                    this.message = String.fromCharCode.apply(null, data)
                    console.log('JDY31收到数据', this.messageHex, this.message)
                } catch (err) {
                    console.log('JDY31读取失败')
                    console.error(err)
                    this.closeSocket()
                    this.connected = false
                    this.connectedDeviceName = ''
                    this.statusText = 'JDY31已断开'
                }
            }, 120)
        },

        closeSocket() {
            if (this.readTimer) {
                clearInterval(this.readTimer)
                this.readTimer = null
            }

            try {
                if (this.inputStream) {
                    this.inputStream.close()
                }
            } catch (err) {
                console.log(err)
            }

            try {
                if (this.outputStream) {
                    this.outputStream.close()
                }
            } catch (err) {
                console.log(err)
            }

            try {
                if (this.socket) {
                    this.socket.close()
                }
            } catch (err) {
                console.log(err)
            }

            this.inputStream = null
            this.outputStream = null
            this.socket = null
        },

        disconnectJdy31() {
            this.stopSearch()
            this.unregisterBondReceiver()
            this.closeSocket()
            this.connected = false
            this.connecting = false
            this.connectedDeviceName = ''
            this.statusText = '已断开连接'
            uni.showToast({
                title: '已断开',
                icon: 'none'
            })
        },

        reconnectJdy31() {
            if (!this.lastDevice) {
                uni.showToast({
                    title: '没有可重连设备',
                    icon: 'none'
                })
                return
            }

            this.disconnectForReconnect()
            this.connectJdy31(this.lastDevice)
        },

        disconnectForReconnect() {
            this.stopSearch()
            this.unregisterBondReceiver()
            this.closeSocket()
            this.connected = false
            this.connecting = false
            this.connectedDeviceName = ''
        },

        sendJdy31(msg) {
            if (!this.outputStream) {
                uni.showToast({
                    title: 'JDY31未连接',
                    icon: 'none'
                })
                return
            }

            try {
                for (let i = 0; i < msg.length; i++) {
                    this.outputStream.write(msg.charCodeAt(i))
                }
                this.outputStream.flush()
            } catch (err) {
                console.error(err)
                uni.showToast({
                    title: '发送失败',
                    icon: 'error'
                })
            }
        }
    }
}
</script>

<style>
.page {
    min-height: 100vh;
    padding: 30rpx;
    box-sizing: border-box;
    background: #f5f7fb;
}

.top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 28rpx;
}

.title {
    font-size: 40rpx;
    font-weight: 700;
    color: #111827;
}

.status {
    max-width: 360rpx;
    padding: 10rpx 18rpx;
    border-radius: 8rpx;
    font-size: 24rpx;
    color: #4b5563;
    background: #e5e7eb;
}

.status.connected {
    color: #047857;
    background: #d1fae5;
}

.search-box {
    margin-bottom: 24rpx;
}

.search-btn {
    height: 88rpx;
    line-height: 88rpx;
    border-radius: 8rpx;
    color: #fff;
    background: #2563eb;
    font-size: 30rpx;
}

.action-row {
    display: flex;
    gap: 18rpx;
    margin-top: 18rpx;
}

.action-btn {
    flex: 1;
    height: 76rpx;
    line-height: 76rpx;
    border-radius: 8rpx;
    font-size: 28rpx;
    color: #fff;
}

.action-btn.disconnect {
    background: #dc2626;
}

.action-btn.reconnect {
    background: #059669;
}

.action-btn.send-a {
    background: #7c3aed;
}

.action-btn.send-b {
    background: #ea580c;
}

.action-btn[disabled] {
    color: #9ca3af;
    background: #e5e7eb;
}

.selected {
    margin-top: 18rpx;
    font-size: 26rpx;
    color: #374151;
}

.msg_x {
    border: 2rpx solid #10b981;
    border-radius: 8rpx;
    background: #fff;
    box-sizing: border-box;
    padding: 24rpx;
}

.msg_x .msg_txt {
    margin-bottom: 20rpx;
}

.msg_txt,
.msg_hex {
    font-size: 28rpx;
    color: #111827;
    word-break: break-all;
}

.device-mask {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    z-index: 99;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding-top: 160rpx;
    background: rgba(17, 24, 39, 0.36);
}

.device-panel {
    width: 690rpx;
    max-height: 760rpx;
    border-radius: 8rpx;
    overflow: hidden;
    background: #fff;
    box-shadow: 0 12rpx 36rpx rgba(15, 23, 42, 0.22);
}

.panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 24rpx;
    border-bottom: 1rpx solid #e5e7eb;
    font-size: 30rpx;
    font-weight: 600;
    color: #111827;
}

.panel-tip {
    font-size: 24rpx;
    font-weight: 400;
    color: #6b7280;
}

.device-list {
    height: 600rpx;
}

.device-item {
    padding: 22rpx 24rpx;
    border-bottom: 1rpx solid #edf0f4;
    background: #fff;
}

.device-item:active {
    background: #eff6ff;
}

.device-line {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 8rpx;
}

.device-name {
    flex: 1;
    font-size: 30rpx;
    color: #111827;
}

.device-type {
    min-width: 96rpx;
    padding: 4rpx 10rpx;
    border-radius: 8rpx;
    text-align: center;
    font-size: 22rpx;
    color: #047857;
    background: #d1fae5;
}

.device-id {
    font-size: 22rpx;
    color: #6b7280;
    word-break: break-all;
}

.empty {
    padding: 80rpx 20rpx;
    text-align: center;
    color: #6b7280;
    font-size: 26rpx;
}
</style>

六.实现效果

说明:这个只是dome接收数据 和发送的格式需要和你自己单片机的一样看上面接口介绍改就行



七.踩坑总结

这次主要踩了几个坑:

  1. JDY-31 不是 BLE,不能用 BLE 接口连接。
  2. 蓝牙串口助手能搜到,不代表 uni-app BLE 接口也能搜到。
  3. 经典蓝牙 SPP 需要 Android 原生能力。
  4. 第一次连接通常必须先配对。
  5. Android 12 以后必须注意 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT 权限。
  6. 修改 manifest.json 后,需要重新运行或重新打包,否则权限可能不生效。
  7. 如果 JDY-31 名称被改
相关推荐
小离a_a1 小时前
uniapp小程序封装圆环显示比例数据
android·小程序·uni-app
__zRainy__1 小时前
uni-app 全局容器实战系列(三):全局 NavBar 和 TabBar 组件设计
uni-app
熙芯XiChip1 小时前
CPLD核心原理与结构
单片机
Restart-AHTCM1 小时前
LangChain学习之模型 I/O 与输出解析器 (Output Parsers)(3/8)
前端·学习·langchain
Liu.7741 小时前
Vue3结合Element Plus封装点击查看大图的自定义指令
javascript·vue.js·elementui
lqj_本人1 小时前
鸿蒙PC:electron-markdownify 从普通 Electron 迁移到 OpenHarmony Electron HAP 的完整实践
前端·javascript·electron
番茄灭世神1 小时前
Vscode开发/调试ARM单片机最新教程
c语言·arm开发·vscode·stm32·嵌入式·gd32
代码搬运媛9 小时前
Jest 测试框架详解与实现指南
前端
为何创造硅基生物10 小时前
C语言 结构体内存对齐规则(通俗易懂版)
c语言·开发语言