前言
本文介绍 uniapp
和 esp32
开发版之间的连接和信息通信过程。不使用 uniapp
插件,直接用提供的 api
进行实现。两个设备间会相互接收或者发送数据。下面的开发板使用 nodemcu esp32s
。
本文中的代码会完整展示:扫描设备->连接设备->相互间的数据发送->断开设备
简单的解释下过程
蓝牙传输数据的过程和 http
协议是不一样的,具体内网网上自行看即可。
思路:在本文中只需要知道一个蓝牙设备会提供一个服务和服务下提供的一个特征,需要分别拿到这两个对象的ID, 拿到ID就就可以愉快的通信了。
uniapp 代码实现步骤:
- 扫描蓝牙设备
- 连接蓝牙设备
- 获取服务ID
- 根据服务ID获取特征ID
- 根据上面拿到的信息调用 api 进行改写特征值(数据)和获取特征值(数据)
esp32 代码实现步骤:
- 启动一个蓝牙服务
- 设置蓝牙服务的服务ID以及服务的特征ID
- 监听特征值变化
- 改变特征值并通知app特征值变化
两个端的代码实现步骤并不难,最终目的就是对一个特征值的改写和获取的过程。
esp32代码
主要特别注意,在断开连接后需要重新启动蓝牙广播,不然客户端就连接不上了,再次连接会提示超时。
c
/*
基于Neil Kolban的IDF示例: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleServer.cpp
*/
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
// 在线生成 UUID
// https://www.uuidgenerator.net/
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
BLEServer *pServer;
// 特征定义,发送数据啥的都是全局使用,所以全局定义下。
BLECharacteristic *pCharacteristic;
// 是否已连接
bool deviceConnected = false;
// 蓝牙连接回调
class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *pServer) {
Serial.println("设备连接");
deviceConnected = true;
};
void onDisconnect(BLEServer *pServer) {
Serial.println("设备断开");
deviceConnected = false;
// 断开后需要重新开始广播,不然就再次连接就会提示 超时
// https://github.com/espressif/arduino-esp32/issues/6016
pServer->startAdvertising();
}
};
// 接收到特征值的回调
class CharacteristicCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
Serial.print("接收到特征值:");
std::string rxValue = pCharacteristic->getValue();
if (rxValue.length() > 0) {
String rxload = "";
for (int i = 0; i < rxValue.length(); i++) {
rxload += (char)rxValue[i];
// Serial.print(rxValue[i]);
}
Serial.println(rxload);
}
}
};
void setup() {
Serial.begin(115200);
Serial.println("低功耗蓝牙开始工作!");
BLEDevice::init("测试蓝牙");
pServer = BLEDevice::createServer();
// 服务连接回调
pServer->setCallbacks(new MyServerCallbacks());
// 创建一个服务
BLEService *pService = pServer->createService(SERVICE_UUID);
// 创建特征,注意,PROPERTY_READ PROPERTY_WRITE PROPERTY_NOTIFY 这三个都得启用
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY);
// 监听客户端改变特征值的回调
pCharacteristic->setCallbacks(new CharacteristicCallbacks());
// 改变特征值,客户端可以监听到特征值的改变
pCharacteristic->setValue("Hello World says Neil");
// 启动服务
pService->start();
// 下面这一堆代码固定写上就行
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06); // 帮助解决iPhone连接问题的功能
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
Serial.println("特征值定义完毕!");
}
void loop() {
if (deviceConnected) {
// 改变特征值并且发送通知给客户端
pCharacteristic->setValue("hello");
pCharacteristic->notify();
delay(3000);
// 改变特征值并且发送通知给客户端
pCharacteristic->setValue("xiaoming");
pCharacteristic->notify();
}
delay(3000);
}
uniapp代码
代码中有坑的地方都会注明问题地址。
vue
<template>
<view class="content">
<view>
<button @click="scan" class="scan-btn">刷新</button>
<button @click="send" class="scan-btn" v-if="connectId && connected">发送数据</button>
</view>
<view style="color:green;padding: 22px 8px;font-size: 22px;">接收到数据:{{received}}</view>
<view class="dev-list">
<h4>设备列表({{devices.length}}台):</h4>
<div v-for="item in devices" class="deviceItem" :key="item.deviceId">
<span>名称:{{item.name}}</span>
<span>id:{{item.deviceId}}</span>
<div style="text-align: right;width: 100%;font-size: 20px;">
<a @click="connectClick(item)" v-if="connectId !== item.deviceId">连接</a>
<a @click="disConnect(item)" v-else style="color: red;">断开连接</a>
</div>
</div>
</view>
</view>
</template>
<script>
// 文档地址 https://uniapp.dcloud.net.cn/api/system/ble.html
export default {
data() {
return {
// 蓝牙板子中的信息
// connectId: "E4:65:B8:74:AB:3A",
// SERVICE_UUID: "4fafc201-1fb5-459e-8fcc-c5c9c331914b",
// CHARACTERISTIC_UUID: "beb5483e-36e1-4688-b7f5-ea07361b26a8",
connectId: "",
SERVICE_UUID: "",
CHARACTERISTIC_UUID: "",
// 是否已连接
connected: false,
inited: false,
devices: [],
// 当前连接的设备id
// 接收到的数据
received: ""
}
},
onLoad() {
this.main();
},
onUnload() {
this.disConnect();
},
methods: {
// 开启蓝牙
initBlue() {
console.log('初始化蓝牙开始...')
const _this = this;
return new Promise((resolve) => {
uni.openBluetoothAdapter({
success(res) {
console.log('初始化蓝牙成功')
_this.inited = true;
// 扫描蓝牙设备
_this.scan();
resolve();
},
fail(err) {
console.log('初始化蓝牙失败', err)
uni.showModal({
title: '失败提示',
content: "初始化蓝牙失败",
showCancel: false
});
}
})
})
},
async main() {
// 初始化蓝牙
await this.initBlue();
},
// 扫描设备
async scan() {
const _this = this;
if (!_this.inited) {
await _this.initBlue();
}
uni.startBluetoothDevicesDiscovery({
success(res) {
console.log('启动搜索')
},
fail(err) {
console.log('启动搜索失败', err)
uni.showModal({
title: '失败提示',
content: "启动搜索失败",
showCancel: false
});
}
})
// 收到到蓝牙设备后的回调
uni.onBluetoothDeviceFound(({ devices }) => {
_this.devices.push(...devices);
// 去重
_this.devices = [..._this.devices].reduce((pre, cur) => {
if ((pre.find(item => item.deviceId == cur.deviceId)) == null) {
pre.push(cur)
}
return pre;
}, [])
})
},
// 字符串转为ArrayBuffer对象,参数为字符串
str2ab(str) {
var buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节
var bufView = new Uint16Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
},
// 点击某个设备的连接
async connectClick(item) {
console.log('即将连接蓝牙:', item);
// 如果当前有连接的设备,需要先断开
if (this.connected) {
await this.disConnect();
}
if (!this.inited) {
await this.initBlue();
// 扫描蓝牙设备
// this.scan();
}
this.connectId = item.deviceId;
this.connect();
},
// 连接蓝牙
connect() {
const _this = this;
console.log('设备id', _this.connectId);
return new Promise((resolve) => {
uni.showLoading({
title: '连接中...'
});
uni.createBLEConnection({
timeout: 5000,
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId: _this.connectId,
success(res) {
console.log("连接成功:", res);
// 必须延时 1000,不然拿不到服务信息
setTimeout(() => {
_this.connected = true;
// 获取服务的 ID, 会有多个 ID, 使用在硬件中定义的那个
// 这里有个问题问题,getBLEDeviceServices和getBLEDeviceCharacteristics如果拆出去后无法正常运行,所以只能写到 success 里面
uni.getBLEDeviceServices({
deviceId: _this.connectId,
success(res) {
// console.log('所有服务:', res)
if (!res.services.length) {
uni.showModal({
title: '失败提示',
content: "获取的服务长度为 0 ",
showCancel: false
});
return;
}
// 这里一般是获取最后一个,板子里把自定义的放到了最后
// 但不是所有情况都这样,具体需要看板子代码里面的定义
_this.SERVICE_UUID = res.services[res.services.length - 1].uuid;
// 必须延时 100,不然拿不到特征信息
setTimeout(() => {
// 获取服务下的特征的 ID, 会有多个 ID, 使用在硬件中定义的那个
uni.getBLEDeviceCharacteristics({
deviceId: _this.connectId,
serviceId: _this.SERVICE_UUID,
success(res) {
// console.log('所有特征:', res);
// 停止扫描设备, 否则会浪费性能
uni.stopBluetoothDevicesDiscovery({})
// 一般直接获取第一个就行
_this.CHARACTERISTIC_UUID = res.characteristics[0].uuid;
// 必须在这里的回调才能获取
uni.onBLECharacteristicValueChange(function(characteristic) {
const str_data = String.fromCharCode.apply(null, new Uint8Array(characteristic.value));
console.log("特征值:", str_data);
_this.received = str_data;
})
// 读取设备的特征值
uni.readBLECharacteristicValue({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId: _this.connectId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId: _this.SERVICE_UUID,
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId: _this.CHARACTERISTIC_UUID,
success(res) {
console.log('读取特征值成功:', res)
},
fail(err) {
console.log('读取特征值失败:', err)
}
})
// 监听特征值的变化
uni.notifyBLECharacteristicValueChange({
state: true, // 启用 notify 功能
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId: _this.connectId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId: _this.SERVICE_UUID,
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId: _this.CHARACTERISTIC_UUID,
success(res) {
console.log('监听特征值成功:', res)
},
fail(err) {
console.log('监听特征值失败:', err)
}
})
// 监听蓝牙连接状态
uni.onBLEConnectionStateChange(function(res) {
// 该方法回调中可以用于处理连接意外断开等异常情况
console.log(`设备连接状态改变 ${res.deviceId}, connected: ${res.connected}`)
})
},
fail(err) {
console.log('获取特征值失败', err)
uni.showModal({
title: '失败提示',
content: "获取特征值失败 " + err.errMsg,
showCancel: false
});
}
})
}, 100)
},
fail(err) {
console.log('获取蓝牙服务失败', err)
uni.showModal({
title: '失败提示',
content: "获取蓝牙服务失败 " + err.errMsg,
showCancel: false
});
}
})
resolve();
}, 1000);
},
fail(err) {
_this.connected = true;
_this.connectId = null
_this.SERVICE_UUID = null
_this.CHARACTERISTIC_UUID = null
console.log('连接服务失败:', err)
uni.showModal({
title: '失败提示',
content: "连接服务失败 " + err.errMsg,
showCancel: false
});
},
complete() {
uni.hideLoading();
}
})
})
},
disConnect() {
const _this = this;
if (!_this.connectId) return;
uni.showLoading({
title: 'loading...'
});
return new Promise((resolve) => {
uni.closeBLEConnection({
deviceId: _this.connectId,
success(res) {
console.log("断开连接成功:", res)
// 断开后必须释放蓝牙资源,不然再点击连接就连不上了
// https://uniapp.dcloud.net.cn/api/system/bluetooth.html
uni.closeBluetoothAdapter({
success() {
console.log('释放蓝牙资源成功')
_this.connected = false;
_this.connectId = null
_this.SERVICE_UUID = null
_this.CHARACTERISTIC_UUID = null;
_this.inited = false;
_this.received = false;
// 延时下保险点
setTimeout(() => {
uni.hideLoading();
resolve();
}, 600)
},
fail() {
uni.hideLoading();
}
})
},
fail(err) {
uni.hideLoading();
console.log('断开连接失败:', err)
uni.showModal({
title: '失败提示',
content: "断开连接失败 " + err.errMsg,
showCancel: false
});
}
})
})
},
send() {
const _this = this;
console.log("设备id:", this.connectId)
console.log("服务id:", this.SERVICE_UUID)
console.log("特征id:", this.CHARACTERISTIC_UUID)
// 发送数据
const reqData = _this.str2ab("hi!");
uni.writeBLECharacteristicValue({
// 这里的 deviceId 需要在 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取
deviceId: this.connectId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId: this.SERVICE_UUID,
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId: this.CHARACTERISTIC_UUID,
// 这里的value是ArrayBuffer类型
value: reqData,
success(res) {
console.log('特征值写入成功:', res.errMsg)
}
})
}
}
}
</script>
<style>
.scan-btn {
width: 200px;
background-color: skyblue;
margin-top: 12px;
}
.dev-list {
border: 1px solid #ccc;
margin: 6px;
padding: 6px;
box-sizing: border-box;
min-height: 200px;
background-color: #efefef;
}
.deviceItem {
padding: 12px 0px;
box-sizing: border-box;
border-bottom: 1px solid #ccc;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.deviceItem span {
padding: 3px;
flex-basis: 50%;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
}
.deviceItem a {
color: blue;
width: 100%;
text-align: right;
font-weight: 600;
}
.msg {
padding: 6px;
display: flex;
align-items: center;
}
.msg input {
border: 1px solid #ccc;
height: 40px;
}
.msg button {
flex: 1;
margin-left: 12px;
}
</style>