技术文章大纲: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端数据上传还存在问题

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐