
// // 【中盛模拟量采集模块 · 0-10V电压显示 BLE蓝牙推送】// 硬件: ESP32-S3 SSD1306 128×64 OLED TTL转RS485模块(自动收发)//// 接线:// OLED: G4 → SCL, G5 → SDA// 485: G17 → TX(发), G18 → RX(收) (ESP32-S3的Serial2)// TTL转485模块是自动收发, 不需要方向控制引脚!//// 通信: Modbus RTU, 中盛模块出厂默认: 地址1, 9600, 8N1// 采集: 1路0-10V, 寄存器地址0x0000, 功能码0x04(读输入寄存器)// 解析: 固定3位小数点 → 实际电压 寄存器原始值 ÷ 1000//// 蓝牙: ESP32-S3 仅支持 BLE, 使用 BLEDevice.h 库// 手机用 nRF Connect / LightBlue APP 连接接收电压数据// 设备名: ESP32S3-Voltage, 每1秒主动通知一次电压值// // ---- 引入 OLED 图形库 ----#include U8g2lib.h// ---- 引入 ESP32 BLE 库(ESP32 Arduino Core 自带, 无需安装) ----#include BLEDevice.h#include BLEServer.h#include BLEUtils.h#include BLE2902.h// ---- 引入 FastLED 库(用于控制板载 WS2812 RGB LED, 请在库管理器搜索安装) ----#include FastLED.h// GOOUUU·ESP32-S3 LED 配置 #define LED_PIN_TEST 48 // WS2812 数据引脚#define LED_IS_WS2812 true // trueWS2812(RGB), false普通GPIO// WS2812 色序: 红绿蓝不正常时改这里// 0GRB(WS2812默认), 1RGB, 2BRG, 3RBG, 4BGR, 5GBR#define WS2812_COLOR_ORDER 1#define WS2812_BRIGHTNESS 200 // 亮度(0-255), 太低红绿蓝会显得暗#define WS2812_NUM 1CRGB leds[WS2812_NUM];// RGB 颜色定义#define COLOR_OFF CRGB(0, 0, 0)#define COLOR_ON CRGB(0, 255, 0) // 默认点亮绿色// // OLED 显示屏对象构造:// U8G2_R0 : 不旋转(0度)// U8X8_PIN_NONE : 无复位引脚// SCL4, SDA5 : I2C 时钟/数据引脚(对应 ESP32-S3 的 G4/G5)U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, /* SCL */ 4, /* SDA */ 5);// 可调参数区 // 修改以下宏定义即可适配不同的从机地址/波特率/寄存器/引脚#define MODBUS_ADDR 1 // Modbus 从机地址(中盛模块出厂默认1)#define MODBUS_BAUD 9600 // 串口波特率(中盛模块出厂默认9600)#define MODBUS_REG_ADDR 0x0000 // 要读取的寄存器起始地址(0-10V通道对应0x0000)#define MODBUS_REG_NUM 1 // 要读取的寄存器数量(只读1路, 故为1)#define RS485_TX_PIN 17 // ESP32 发送引脚 → 接 RS485 模块的 TX(模块RX侧)#define RS485_RX_PIN 18 // ESP32 接收引脚 → 接 RS485 模块的 RX(模块TX侧)#define REFRESH_MS 200 // 主循环刷新周期(毫秒), 200ms 即 5Hz 刷新率#define BLE_NOTIFY_MS 1000 // BLE 推送周期(毫秒), 1000ms 即每秒推送一次电压#define LED_PIN 48 // ESP32-S3 板载 LED 引脚(GPIO48)#define LED_ACTIVE_LOW 1 // 1低电平点亮(常见), 0高电平点亮// BLE 参数区 // 自定义 UUID(可用 https://www.uuidgenerator.net/ 生成)#define SERVICE_UUID 4fafc201-1fb5-459e-8fcc-c5c9c331914b#define CHARACTERISTIC_UUID beb5483e-36e1-4688-b7f5-ea07361b26a8 // 电压推送特征值#define LED_CHARACTERISTIC_UUID 8e7f1a2b-3c4d-5e6f-7081-9283a4b5c6d7 // LED控制特征值#define BLE_DEVICE_NAME ESP32S3-Voltage // 手机扫描时看到的设备名// ---- 全局 BLE 对象 ----BLEServer* pServer nullptr;BLECharacteristic* pCharacteristic nullptr; // 电压推送特征值BLECharacteristic* pLedCharacteristic nullptr; // LED 控制特征值bool deviceConnected false; // 是否有手机已连接// ------------------------------------------------------------// BLE 连接/断开回调类// 功能: 当手机连接或断开时触发, 用于更新连接状态// 注意: 断开后必须重新 startAdvertising(), 否则无法被再次扫描到// ------------------------------------------------------------class MyServerCallbacks: public BLEServerCallbacks {void onConnect(BLEServer* pServer) {deviceConnected true;Serial.println([BLE] 手机已连接);}void onDisconnect(BLEServer* pServer) {deviceConnected false;Serial.println([BLE] 手机已断开, 重新广播...);BLEDevice::startAdvertising(); // 断开后重新广播, 允许重连}};// ------------------------------------------------------------// 函数: setLedState// 功能: 根据当前模式(FastLED/GPIO)设置 LED 开关状态// ------------------------------------------------------------void setLedState(bool on) {if (LED_IS_WS2812) {leds[0] on ? COLOR_ON : COLOR_OFF;FastLED.show();Serial.printf([LED] 开/关: %s (R%d G%d B%d)\r\n, on ? 开 : 关,leds[0].r, leds[0].g, leds[0].b);} else {pinMode(LED_PIN_TEST, OUTPUT);digitalWrite(LED_PIN_TEST, on ? HIGH : LOW);Serial.printf([LED] GPIO%d %s\r\n, LED_PIN_TEST, on ? 点亮 : 熄灭);}}// ------------------------------------------------------------// 函数: setLedColor// 功能: 设置 RGB 颜色, 自动按 WS2812_COLOR_ORDER 重新映射色序// ------------------------------------------------------------void setLedColor(CRGB color) {if (LED_IS_WS2812) {uint8_t r color.r, g color.g, b color.b;// 根据配置的色序重新排列 R,G,B 顺序switch (WS2812_COLOR_ORDER) {case 0: /* GRB */ leds[0].setRGB(g, r, b); break;case 1: /* RGB */ leds[0].setRGB(r, g, b); break;case 2: /* BRG */ leds[0].setRGB(b, r, g); break;case 3: /* RBG */ leds[0].setRGB(r, b, g); break;case 4: /* BGR */ leds[0].setRGB(b, g, r); break;case 5: /* GBR */ leds[0].setRGB(g, b, r); break;default: leds[0].setRGB(r, g, b); break;}FastLED.show();Serial.printf([LED] 颜色 (输入 R%d G%d B%d) - 输出 R%d G%d B%d, 色序%d\r\n,r, g, b, leds[0].r, leds[0].g, leds[0].b, WS2812_COLOR_ORDER);} else {bool on (color.r ! 0) || (color.g ! 0) || (color.b ! 0);pinMode(LED_PIN_TEST, OUTPUT);digitalWrite(LED_PIN_TEST, on ? HIGH : LOW);Serial.printf([LED] GPIO%d %s (GPIO无RGB)\r\n, LED_PIN_TEST, on ? 点亮 : 熄灭);}}// ------------------------------------------------------------// BLE 写入回调类(用于 LED 控制)// 功能: 当手机向 LED 特征值写入数据时触发, 解析指令控制 LED// 支持的指令(不区分大小写):// 开关类: ON 1 → 点亮默认色(绿)// OFF 0 → 熄灭// 颜色类: RED / R → 红灯// GREEN/ G → 绿灯// BLUE / B → 蓝灯// YELLOW/Y → 黄灯// WHITE / W → 白灯// RGB类: RRGGBB → 16进制颜色, 如 FF0000红// ------------------------------------------------------------class MyLedCallbacks: public BLECharacteristicCallbacks {void onWrite(BLECharacteristic* pCharacteristic) {const char* raw pCharacteristic-getValue().c_str();String value(raw);Serial.printf([BLE] 收到LED指令: \%s\ (len%d)\r\n, raw, value.length());if (value.length() 0) {Serial.println([BLE] 指令为空, 忽略);return;}value.toUpperCase();value.trim();// 1. 开关类指令if (value ON || value 1) {setLedState(true);} else if (value OFF || value 0) {setLedState(false);// 2. 颜色类指令} else if (value RED || value R) {setLedColor(CRGB(255, 0, 0));} else if (value GREEN || value G) {setLedColor(CRGB( 0, 255, 0));} else if (value BLUE || value B) {setLedColor(CRGB( 0, 0, 255));} else if (value YELLOW|| value Y) {setLedColor(CRGB(255, 255, 0));} else if (value WHITE || value W) {setLedColor(CRGB(255, 255, 255));// 3. 6位16进制 RGB} else if (value.length() 6 value.toInt() ! 0 /* 非纯数字也算合法 */) {// 逐字符校验是否 0-9 A-Fbool ok true;for (int i 0; i 6; i) {char c value[i];if (!((c 0 c 9) || (c A c F))) { ok false; break; }}if (ok) {long hexVal strtoul(value.c_str(), NULL, 16);uint8_t r (hexVal 16) 0xFF;uint8_t g (hexVal 8) 0xFF;uint8_t b hexVal 0xFF;setLedColor(CRGB(r, g, b));} else {Serial.printf([LED] 无效6进制颜色: %s\r\n, raw);}} else {Serial.printf([LED] 未知指令: \%s\\r\n, raw);Serial.println([LED] 支持: ON/OFF/1/0, RED(R)/GREEN(G)/BLUE(B)/YELLOW(Y)/WHITE(W), RRGGBB);}}};// ------------------------------------------------------------// 函数: modbusCRC// 功能: 计算 Modbus RTU 标准的 CRC-16 校验值// 算法: CRC-16/MODBUS, 多项式 0xA001(即 0x8005 的位反转), 初始值 0xFFFF// 参数:// data - 待校验的数据字节数组指针// len - 数据长度(字节数)// 返回: 16 位 CRC 校验值(低字节在前, 高字节在后)// ------------------------------------------------------------uint16_t modbusCRC(uint8_t* data, uint8_t len) {uint16_t crc 0xFFFF; // CRC 初始值for (uint8_t i 0; i len; i) { // 逐字节处理crc ^ data[i]; // 当前字节异或到 CRC 低 8 位for (uint8_t j 0; j 8; j) { // 每字节处理 8 位if (crc 0x0001) { // 最低位为 1crc (crc 1) ^ 0xA001; // 右移 1 位后异或多项式 0xA001} else { // 最低位为 0crc 1; // 仅右移 1 位}}}return crc; // 返回最终 CRC 值}// ------------------------------------------------------------// 函数: readVoltageRaw// 功能: 通过 Modbus RTU 向中盛模块发送读取请求, 并接收/解析响应// 流程: 组帧 → 发送 → 等待响应 → 校验 → 返回原始寄存器值// 返回:// 通信成功 → 返回 16 位寄存器原始值(0~10000, 对应 0.000~10.000V)// 通信失败 → 返回 0xFFFF(因 0-10V 量程内不会出现 0xFFFF, 故作错误标志)// ------------------------------------------------------------uint16_t readVoltageRaw() {// ---- 1. 组装 Modbus 请求帧(共 8 字节) ----// 帧格式: [从机地址][功能码][寄存器高][寄存器低][数量高][数量低][CRC低][CRC高]uint8_t request[8];request[0] MODBUS_ADDR; // 从机地址request[1] 0x04; // 功能码 0x04 读输入寄存器request[2] (MODBUS_REG_ADDR 8) 0xFF; // 寄存器起始地址 高字节request[3] MODBUS_REG_ADDR 0xFF; // 寄存器起始地址 低字节request[4] 0x00; // 寄存器数量 高字节request[5] MODBUS_REG_NUM; // 寄存器数量 低字节uint16_t crc modbusCRC(request, 6); // 对前 6 字节计算 CRCrequest[6] crc 0xFF; // CRC 低字节(Modbus 规定低字节在前)request[7] (crc 8) 0xFF; // CRC 高字节// ---- 2. 清空接收缓冲区, 发送请求帧 ----while (Serial2.available()) Serial2.read(); // 清空残留数据, 避免干扰本次通信Serial2.write(request, 8); // 发送 8 字节请求帧Serial2.flush(); // 等待发送完成// ---- 3. 等待从机响应(超时 500ms) ----// 正常响应帧长度 3(地址功能码字节数) 2*寄存器数 2(CRC) 7 字节(读1个寄存器)unsigned long startT millis();while (Serial2.available() 7) { // 等待至少 7 字节到达if (millis() - startT 500) return 0xFFFF; // 超过 500ms 无响应 → 通信失败delay(1); // 让出 CPU, 避免 busy-wait}// ---- 4. 读取完整响应帧 ----uint8_t response[32]; // 响应缓冲区(最大支持 32 字节)uint8_t idx 0; // 实际接收字节计数while (Serial2.available() idx 32) { // 持续读取直到缓冲区空或满response[idx] Serial2.read();delayMicroseconds(200); // 等待 200us, 给后续字节到达的时间}// ---- 5. 响应帧合法性校验 ----if (response[0] ! MODBUS_ADDR) return 0xFFFF; // 从机地址不匹配if (response[1] ! 0x04) return 0xFFFF; // 功能码不匹配(或异常响应)if (idx 5) return 0xFFFF; // 帧过短, 无法包含有效数据// ---- 6. CRC 校验(对接收帧去掉最后 2 字节 CRC 后重新计算并比对) ----uint16_t recvCRC modbusCRC(response, idx - 2); // 计算除 CRC 外所有字节的 CRCif ((response[idx - 2] ! (recvCRC 0xFF)) || // 比对低字节(response[idx - 1] ! ((recvCRC 8) 0xFF))) { // 比对高字节return 0xFFFF; // CRC 校验失败}// ---- 7. 提取寄存器原始值 ----// 响应帧格式: [地址][功能码][字节数][数据高][数据低][CRC低][CRC高]// index: 0 1 2 3 4 5 6// 数据为 16 位大端格式, 高字节在前uint16_t value (response[3] 8) | response[4]; // 拼接高低字节得到原始值return value;}// ------------------------------------------------------------// 函数: initBLE// 功能: 初始化 BLE 服务端, 创建 Service 和 Characteristic, 开始广播// 流程: init → createServer → createService → createCharacteristic// → addDescriptor → start → startAdvertising// ------------------------------------------------------------void initBLE() {Serial.println([BLE] 初始化中...);// 1. 创建 BLE 设备, 设置设备名(手机扫描时显示)BLEDevice::init(BLE_DEVICE_NAME);// 2. 创建 BLE ServerpServer BLEDevice::createServer();pServer-setCallbacks(new MyServerCallbacks()); // 注册连接/断开回调// 3. 创建 Service(用自定义 UUID)BLEService *pService pServer-createService(SERVICE_UUID);// 4. 创建电压推送 Characteristic(特征值)// 属性: READ(可读) NOTIFY(可主动通知推送)pCharacteristic pService-createCharacteristic(CHARACTERISTIC_UUID,BLECharacteristic::PROPERTY_READ |BLECharacteristic::PROPERTY_NOTIFY);// 5. 添加 Descriptor(描述符)// BLE2902 是 CCCD(Client Characteristic Configuration Descriptor)// 是 NOTIFY 功能必需的, 客户端通过它订阅通知pCharacteristic-addDescriptor(new BLE2902());// 6. 创建 LED 控制 Characteristic(特征值)// 属性: READ(可读) WRITE(可写) WRITE_NR(无响应写入, 兼容更多客户端)pLedCharacteristic pService-createCharacteristic(LED_CHARACTERISTIC_UUID,BLECharacteristic::PROPERTY_READ |BLECharacteristic::PROPERTY_WRITE |BLECharacteristic::PROPERTY_WRITE_NR);// 注册写入回调, 手机写入数据时触发 MyLedCallbacks::onWritepLedCharacteristic-setCallbacks(new MyLedCallbacks());pLedCharacteristic-setValue(OFF); // 初始状态显示为 OFF// 7. 设置电压推送初始值并启动服务pCharacteristic-setValue(waiting...); // 初始显示文字pService-start();// 8. 开始广播, 让手机能扫描到BLEAdvertising *pAdvertising BLEDevice::getAdvertising();pAdvertising-addServiceUUID(SERVICE_UUID); // 广播中包含服务 UUIDpAdvertising-setScanResponse(true); // 允许扫描响应(显示更多信息)BLEDevice::startAdvertising();Serial.printf([BLE] 已启动, 设备名: \%s\\r\n, BLE_DEVICE_NAME);Serial.println([BLE] 请用 nRF Connect / BLE调试助手 APP 扫描连接);Serial.println([BLE] LED控制: 向 LED 特征值写入 ON/OFF 或 1/0);}// ------------------------------------------------------------// 函数: sendVoltageViaBLE// 功能: 通过 BLE Notify 向已连接的手机推送电压字符串// 参数:// voltage - 电压浮点值(单位: V)// commOK - 通信是否成功(失败时推送错误信息)// ------------------------------------------------------------void sendVoltageViaBLE(float voltage, bool commOK) {if (!deviceConnected) return; // 无手机连接则不推送char buf[24];if (commOK) {// 通信正常: 推送 X.XXXV 格式(手动拆分避免 sprintf %f 兼容问题)int intPart (int)voltage;int decPart (int)((voltage - intPart) * 1000 0.5);sprintf(buf, %d.%03dV, intPart, decPart);} else {// 通信失败: 推送错误标识strcpy(buf, ERR:NoRS485);}pCharacteristic-setValue(buf); // 更新特征值pCharacteristic-notify(); // 主动通知订阅的客户端Serial.printf([BLE] 已推送: %s\r\n, buf);}// // setup: 系统初始化(仅执行一次)// void setup() {// ---- 初始化调试串口 ----Serial.begin(115200);delay(1500);Serial.println(\n\n 中盛0-10V采集 BLE LED );// ---- 初始化 LED ----if (LED_IS_WS2812) {// 注意: 虽然 FastLED 模板参数写的是 GRB, 但实际色序由 WS2812_COLOR_ORDER// 在 setLedColor() 中动态重排, 所以模板参数固定写 GRB 即可FastLED.addLedsWS2812, LED_PIN_TEST, GRB(leds, WS2812_NUM);FastLED.setBrightness(WS2812_BRIGHTNESS);leds[0] CRGB(0, 0, 0);FastLED.show();Serial.printf(LED: WS2812 模式, 引脚GPIO%d, 亮度%d, 色序%d(0GRB 1RGB 2BRG ...)\r\n,LED_PIN_TEST, WS2812_BRIGHTNESS, WS2812_COLOR_ORDER);} else {pinMode(LED_PIN_TEST, OUTPUT);digitalWrite(LED_PIN_TEST, LOW);Serial.printf(LED: GPIO 模式, 引脚GPIO%d\r\n, LED_PIN_TEST);}// ---- LED 自检: 循环变色 3 次 ----Serial.println( LED 自检 );CRGB testColors[] {CRGB(255,0,0), CRGB(0,255,0), CRGB(0,0,255)};for (int i 0; i 3; i) {if (LED_IS_WS2812) {// 自检也走 setLedColor, 让色序重排生效setLedColor(testColors[i]);} else {digitalWrite(LED_PIN_TEST, HIGH);}delay(300);if (LED_IS_WS2812) {leds[0] CRGB(0, 0, 0);FastLED.show();} else {digitalWrite(LED_PIN_TEST, LOW);}delay(200);}Serial.println(LED 自检完成! 看到闪烁就说明硬件正常);// ---- 初始化 RS485 ----Serial2.begin(MODBUS_BAUD, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN);Serial.printf(RS485: TXG%d, RXG%d, %d baud\r\n, RS485_TX_PIN, RS485_RX_PIN, MODBUS_BAUD);// ---- 初始化 OLED ----u8g2.begin();Serial.println(OLED 初始化完成);// ---- 初始化 BLE ----initBLE();}// // loop: 主循环(反复执行)// 每次循环: 读电压 → 串口打印 → OLED 显示 → 定时BLE推送 → 延时// void loop() {// ---- 1. 读取电压原始值 ----uint16_t rawValue readVoltageRaw(); // 调用 Modbus 读取float voltage 0.0; // 电压浮点值(单位: V)bool commOK (rawValue ! 0xFFFF); // 通信是否成功if (commOK) voltage (float)rawValue / 1000.0; // 原始值 ÷ 1000 实际电压// ---- 2. 串口调试输出 ----if (commOK) {Serial.printf(原始值: %d → 电压: %.3fV\r\n, rawValue, voltage);} else {Serial.println(通信失败! 检查接线和地址);}// ---- 3. OLED 显示绘制 ----u8g2.clearBuffer(); // 清空显示缓冲区// 顶部标题区u8g2.setFont(u8g2_font_ncenB10_tr); // 设置 10pt 字体u8g2.drawStr(0, 12, 0-10V Input); // 标题文字u8g2.drawLine(0, 16, 128, 16); // 分隔线(横线)if (commOK) {// ---- 通信正常: 显示电压数值 ----// 手动拆分整数/小数部分, 避免 sprintf 的 %f 在某些平台上不可用char buf[16];int intPart (int)voltage; // 整数部分(0~10)int decPart (int)((voltage - intPart) * 1000 0.5); // 小数部分(0~999), 0.5 用于四舍五入sprintf(buf, %d.%03d, intPart, decPart); // 格式化为 X.XXXu8g2.setFont(u8g2_font_fub25_tr); // 25pt 大字体显示数值u8g2.drawStr(5, 50, buf); // 在 (5,50) 位置绘制数值u8g2.setFont(u8g2_font_ncenB18_tr); // 18pt 字体u8g2.drawStr(110, 48, V); // 单位 Vu8g2.setFont(u8g2_font_ncenB10_tr); // 10pt 字体// 底部状态: 显示 BLE 连接状态, 方便用户判断u8g2.drawStr(0, 62, deviceConnected ? BLE:Connected : BLE:Waiting);} else {// ---- 通信失败: 显示占位符和错误提示 ----u8g2.setFont(u8g2_font_fub17_tr); // 17pt 字体u8g2.drawStr(20, 45, ----); // 用 ---- 占位u8g2.setFont(u8g2_font_ncenB18_tr); // 18pt 字体u8g2.drawStr(90, 45, V); // 单位 Vu8g2.setFont(u8g2_font_ncenB10_tr); // 10pt 字体u8g2.drawStr(0, 62, No Response!); // 底部错误提示}u8g2.sendBuffer(); // 将缓冲区内容推送到 OLED 显示// ---- 4. 定时 BLE 推送(每 BLE_NOTIFY_MS 毫秒推送一次) ----static unsigned long lastNotify 0; // 上次推送时间(静态变量保持)if (millis() - lastNotify BLE_NOTIFY_MS) {sendVoltageViaBLE(voltage, commOK); // 推送电压数据lastNotify millis(); // 更新推送时间戳}// ---- 5. BLE 写入轮询兜底(防止 onWrite 回调丢失, 支持全部颜色指令) ----static String lastLedCmd ;if (pLedCharacteristic ! nullptr) {String currentCmd String(pLedCharacteristic-getValue().c_str());if (currentCmd.length() 0 currentCmd ! lastLedCmd) {lastLedCmd currentCmd;String cmd currentCmd;cmd.toUpperCase();cmd.trim();Serial.printf([轮询兜底] 检测到LED指令: \%s\\r\n, cmd.c_str());if (cmd ON || cmd 1) {setLedState(true);} else if (cmd OFF || cmd 0) {setLedState(false);} else if (cmd RED || cmd R) {setLedColor(CRGB(255, 0, 0));} else if (cmd GREEN || cmd G) {setLedColor(CRGB( 0, 255, 0));} else if (cmd BLUE || cmd B) {setLedColor(CRGB( 0, 0, 255));} else if (cmd YELLOW|| cmd Y) {setLedColor(CRGB(255, 255, 0));} else if (cmd WHITE || cmd W) {setLedColor(CRGB(255, 255, 255));} else if (cmd.length() 6) {bool ok true;for (int i 0; i 6; i) {char c cmd[i];if (!((c 0 c 9) || (c A c F))) { ok false; break; }}if (ok) {long hexVal strtoul(cmd.c_str(), NULL, 16);uint8_t r (hexVal 16) 0xFF;uint8_t g (hexVal 8) 0xFF;uint8_t b hexVal 0xFF;setLedColor(CRGB(r, g, b));}}}}delay(REFRESH_MS); // 按设定周期延时(默认 200ms)}