基于 ESP32-C3 pro mini 的蓝牙电门锁控制设备。手机连接后自动导通 MOS 开关管,断开后自动关闭。使用自复位按钮控制蓝牙广播,配对码固定为 123456。

功能特性

  • 蓝牙名称:EBike
  • 配对码:123456(静态,可在代码中修改)
  • HID 类型:消费者控制(Consumer Control)
  • 按下 GPIO3 按钮开始广播,GPIO8 LED(板载LED)闪亮,再次按下停止广播
  • 蓝牙连接成功 → GPIO2 输出高电平,MOS 导通,GPIO8 LED 点亮
  • 蓝牙断开 → GPIO2 输出低电平,MOS 断开,GPIO8 LED 熄灭
  • 未连接且未广播时,按按钮启动广播;已连接时按钮无效
  • 每次都需要按按钮启动广播,降低待机功耗,加强安全性

硬件清单

名称 数量 说明
ESP32-C3 Pro Mini 1 主控板
双 MOS 管开关控制板 1 控制负载通断
自复位按钮 1 触发广播
公对母杜邦线 若干 连接用
LED(可选) 1 若板载 GPIO8 LED 不可用,可以选择性外接
负载(不在模块范围内) 1 电动车电门锁

ESP32-C3 pro mini

双mos管开关控制板

接线说明

ESP32-C3 与按钮(如图)

ESP32-C3 按钮 说明
GPIO3 一端 另一端接 GND
GND 另一端 共地

按钮使用内部上拉,无需外接上拉电阻。

ESP32-C3 与双 MOS 管控制板(如模拟图,不完全正确)

ESP32-C3 MOS 控制板 电门锁 说明
GPIO2 TRIG/PWM - 高电平导通,低电平断开
GND GND - 必须与主控板共地
- OUT+ 控制器线 -
- VCC+ 12V供电线 -

MOS 控制板的输出端接负载,注意负载电流不要超过 MOS 板额定值。

ESP32-C3 与 LED

ESP32-C3 LED 说明
GPIO8 正极(串限流电阻) 高电平点亮
GND 负极 共地

若使用板载 LED,请确认 GPIO8 是否为普通 GPIO。部分开发板 GPIO8 连接的是 WS2812,需改用相应库驱动。

软件环境

  • Arduino IDE 2.x
  • ESP32 Arduino 核心(我的环境为3.3.12版本)
  • NimBLE-Arduino 2.5.1
  • 开发板选择:ESP32C3 Dev Module
  • Flash 模式:DIO
  • 上传速度:921600(可按实际调整)

库安装

  1. 打开 Arduino IDE → 工具 → 管理库
  2. 搜索 NimBLE-Arduino
  3. 选择版本 2.5.1 安装
  4. 确保已安装 ESP32 开发板支持包

编译与上传

  1. 将代码复制到 Arduino IDE 新建的 .ino 文件中
  2. 选择正确的开发板和端口
  3. 点击编译上传

使用方法

  1. 上电后,ESP32 不广播,MOS 断开,LED 熄灭
  2. 按下按钮(GPIO3),LED闪亮
  3. 手机打开蓝牙,搜索 EBike 并点击连接
  4. 手机弹出配对码输入框,输入 123456
  5. 连接成功:MOS 导通,LED 常亮
  6. 手机主动断开或超出范围:MOS 断开,LED 熄灭
  7. 再次按下按钮可停止广播;已连接状态下按钮无效
  8. 断开后需重新按按钮启动广播

配对码说明

  • 固定配对码:123456
  • 安卓手机连接 HID 设备时会强制弹出配对码输入框
  • 如需修改,请更改代码中的:
    #define STATIC_PASSKEY  123456
    

注意事项

  • ESP32-C3 GPIO 为 3.3V 电平,请确认 MOS 控制板支持 3.3V 控制信号
  • 双 MOS 管控制板用于大电流时,务必注意散热和线径
  • ESP32 与 MOS 控制板 必须共地
  • GPIO2 和 GPIO8 是 ESP32-C3 的 strapping 引脚,外接电路可能影响启动。若无法启动,请先断开外设再试
  • 蓝牙连接后手机可能显示为“输入设备”,但实际无键盘功能,不会影响输入法
  • 若无法配对,请删除手机蓝牙记录,重启 ESP32 后重新按按钮广播再配对
  • NimBLE 2.5.1 与 ESP32 核心版本可能存在兼容性问题,建议使用推荐版本

常见问题

Q:手机搜索不到 EBike?
A:确认已按下按钮启动广播;查看串口是否输出“广播已启动”。

Q:连接后 MOS 不导通?
A:检查 GPIO2 接线、MOS 板供电、是否共地;用万用表测量 GPIO2 是否为高电平。

Q:配对码输入后连接失败?
A:删除手机蓝牙记录,重启 ESP32,重新按按钮广播再配对。

Q:上传失败?
A:检查开发板选择、端口、USB 线;ESP32-C3 可能需要按住 BOOT 键进入下载模式。

#include <Arduino.h>
#include <NimBLEDevice.h>
#include <NimBLEHIDDevice.h>
#include <HIDTypes.h>

// ==================== 引脚定义 ====================
#define BUTTON_PIN  3   // 广播触发按钮
#define MOS_PIN     2   // MOS 开关管控制引脚
#define LED_PIN     8   // ESP32 板载 LED

// ==================== 配对码 ====================
#define STATIC_PASSKEY  123456  // 6 位配对码,可自行修改

// ==================== LED 闪烁参数 ====================
#define LED_BLINK_INTERVAL  300  // 广播时闪烁间隔(毫秒)

// ==================== 全局对象 ====================
NimBLEHIDDevice* hid = nullptr;
NimBLECharacteristic* inputReport = nullptr;
bool isConnected = false;       // 蓝牙连接状态
bool isAdvertising = false;     // 广播状态
uint8_t lastButtonState = LOW;  // 按钮上次状态

unsigned long lastBlinkTime = 0;  // 上次 LED 翻转时间
bool ledBlinkState = false;       // 当前闪烁电平

// ==================== 蓝牙设备控制报告描述符 ====================
static const uint8_t kReportMap[] = {
    0x05, 0x0C,
    0x09, 0x01,
    0xA1, 0x01,

    0x85, 0x01,

    0x09, 0xE9,
    0x15, 0x00,
    0x25, 0x01,
    0x75, 0x01,
    0x95, 0x01,
    0x81, 0x02,

    0x09, 0xEA,
    0x15, 0x00,
    0x25, 0x01,
    0x75, 0x01,
    0x95, 0x01,
    0x81, 0x02,

    0x95, 0x06,
    0x81, 0x03,

    0xC0
};

// ==================== 服务器回调 ====================
class ServerCallbacks : public NimBLEServerCallbacks {
    void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override {
        isConnected = true;
        isAdvertising = false;

        digitalWrite(MOS_PIN, HIGH);
        digitalWrite(LED_PIN, LOW);   // 连接后 LED 常亮

        NimBLEDevice::getAdvertising()->stop();
    }

    void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override {
        isConnected = false;

        digitalWrite(MOS_PIN, LOW);
        digitalWrite(LED_PIN, HIGH);  // 断开后 LED 灭

        isAdvertising = false;
    }
};

// ==================== 初始化蓝牙 ====================
void initBLE() {
    NimBLEDevice::init("EBike");

    NimBLEDevice::setSecurityAuth(true, true, true);
    NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY);
    NimBLEDevice::setSecurityPasskey(STATIC_PASSKEY);
    NimBLEDevice::setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
    NimBLEDevice::setSecurityRespKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);

    NimBLEServer* server = NimBLEDevice::createServer();
    server->setCallbacks(new ServerCallbacks());

    hid = new NimBLEHIDDevice(server);
    hid->setManufacturer("DIY");
    hid->setPnp(0x02, 0xE502, 0xA111, 0x0210);
    hid->setHidInfo(0x00, 0x01);
    hid->setReportMap((uint8_t*)kReportMap, sizeof(kReportMap));
    hid->startServices();

    inputReport = hid->getInputReport(1);
}

// ==================== 开始广播 ====================
void startAdvertising() {
    if (isAdvertising || isConnected) return;

    NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
    adv->setAppearance(HID_KEYBOARD);
    adv->addServiceUUID(hid->getHidService()->getUUID());
    adv->setName("biu-EBike");

    adv->start();
    isAdvertising = true;

    // 重置闪烁状态
    ledBlinkState = false;
    lastBlinkTime = millis();
}

// ==================== 停止广播 ====================
void stopAdvertising() {
    if (!isAdvertising) return;

    NimBLEDevice::getAdvertising()->stop();
    isAdvertising = false;

    // 停止广播后 LED 灭
    digitalWrite(LED_PIN, HIGH);
    ledBlinkState = false;
}

// ==================== 按钮处理 ====================
void handleButton() {
    uint8_t currentState = digitalRead(BUTTON_PIN);

    if (currentState == LOW && lastButtonState == HIGH) {
        delay(50);
        if (digitalRead(BUTTON_PIN) == LOW) {
            if (!isConnected && !isAdvertising) {
                startAdvertising();
            } else if (isAdvertising) {
                stopAdvertising();
            }
        }
    }
    lastButtonState = currentState;
}

// ==================== LED 状态更新 ====================
void updateLED() {
    if (isConnected) {
        // 已连接:常亮
        digitalWrite(LED_PIN, LOW);
    } else if (isAdvertising) {
        // 广播中:闪烁
        unsigned long now = millis();
        if (now - lastBlinkTime >= LED_BLINK_INTERVAL) {
            lastBlinkTime = now;
            ledBlinkState = !ledBlinkState;
            digitalWrite(LED_PIN, ledBlinkState ? LOW : HIGH);
        }
    } else {
        // 空闲:灭
        digitalWrite(LED_PIN, HIGH);
        ledBlinkState = false;
    }
}

// ==================== 主循环中的蓝牙状态检查 ====================
void checkBLEStatus() {
    if (isAdvertising && isConnected) {
        stopAdvertising();
    }
}

// ==================== setup ====================
void setup() {
    Serial.begin(115200);
    delay(500);

    pinMode(BUTTON_PIN, INPUT_PULLUP);
    pinMode(MOS_PIN, OUTPUT);
    pinMode(LED_PIN, OUTPUT);

    digitalWrite(MOS_PIN, LOW);
    digitalWrite(LED_PIN, HIGH);

    initBLE();
}

// ==================== loop ====================
void loop() {
    handleButton();
    checkBLEStatus();
    updateLED();

    if (isConnected) {
        digitalWrite(MOS_PIN, HIGH);
    }

    delay(10);
}