ESP32+Web BLE实战:手把手教你打造智能旋钮控制器(附完整代码)

ESP32+Web BLE实战:手把手教你打造智能旋钮控制器(附完整代码) ESP32Web BLE实战手把手教你打造智能旋钮控制器附完整代码在物联网设备开发中蓝牙低功耗BLE技术因其低功耗、低成本的特点成为连接智能硬件的首选方案。本文将带你从零开始基于ESP32开发板和Web BLE API构建一个完整的智能旋钮控制系统。这个项目不仅适用于家庭自动化控制也可扩展应用于工业设备调节、多媒体控制等场景。1. 项目架构与核心组件智能旋钮控制器的核心由三部分组成硬件层ESP32开发板负责采集旋钮编码器信号和按钮状态通信层BLE GATT协议实现设备与浏览器间的双向数据传输应用层Web界面提供可视化控制面板技术选型对比表方案开发难度功耗延迟适用场景经典蓝牙中等高低音频传输BLE低极低中等物联网设备WiFi高高低大数据量传输提示ESP32同时支持BLE和WiFi但针对旋钮控制这类低频次小数据量场景BLE是更优选择2. ESP32 BLE服务端开发2.1 硬件准备与开发环境所需物料清单ESP32开发板推荐ESP32-WROOM-32EC11旋转编码器模块2个轻触开关10KΩ电阻若干面包板及连接线开发环境配置步骤安装Arduino IDE 2.0添加ESP32开发板支持ESP32 by Espressif Systems安装BLE库BLEDevice、BLEUtils、BLEServer// 基础硬件测试代码 void setup() { Serial.begin(115200); pinMode(ENCODER_A, INPUT); pinMode(ENCODER_B, INPUT); pinMode(BUTTON1, INPUT_PULLUP); pinMode(BUTTON2, INPUT_PULLUP); } void loop() { int encoderVal readEncoder(); bool btn1State digitalRead(BUTTON1); bool btn2State digitalRead(BUTTON2); Serial.printf(Encoder:%d Btn1:%d Btn2:%d\n, encoderVal, btn1State, btn2State); delay(100); }2.2 BLE服务构建关键代码创建自定义GATT服务需要定义三个核心元素服务UUID16位或128位特征值Characteristics及其属性描述符Descriptors#include BLEDevice.h #include BLEUtils.h #include BLEServer.h #define SERVICE_UUID 4fafc201-1fb5-459e-8fcc-c5c9c331914b #define CHARACTERISTIC_UUID beb5483e-36e1-4688-b7f5-ea07361b26a8 BLEServer* pServer; BLECharacteristic* pCharacteristic; void setupBLE() { BLEDevice::init(SmartKnob); pServer BLEDevice::createServer(); BLEService *pService pServer-createService(SERVICE_UUID); pCharacteristic pService-createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY ); pService-start(); BLEAdvertising *pAdvertising BLEDevice::getAdvertising(); pAdvertising-addServiceUUID(SERVICE_UUID); pAdvertising-setScanResponse(true); BLEDevice::startAdvertising(); }3. Web BLE客户端开发3.1 浏览器API基础用法Web BLE API提供了四个核心方法requestDevice()- 扫描并选择BLE设备gatt.connect()- 建立GATT连接getPrimaryService()- 获取服务getCharacteristic()- 获取特征值典型连接流程async function connectDevice() { const device await navigator.bluetooth.requestDevice({ filters: [{ name: SmartKnob }], optionalServices: [SERVICE_UUID] }); const server await device.gatt.connect(); const service await server.getPrimaryService(SERVICE_UUID); const characteristic await service.getCharacteristic(CHARACTERISTIC_UUID); return characteristic; }3.2 数据通信实现ESP32与Web端的数据交换采用二进制格式定义如下协议字节偏移长度说明04旋钮值int3241按钮1状态0/151按钮2状态0/162CRC校验Web端数据解析示例characteristic.addEventListener(characteristicvaluechanged, event { const value event.target.value; const knobValue value.getInt32(0, true); const btn1 value.getUint8(4); const btn2 value.getUint8(5); updateUI(knobValue, btn1, btn2); }); await characteristic.startNotifications();4. 实战问题解决方案4.1 多特征值传输异常处理原始方案中同时Notify两个特征值会导致Web端接收异常。解决方案是合并多个参数到单个特征值使用结构体打包数据在Web端解析复合数据优化后的ESP32代码void sendKnobData(int knobPos, bool btn1, bool btn2) { uint8_t buffer[8]; memcpy(buffer[0], knobPos, sizeof(knobPos)); buffer[4] btn1 ? 1 : 0; buffer[5] btn2 ? 1 : 0; pCharacteristic-setValue(buffer, sizeof(buffer)); pCharacteristic-notify(); }4.2 断线重连机制BLE连接不稳定是常见问题需要实现连接状态监测断开后自动重新广播心跳包检测void loop() { static bool lastConnected false; if (deviceConnected !lastConnected) { Serial.println(Device connected); lastConnected true; } if (!deviceConnected lastConnected) { Serial.println(Device disconnected); pServer-startAdvertising(); lastConnected false; } if (deviceConnected) { sendKnobData(getEncoderValue(), readButtons()); delay(100); } }5. 完整项目代码实现5.1 ESP32完整代码#include BLEDevice.h #include BLEUtils.h #include BLEServer.h // 硬件引脚定义 #define ENCODER_A 34 #define ENCODER_B 35 #define BUTTON1 32 #define BUTTON2 33 // BLE UUID定义 #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) { deviceConnected true; } void onDisconnect(BLEServer* pServer) { deviceConnected false; } }; void setup() { Serial.begin(115200); // 初始化硬件 pinMode(ENCODER_A, INPUT); pinMode(ENCODER_B, INPUT); pinMode(BUTTON1, INPUT_PULLUP); pinMode(BUTTON2, INPUT_PULLUP); // 初始化BLE BLEDevice::init(SmartKnob); pServer BLEDevice::createServer(); pServer-setCallbacks(new MyServerCallbacks()); BLEService *pService pServer-createService(SERVICE_UUID); pCharacteristic pService-createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY ); pService-start(); BLEAdvertising *pAdvertising BLEDevice::getAdvertising(); pAdvertising-addServiceUUID(SERVICE_UUID); pAdvertising-setScanResponse(true); BLEDevice::startAdvertising(); } void loop() { static int lastPos 0; int currentPos readEncoder(); bool btn1 !digitalRead(BUTTON1); bool btn2 !digitalRead(BUTTON2); if (deviceConnected (currentPos ! lastPos || btn1 || btn2)) { sendKnobData(currentPos, btn1, btn2); lastPos currentPos; } delay(50); }5.2 Web前端完整代码!DOCTYPE html html head titleSmart Knob Controller/title style .knob { width: 200px; height: 200px; border-radius: 50%; background: #f0f0f0; position: relative; margin: 30px auto; } .indicator { position: absolute; width: 20px; height: 20px; background: red; border-radius: 50%; top: 10px; left: 90px; } /style /head body h1Smart Knob Controller/h1 button idconnectBtnConnect/button div classknob div classindicator idknobIndicator/div /div div pButton 1: span idbtn1StateOFF/span/p pButton 2: span idbtn2StateOFF/span/p /div script const serviceUUID 4fafc201-1fb5-459e-8fcc-c5c9c331914b; const characteristicUUID beb5483e-36e1-4688-b7f5-ea07361b26a8; let characteristic; document.getElementById(connectBtn).addEventListener(click, async () { try { const device await navigator.bluetooth.requestDevice({ filters: [{ name: SmartKnob }], optionalServices: [serviceUUID] }); const server await device.gatt.connect(); const service await server.getPrimaryService(serviceUUID); characteristic await service.getCharacteristic(characteristicUUID); characteristic.addEventListener(characteristicvaluechanged, handleData); await characteristic.startNotifications(); document.getElementById(connectBtn).textContent Connected; } catch (error) { console.error(Connection failed:, error); } }); function handleData(event) { const value event.target.value; const knobValue value.getInt32(0, true); const btn1 value.getUint8(4); const btn2 value.getUint8(5); updateUI(knobValue, btn1, btn2); } function updateUI(knobValue, btn1, btn2) { const angle (knobValue % 360) * Math.PI / 180; const indicator document.getElementById(knobIndicator); const x 90 80 * Math.sin(angle); const y 90 - 80 * Math.cos(angle); indicator.style.left ${x}px; indicator.style.top ${y}px; document.getElementById(btn1State).textContent btn1 ? ON : OFF; document.getElementById(btn2State).textContent btn2 ? ON : OFF; } /script /body /html6. 项目优化与扩展6.1 性能优化技巧数据压缩对旋钮值采用差值编码而非绝对位置传输频率控制设置最小变化阈值如旋钮变化超过5度才发送批量传输积累多个事件后一次性发送// 优化后的数据传输逻辑 void sendOptimizedData() { static int lastSentPos 0; static unsigned long lastSendTime 0; int currentPos readEncoder(); unsigned long now millis(); if (abs(currentPos - lastSentPos) 5 || now - lastSendTime 200) { sendKnobData(currentPos, readButtons()); lastSentPos currentPos; lastSendTime now; } }6.2 功能扩展方向多设备联动一个旋钮控制多个BLE设备模式切换通过长按按钮切换控制模式配置保存使用ESP32的Flash存储保存偏好设置OTA升级通过BLE实现固件无线更新模式切换实现示例enum ControlMode { VOLUME, BRIGHTNESS, ZOOM }; ControlMode currentMode VOLUME; void checkModeSwitch() { static unsigned long pressStart 0; if (digitalRead(BUTTON1) LOW) { if (pressStart 0) { pressStart millis(); } else if (millis() - pressStart 2000) { currentMode (ControlMode)((currentMode 1) % 3); sendModeChange(currentMode); pressStart 0; } } else { pressStart 0; } }