uniapp+蓝牙+esp32灯控
·
技术文章大纲:UniApp + 蓝牙 + ESP32 灯控开发指南
引言
- 背景介绍:物联网设备与移动端结合的常见场景
- 技术选型原因:UniApp跨平台优势、蓝牙通信的低功耗特性、ESP32的高性价比
- 文章目标:实现通过UniApp控制ESP32驱动的LED灯
硬件准备
- ESP32开发板选型建议(ESP32S3)
软件环境搭建
- UniApp开发环境:HBuilderX安装与项目创建
- ESP32开发环境:Arduino IDE
- 必要的库文件:BLE库(如
NimBLE-Arduino)、UniApp蓝牙API
uniapp端代码
<template>
<view class="container">
<view class="title">ESP32 蓝牙通信演示</view>
<view class="btn-group">
<button @click="openBluetoothAdapter" class="btn">初始化蓝牙</button>
<button @click="startBluetoothDevicesDiscovery" class="btn">搜索设备</button>
<button @click="stopBluetoothDevicesDiscovery" class="btn" v-if="isDiscovering">停止搜索</button>
<button @click="closeBluetoothAdapter" class="btn">关闭蓝牙</button>
</view>
<view class="device-list">
<view class="section-title">发现的设备:</view>
<view
class="device-item"
v-for="(device, index) in devices"
:key="index"
@click="createBLEConnection(device.deviceId, device.name)"
>
<text class="device-name">{{ device.name || '未知设备' }}</text>
<text class="device-id">{{ device.deviceId }}</text>
</view>
</view>
<view class="connection-status" v-if="connected">
<text>已连接设备: {{ connectedDeviceName }}</text>
</view>
<view class="communication-area" v-if="connected">
<view class="section-title">发送消息:</view>
<input
type="text"
v-model="messageToSend"
placeholder="输入要发送的消息"
class="message-input"
/>
<button @click="writeBLECharacteristicValue" class="send-btn">发送</button>
<view class="section-title">接收的消息:</view>
<scroll-view class="message-list" scroll-y="true">
<view class="message-item" v-for="(msg, index) in receivedMessages" :key="index">
<text>{{ msg }}</text>
</view>
</scroll-view>
</view>
<!-- 调试信息区域 -->
<view class="debug-area">
<view class="section-title">调试信息:</view>
<text class="debug-text">{{ debugInfo }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
// 蓝牙相关
isDiscovering: false,
devices: [],
connected: false,
connectedDeviceId: '',
connectedDeviceName: '',
serviceId: '4fafc201-1fb5-459e-8fcc-c5c9c331914b',
characteristicId: 'beb5483e-36e1-4688-b7f5-ea07361b26a8',
// 消息相关
messageToSend: '',
receivedMessages: [],
// 调试信息
debugInfo: '',
hasRegisteredDeviceFound: false
};
},
onUnload() {
this.cleanupBluetooth();
},
methods: {
// 初始化蓝牙适配器
openBluetoothAdapter() {
this.updateDebug('开始初始化蓝牙适配器...');
uni.openBluetoothAdapter({
success: (res) => {
this.updateDebug('初始化蓝牙成功');
this.showToast('初始化蓝牙成功');
// 延迟搜索,确保适配器就绪
setTimeout(() => {
this.startBluetoothDevicesDiscovery();
}, 500);
},
fail: (res) => {
this.updateDebug(`初始化蓝牙失败: ${res.errMsg}`);
this.showToast(`初始化失败: ${res.errMsg}`);
}
});
},
// 开始搜索设备
startBluetoothDevicesDiscovery() {
if (this.isDiscovering) return;
this.devices = [];
this.isDiscovering = true;
this.updateDebug('开始搜索蓝牙设备...');
uni.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false,
success: (res) => {
this.updateDebug('搜索设备命令已发出');
this.showToast('开始搜索设备');
if (!this.hasRegisteredDeviceFound) {
uni.onBluetoothDeviceFound((devices) => {
this.handleDeviceFound(devices);
});
this.hasRegisteredDeviceFound = true;
}
},
fail: (res) => {
this.updateDebug(`搜索设备失败: ${res.errMsg}`);
this.showToast(`搜索失败: ${res.errMsg}`);
this.isDiscovering = false;
}
});
setTimeout(() => {
this.stopBluetoothDevicesDiscovery();
}, 15000);
},
// 处理发现的设备
handleDeviceFound(devices) {
const device = devices.devices[0];
if (!device) return;
const isExist = this.devices.some(d => d.deviceId === device.deviceId);
if (!isExist) {
this.devices.push(device);
this.updateDebug(`发现设备: ${device.name || '未知'}, ID: ${device.deviceId}`);
}
},
// 停止搜索设备
stopBluetoothDevicesDiscovery() {
if (!this.isDiscovering) return;
uni.stopBluetoothDevicesDiscovery({
success: (res) => {
this.updateDebug('已停止搜索设备');
this.isDiscovering = false;
this.showToast('已停止搜索');
}
});
},
// 连接设备
createBLEConnection(deviceId, deviceName) {
this.stopBluetoothDevicesDiscovery();
this.updateDebug(`连接设备: ${deviceName}, ID: ${deviceId}`);
if (this.connected) {
uni.closeBLEConnection({ deviceId: this.connectedDeviceId });
}
uni.createBLEConnection({
deviceId,
success: (res) => {
this.updateDebug(`连接成功: ${deviceName}`);
this.connected = true;
this.connectedDeviceId = deviceId;
this.connectedDeviceName = deviceName || '未知设备';
this.showToast(`已连接 ${this.connectedDeviceName}`);
// 关键优化:延长获取服务的延迟,确保连接稳定
setTimeout(() => {
this.getBLEDeviceServices(deviceId);
}, 2000); // 延长至2秒,确保ESP32就绪
},
fail: (res) => {
this.updateDebug(`连接失败: ${res.errMsg}`);
this.showToast(`连接失败: ${res.errMsg}`);
}
});
},
// 获取设备服务
getBLEDeviceServices(deviceId) {
this.updateDebug(`获取服务: ${deviceId}`);
uni.getBLEDeviceServices({
deviceId,
success: (res) => {
this.updateDebug(`发现${res.services.length}个服务`);
res.services.forEach(service => {
this.updateDebug(`服务UUID: ${service.uuid}`);
});
// 强制匹配服务(忽略大小写和格式差异)
const service = res.services.find(s =>
s.uuid.toLowerCase().replace(/-/g, '') === this.serviceId.toLowerCase().replace(/-/g, '')
);
if (service) {
this.getBLEDeviceCharacteristics(deviceId, service.uuid);
} else {
this.updateDebug(`未找到目标服务: ${this.serviceId}`);
this.showToast('未找到目标服务');
}
},
fail: (res) => {
this.updateDebug(`获取服务失败: ${res.errMsg}`);
}
});
},
// 获取特征值
getBLEDeviceCharacteristics(deviceId, serviceId) {
this.updateDebug(`获取特征值: 服务=${serviceId}`);
uni.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (res) => {
this.updateDebug(`发现${res.characteristics.length}个特征值`);
res.characteristics.forEach(characteristic => {
this.updateDebug(`特征值UUID: ${characteristic.uuid}, 属性: ${this.getProps(characteristic.properties)}`);
});
// 强制匹配特征值(忽略大小写和格式差异)
const characteristic = res.characteristics.find(c =>
c.uuid.toLowerCase().replace(/-/g, '') === this.characteristicId.toLowerCase().replace(/-/g, '')
);
if (characteristic) {
if (characteristic.properties.notify || characteristic.properties.indicate) {
// 关键优化:连续两次启用Notify,确保生效
this.notifyBLECharacteristicValueChange(deviceId, serviceId, characteristic.uuid);
setTimeout(() => {
this.notifyBLECharacteristicValueChange(deviceId, serviceId, characteristic.uuid);
}, 500);
} else {
this.updateDebug('特征值不支持Notify/Indicate');
this.showToast('设备不支持接收数据');
}
} else {
this.updateDebug(`未找到目标特征值: ${this.characteristicId}`);
}
},
fail: (res) => {
this.updateDebug(`获取特征值失败: ${res.errMsg}`);
}
});
},
// 启用Notify(核心修复)
notifyBLECharacteristicValueChange(deviceId, serviceId, characteristicId) {
this.updateDebug(`启用Notify: ${characteristicId}`);
uni.notifyBLECharacteristicValueChange({
deviceId,
serviceId,
characteristicId,
state: true,
success: (res) => {
this.updateDebug('✅ Notify启用成功!');
this.showToast('已准备接收数据');
// 关键:直接读取一次当前值,确认通道是否通畅
uni.readBLECharacteristicValue({
deviceId,
serviceId,
characteristicId,
success: (res) => {
const value = this.ab2str(res.value);
this.updateDebug(`初始读取值: ${value}`);
}
});
// 注册监听(确保每次启用都重新注册)
uni.onBLECharacteristicValueChange((res) => {
this.updateDebug(`收到原始数据: ${JSON.stringify(res)}`);
this.handleCharacteristicValueChange(res);
});
},
fail: (res) => {
this.updateDebug(`❌ Notify启用失败: ${res.errMsg}`);
this.showToast(`接收失败: ${res.errMsg}`);
// 自动重试一次
setTimeout(() => {
this.notifyBLECharacteristicValueChange(deviceId, serviceId, characteristicId);
}, 1000);
}
});
},
// 处理接收到的数据
handleCharacteristicValueChange(res) {
try {
// 优化:支持多种编码格式尝试
let value = '';
// 尝试UTF-8解码
if (window.TextDecoder) {
value = new TextDecoder('utf-8').decode(res.value);
}
// 如果解码失败,尝试ASCII
if (!value || value === '') {
const uint8Array = new Uint8Array(res.value);
value = String.fromCharCode.apply(null, uint8Array);
}
if (value) {
this.updateDebug(`收到数据: ${value}`);
this.receivedMessages.push(`[${this.formatTime()}] 收到: ${value}`);
} else {
this.updateDebug('收到空数据或解码失败');
}
} catch (e) {
this.updateDebug(`数据解析错误: ${e.message}`);
}
},
// 发送数据
writeBLECharacteristicValue() {
if (!this.messageToSend) return;
try {
const buffer = this.str2ab(this.messageToSend);
uni.writeBLECharacteristicValue({
deviceId: this.connectedDeviceId,
serviceId: this.serviceId,
characteristicId: this.characteristicId,
value: buffer,
success: () => {
this.updateDebug(`发送成功: ${this.messageToSend}`);
this.receivedMessages.push(`[${this.formatTime()}] 发送: ${this.messageToSend}`);
this.messageToSend = '';
},
fail: (res) => {
this.updateDebug(`发送失败: ${res.errMsg}`);
}
});
} catch (e) {
this.updateDebug(`发送错误: ${e.message}`);
}
},
// 关闭蓝牙
closeBluetoothAdapter() {
this.cleanupBluetooth();
},
// 清理资源
cleanupBluetooth() {
this.updateDebug('清理蓝牙资源');
if (uni.offBluetoothAdapterStateChange) uni.offBluetoothAdapterStateChange();
if (uni.offBluetoothDeviceFound) uni.offBluetoothDeviceFound();
if (uni.offBLEConnectionStateChange) uni.offBLEConnectionStateChange();
if (uni.offBLECharacteristicValueChange) uni.offBLECharacteristicValueChange();
this.hasRegisteredDeviceFound = false;
if (this.connectedDeviceId) {
uni.closeBLEConnection({ deviceId: this.connectedDeviceId });
}
uni.closeBluetoothAdapter({
success: () => {
this.updateDebug('蓝牙已关闭');
this.connected = false;
this.devices = [];
}
});
},
// 辅助函数:ArrayBuffer转字符串
ab2str(buffer) {
try {
return new TextDecoder('utf-8').decode(buffer);
} catch (e) {
return String.fromCharCode.apply(null, new Uint8Array(buffer));
}
},
// 辅助函数:字符串转ArrayBuffer
str2ab(str) {
const buffer = new ArrayBuffer(str.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < str.length; i++) {
view[i] = str.charCodeAt(i);
}
return buffer;
},
// 辅助函数:显示提示
showToast(title) {
uni.showToast({ title, icon: 'none', duration: 2000 });
},
// 辅助函数:格式化时间
formatTime() {
const d = new Date();
return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}:${d.getSeconds().toString().padStart(2, '0')}`;
},
// 辅助函数:更新调试信息
updateDebug(info) {
console.log('[调试]', info);
this.debugInfo = `${this.formatTime()} - ${info}\n${this.debugInfo || ''}`.split('\n').slice(0, 10).join('\n');
},
// 辅助函数:解析特征值属性
getProps(properties) {
const props = [];
if (properties.read) props.push('READ');
if (properties.write) props.push('WRITE');
if (properties.notify) props.push('NOTIFY');
if (properties.indicate) props.push('INDICATE');
return props.join(',');
}
}
};
</script>
<style scoped>
/* 样式保持不变 */
.container { padding: 16px; background-color: #f5f5f5; min-height: 100vh; }
.title { font-size: 20px; font-weight: bold; text-align: center; margin-bottom: 20px; color: #333; }
.btn-group { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; }
.btn { flex: 1; min-width: 120px; padding: 10px; background-color: #007aff; color: white; border: none; border-radius: 5px; font-size: 14px; }
.device-list { background-color: white; border-radius: 8px; padding: 10px; margin-bottom: 20px; }
.section-title { font-size: 16px; font-weight: bold; margin: 10px 0; color: #333; }
.device-item { padding: 12px; border-bottom: 1px solid #eee; cursor: pointer; }
.device-item:last-child { border-bottom: none; }
.device-item:active { background-color: #f0f0f0; }
.device-name { font-size: 15px; color: #333; margin-bottom: 4px; display: block; }
.device-id { font-size: 12px; color: #666; word-break: break-all; }
.connection-status { background-color: #e8f4fd; color: #007aff; padding: 10px; border-radius: 5px; margin-bottom: 20px; text-align: center; }
.communication-area { background-color: white; border-radius: 8px; padding: 10px; margin-bottom: 20px; }
.message-input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; margin-bottom: 10px; font-size: 14px; }
.send-btn { width: 100%; padding: 10px; background-color: #007aff; color: white; border: none; border-radius: 5px; font-size: 14px; margin-bottom: 15px; }
.message-list { height: 300px; border: 1px solid #eee; border-radius: 5px; padding: 10px; background-color: #f9f9f9; }
.message-item { margin-bottom: 8px; padding: 6px; background-color: white; border-radius: 4px; font-size: 14px; }
.debug-area { background-color: #f0f0f0; border-radius: 8px; padding: 10px; font-size: 12px; }
.debug-text { color: #666; white-space: pre-wrap; word-break: break-all; }
</style>
ESP32端代码
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Arduino.h>
#include <FreeRTOS.h>
#include <task.h>
// 引脚定义
#define LED_PIN 2 // ESP32内置LED引脚,可根据实际硬件修改
// 服务UUID和特征值UUID
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
// BLE相关对象
BLEServer* pServer = NULL;
BLECharacteristic* pCharacteristic = NULL;
bool deviceConnected = false;
bool oldDeviceConnected = false;
// 全局状态变量
bool ledState = false; // LED状态:false-关闭,true-打开
SemaphoreHandle_t xSemaphore = NULL; // 信号量用于任务间同步
// 模拟传感器数据结构
struct SensorData {
float temperature; // 温度 (°C)
float humidity; // 湿度 (%)
int lightLevel; // 光照强度 (0-1023)
int counter; // 计数器
};
SensorData sensorData;
unsigned long previousSendMillis = 0;
const long sendInterval = 1000; // 发送间隔(毫秒)
// LED控制任务函数
void ledControlTask(void *parameter) {
for (;;) { // 无限循环
// 等待信号量或超时,确保任务不会一直占用CPU
if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
// 根据状态控制LED
digitalWrite(LED_BUILTIN, ledState ? HIGH : LOW);
Serial.print("LED状态更新: ");
Serial.println(ledState ? "打开" : "关闭");
}
// 短暂延时释放CPU资源
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
// 连接回调类
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
Serial.println("设备已连接");
// 连接后重置计数器
sensorData.counter = 0;
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
Serial.println("设备已断开连接");
// 重新开始广播,允许重连
pServer->startAdvertising();
}
};
// 特征值回调类 - 处理接收数据
class MyCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string rxValue = pCharacteristic->getValue();
if (rxValue.length() > 0) {
Serial.print("收到数据: ");
for (int i = 0; i < rxValue.length(); i++) {
Serial.print(rxValue[i]);
}
Serial.println();
// 解析收到的指令 (1:开灯, 2:关灯)
if (rxValue == "1") {
ledState = true;
xSemaphoreGive(xSemaphore); // 通知LED任务更新状态
pCharacteristic->setValue("指令已接收: 开灯");
}
else if (rxValue == "2") {
ledState = false;
xSemaphoreGive(xSemaphore); // 通知LED任务更新状态
pCharacteristic->setValue("指令已接收: 关灯");
}
else {
pCharacteristic->setValue("未知指令: 请发送1(开灯)或2(关灯)");
}
// 发送确认信息
if (deviceConnected) {
pCharacteristic->notify();
}
}
}
};
// 生成模拟传感器数据
void generateMockData() {
// 温度: 在20-30°C之间小幅波动
sensorData.temperature = 25.0 + sin(millis() / 10000.0) * 5.0;
// 湿度: 在40-60%之间小幅波动
sensorData.humidity = 50.0 + cos(millis() / 8000.0) * 10.0;
// 光照强度: 随机在300-800之间变化
sensorData.lightLevel = random(300, 800);
// 递增计数器
sensorData.counter++;
}
// 格式化数据为字符串以便传输
String formatDataForTransmission() {
String dataStr = "Temp: " + String(sensorData.temperature, 1) + "°C, ";
dataStr += "Humi: " + String(sensorData.humidity, 1) + "%, ";
dataStr += "Light: " + String(sensorData.lightLevel) + ", ";
dataStr += "Count: " + String(sensorData.counter) + ", ";
dataStr += "LED: " + String(ledState ? "ON" : "OFF");
return dataStr;
}
void setup() {
Serial.begin(115200);
randomSeed(analogRead(0)); // 初始化随机数生成器
// 初始化LED引脚
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW); // 初始关闭状态
// 创建信号量用于任务同步
xSemaphore = xSemaphoreCreateBinary();
if (xSemaphore == NULL) {
Serial.println("信号量创建失败!");
while (1); // 初始化失败则停止
}
// 初始化模拟数据
sensorData.temperature = 25.0;
sensorData.humidity = 50.0;
sensorData.lightLevel = 500;
sensorData.counter = 0;
// 创建LED控制任务
xTaskCreate(
ledControlTask, // 任务函数
"LEDControlTask", // 任务名称
1024, // 任务栈大小
NULL, // 传递给任务的参数
1, // 任务优先级 (低于主循环任务)
NULL // 任务句柄
);
// 初始化BLE设备
BLEDevice::init("ESP32-LED-Control");
// 创建BLE服务器
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// 创建BLE服务
BLEService *pService = pServer->createService(SERVICE_UUID);
// 创建BLE特征值
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_NOTIFY |
BLECharacteristic::PROPERTY_INDICATE
);
// 添加描述符
pCharacteristic->addDescriptor(new BLE2902());
// 设置特征值回调
pCharacteristic->setCallbacks(new MyCallbacks());
// 初始值
pCharacteristic->setValue("等待连接...");
// 启动服务
pService->start();
// 开始广播
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06); // 适用于Android 5.0及以上版本
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
Serial.println("等待设备连接...");
}
void loop() {
// 处理连接状态变化
if (!deviceConnected && oldDeviceConnected) {
delay(500); // 给蓝牙堆栈准备时间
pServer->startAdvertising(); // 重新开始广播
Serial.println("开始重新广播");
oldDeviceConnected = deviceConnected;
}
if (deviceConnected && !oldDeviceConnected) {
oldDeviceConnected = deviceConnected;
}
// 定时发送模拟数据和LED状态
unsigned long currentMillis = millis();
if (currentMillis - previousSendMillis >= sendInterval) {
previousSendMillis = currentMillis;
if (deviceConnected) {
// 生成并发送数据
generateMockData();
String dataToSend = formatDataForTransmission();
pCharacteristic->setValue(dataToSend.c_str());
pCharacteristic->notify();
Serial.println("发送数据: " + dataToSend);
}
}
// 主循环短暂延时,释放CPU
vTaskDelay(10 / portTICK_PERIOD_MS);
}
注意:uniapp需要在真机中模拟才能够搜索蓝牙
(目前只能怪从app端下发数据到esp32端进行灯控)
esp32端数据上传还存在问题
更多推荐

所有评论(0)