【人脸+手势AI识别模组】100元自己做一个数码相机

    科创经济 朗峰江湖 2025-09-27 4917 次浏览

    以下作品由安信可社区用户

    dzy7455339制作

    原贴地址

    电子DIY作品】BW21数码相机+BW21-CBV-KIT

    一直想自己DIY一个相机,但是奈何笔者个人水平有限,虽然有各种强大的芯片,但是自己用不了,后来有了ESP32但是拍摄出的画面质量不是很满意,所以这个想法一直搁置。

    看到安信可新出的BW21-CBV-Kit支持摄像头、1080p录像和SD卡,最关键的是它还支持Arduino编程,让笔者做相机的想法得以快速实现。

    wKgZPGjJGmiAHH8PAABaUH_KpSw39.webp

    1硬件准备

    考虑到个人硬件水平有限,这里直接使用了BW21-CBV-Kit开发板作为核心。围绕核心功能相机,需要准备的外部设备还有电源、屏幕、闪光灯、计时器、按键这些功能。

    另外板子虽然板载了一个模拟麦克风,但是实际使用起来比较差强人意,所以数字麦克风也加入了外设的清单里。

    以BW21-CBV-Kit为基础做了2个扩展版:

    第一个扩展版主要承载屏幕、按键以及BW21-CBV-Kit,

    第二个扩展版则集成了充电、RTC、闪光灯以及数字麦克风。

    因为不会使用3D软件,这里直接在扩展版的基础上利用立创EDA的制作外壳功能画了一个简单的外壳。

    wKgZPGjJGmmAepjsAABEDErQ3RA93.webp

    先对开发板进行了单项基础功能测试,发现2个问题:

    一是选择引脚的时候没有避开SWD引脚,导致I2C通讯失败。

    二是板子和外壳留孔对不上,没办法只能重新来过。

    幸好第二次板子功能正常,和外壳也很搭配。

    2软件

    其实开发板在Arduino中准备了很多使用的例子进行使用,需要做的就是把这些组合起来。

    这里用到的核心例程主要有Camera_2_LCD, SingleVideoWithAudio以及SDCARDsaveJPG几个示例。

    第一个示例实现了摄像头画面到屏幕

    第二个示例实现了录制MP4视频到SD卡

    第三个示例实现了拍摄图像到SD卡

    这是相机的3个核心任务,在arduino中使用RTOS建立了3个程序,并用相应的按键来控制这3个任务的启动或者停止。

    wKgZPGjJGmmAZjFvAABTNuktL1o59.webp

    核心功能之外,相机还需要一些简单的设置和显示功能。比如设置时间、屏幕亮度、闪光灯开关、浏览相片、蓝牙遥控等。

    这里使用裸机直接写了一个简单的目录界面,使用按键进行控制,在界面中可以进行相片浏览、屏幕亮度调整以及蓝牙遥控的开关等功能。

    wKgZO2jJGmqAAE4wAAA-xq6zE0A12.webp

    蓝牙控制使用了一个Ai-M61-32S开发板来充当蓝牙遥控,当然手机搜索对应的广播名称并发送指令Snapshot也是能够控制的。

    BW21-CBV-Kit充当主机设备扫描并连接特定名称设备,Ai-M61-32S作为从机进行广播。

    wKgZPGjJGmqAN01MAAA74D1txFQ19.webp

    板子被封在壳子里不能像常规相机一样直接拿取SD卡拷贝照片和视频,参考官方例程实现了通过USB来读取照片和视频,功能的打开通过按键实现。

    wKgZO2jJGmuAWzJIAAA7nOe75f026.webp

    电源使用的充放电一体芯片,但是芯片充电的时候不能关机,会导致充电时相机还处于运行状态。

    这里使用ADC检测电压,如果检测到电压高于4.2V就进入睡眠模式来实现假关机。

    wKgZPGjJGmuAf6REAAArAk24Lvo21.webp

    BW21-CBV-Kit开发板的代码

    #include "StreamIO.h"
    #include "VideoStream.h"
    #include "AudioStream.h"
    #include "AudioEncoder.h"
    #include "MP4Recording.h"
    #include "AmebaFatFS.h"
    #include "AmebaST7789.h"
    #include "TJpg_Decoder.h"
    #include "USBMassStorage.h"  //USB存储
    #include "sys_api.h"         //系统调用
    #include "BLEDevice.h"
    #include "PowerMode.h"
    // wake up by AON timer :   0
    // wake up by AON GPIO  :   1
    // wake up by eRtc       :   2
    #define WAKEUP_SOURCE 1
    #define RETENTION     0
    // set wake up AON GPIO pin :   21 / 22
    #define WAKUPE_SETTING 21
    //BLE相关
    #define UART_SERVICE_UUID      "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
    #define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
    #define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
    #define TARGET_DEVICE_NAME "Ble_cam_control"
    #define STRING_BUF_SIZE 100
    BLEAdvertData foundDevice;
    BLEAdvertData targetDevice;
    BLEClient* client;
    BLERemoteService* UartService;
    BLERemoteCharacteristic* Rx;
    BLERemoteCharacteristic* Tx;
    TaskHandle_t xBLETaskHandle = NULL;  // 按键任务全局句柄,初始为 NULL
    int8_t g_connID = -1; // 存储连接ID
    bool g_bleReady = false; // 标志位,表示BLE已准备就绪
    bool g_deviceFound = false; // < << 新增:标志位,表示目标设备已被发现
    bool enableBLE = false;//开启蓝牙控制
    bool BLETaskState = false;//BLE任务是否启动
    //文件浏览
    const char *PHOTO_FOLDER = "photos";  // 修改为您想浏览的文件夹名
    const char *VIDEO_FOLDER = "videos";  // 修改为您想浏览的文件夹名
    #define MAX_IMAGES 50
    char imageList[MAX_IMAGES][32];  // 存储文件名
    int imageCount = 0;
    int currentImageIndex = 0;
    uint8_t currentScale = 1;
    uint16_t currentJpgWidth = 0;   // 原始图片宽度
    uint16_t currentJpgHeight = 0;  // 原始图片高度
    uint8_t LED_BRIGHTNESS = 250;
    uint8_t TFT_BRIGHTNESS = 250;
    int16_t reviewX = 0;  //缩放时进行偏移
    int16_t reviewY = 0;  //缩放时进行偏移
    bool LEDON = false;  //开关LED
    /*USB存储*/
    USBMassStorage USBMS;
    bool usbModeFlag = false;
    bool usbStart = false;
    #include "PCF8563.h"
    /* eRtc相关定义*/
    #define PIN_STORAGE 1
    #define PIN_BUTTON_UP 27
    #define PIN_BUTTON_DOWN 19
    #define PIN_BUTTON_SELECT 20
    #define BTN_PREV 17  // 上一张
    #define BTN_NEXT 28  // 下一张
    // 当前设置状态枚举
    enum {
      SET_YEAR,
      SET_MONTH,
      SET_DAY,
      SET_HOUR,
      SET_MINUTE,
      SET_SECOND,
      SET_DONE
    };
    bool setMenuFlag = false;    //避免屏幕占用
    int8_t setTimeState = -1;    // -1 = 未进入设置,0~5 = 正在设置某项
    #define MAX_JPG_SIZE 655360  // 128KB 图像缓冲区
    static uint8_t jpgBuffer[MAX_JPG_SIZE];
    PCF8563 eRtc(&Wire1);//外部时钟
    /* eRtc相关定义*/
    /* TFT相关定义*/
    #define TFT_DC 8  //A0
    #define TFT_RST -1
    #define TFT_CS SPI_SS
    #define BL_PIN 7
    #define FLASH_PIN 6               //闪光灯引脚
    #define PIN_VOLTAGE 11            //电压引脚
    float vBatRate = 2 * 3.3 / 1020;  //电压换算
    #define VOLTAGE_BASE 3.2
    AmebaST7789 tft = AmebaST7789(TFT_CS, TFT_DC, TFT_RST, 240, 320);
    /* TFT相关定义*/
    /* FLASH相关定义*/
    #include < FlashMemory.h >             //flash
    unsigned int photoCount = 0;         //照片编号
    #define PHOTO_COUNTER_OFFSET 0x1E00  // Flash 偏移地址,用于存储照片计数
    #define MAX_PHOTO_COUNT 10000        // 防止溢出或异常值(可选)
    #define FILENAME "photo"
    /* eRtc相关定义*/
    uint32_t rec_addr = 0;
    uint32_t rec_len = 0;
    uint32_t img_addr = 0;
    uint32_t img_len = 0;
    bool current_buffer = false;
    AmebaFatFS fs;
    #define CHANNEL_SCREEN 0
    #define CHANNEL_RECORD 1
    #define REC_BTN 0   //录像按钮
    #define SNAP_BTN 4  //模式切换按钮
    CameraSetting configCam;
    // Default preset configurations for each video channel:
    // Channel 0 : 1920 x 1080 30FPS H264
    // Channel 1 : 1280 x 720  30FPS H264
    // Default audio preset configurations:
    // 0 :  8kHz Mono Analog Mic
    // 1 : 16kHz Mono Analog Mic
    // 2 :  8kHz Mono Digital PDM Mic
    // 3 : 16kHz Mono Digital PDM Mic
    bool snapAnamiton = false;            //拍照动画通知
    SemaphoreHandle_t xBinarySemaphore;   //等待信号拍照
    SemaphoreHandle_t xBinarySemaphore1;  //等待信号开始录像
    VideoSetting config1(240, 304, 30, VIDEO_JPEG, 1);
    VideoSetting config3(VIDEO_FHD, CAM_FPS, VIDEO_H264_JPEG, 1);
    //VideoSetting configV(CHANNEL);
    AudioSetting configA(3);
    Audio audio;
    AAC aac;
    MP4Recording mp4;
    StreamIO audioStreamer(1, 1);  // 1 Input Audio -> 1 Output AAC
    StreamIO avMixStreamer(2, 1);  // 2 Input Video + Audio -> 1 Output MP4
    bool isRecording = false;  //是否在录像
    TaskHandle_t displayTaskHandle = NULL;
    bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) {
      if (y > 240) {
        return 0;
      }
      tft.drawBitmap(x, y, w, h, bitmap);
      return 1;
    }
    void setup() {
      Serial.begin(115200);
      xBinarySemaphore = xSemaphoreCreateBinary();
      xBinarySemaphore1 = xSemaphoreCreateBinary();
      if (xBinarySemaphore1 == NULL || xBinarySemaphore == NULL) {
        Serial.println(" 信号量创建失败!");
        while (1)
          ;  // 停机
      }
      // pinMode(FLASH_PIN,OUTPUT);
      // digitalWrite(FLASH_PIN,LOW);
      analogWrite(FLASH_PIN, 0);
      if (!fs.begin()) {
        Serial.println(" SD卡初始化失败!");
        while (1)
          ;
      }
      createDirIfNotExists(PHOTO_FOLDER);
      createDirIfNotExists(VIDEO_FOLDER);
      TJpgDec.setSwapBytes(true);
      TJpgDec.setJpgScale(currentScale);
      TJpgDec.setCallback(tft_output);
      Wire1.begin();
      rtc.begin();
      rtc.printTime(Serial);
      rtc.printTime(Serial);
      setCamera();
      tft.begin();
      tft.setRotation(1);
      tft.fillScreen(ST7789_BLACK);
      tft.flush();
      analogWrite(BL_PIN, TFT_BRIGHTNESS);
      // Configure camera video channel with video format information
      xTaskCreate(recordVideo, "record Video", 4096, NULL, 1, NULL);
      xTaskCreate(snapShot, "take photo", 4096, NULL, 1, NULL);
      xTaskCreate(displayTask, "Display Task", 4096, NULL, 1, &displayTaskHandle);
      setupButtons();
    }
    void loop() {  //进入目录
      if (digitalRead(PIN_BUTTON_SELECT) == HIGH) {
        vTaskDelay(pdMS_TO_TICKS(1000));  // 长按 1 秒进入设置
        if (digitalRead(PIN_BUTTON_SELECT) == HIGH && !setMenuFlag) {
          setMenuFlag = true;
          navigateMainMenu();  // 进入设置
        }
      }
      if (buttonPressed(SNAP_BTN) && !setMenuFlag) {
        xSemaphoreGive(xBinarySemaphore);
      }
      if (buttonPressed(REC_BTN) && !setMenuFlag) {
        xSemaphoreGive(xBinarySemaphore1);
      }
      //进入USB
      if (buttonPressed(PIN_STORAGE)) {
        usbModeFlag = !usbModeFlag;
      }
      if (usbModeFlag && !usbStart) {
        vTaskSuspend(displayTaskHandle);
        tft.setFontColor(ST7789_WHITE);
        tft.setFontSize(2);
        tft.fillScreen(ST7789_BLACK);
        tft.setCursor(100, 100);
        tft.print("USB MODE");
        tft.flush();
        fs.end();
        USBMS.USBInit();
        USBMS.SDIOInit();
        USBMS.USBStatus();
        USBMS.initializeDisk();
        USBMS.loadUSBMassStorageDriver();
        usbStart = true;
      }
      if (usbStart && !usbModeFlag) {
        sys_reset();  //结束USB系统重启
      }
      vTaskDelay(pdMS_TO_TICKS(100));
    }
    void createDirIfNotExists(const char *dirname) {
      char path[128];
      sprintf(path, "%s%s", fs.getRootPath(), dirname);
      if (!fs.exists(path)) {
        if (fs.mkdir(path)) {
          printf("创建文件夹: "%s"rn", path);
        } else {
          printf("创建文件夹失败: "%s"rn", path);
        }
      } else {
        printf("文件夹已存在: "%s"rn", path);
      }
    }
    void recordVideo(void *pvParameters) {
      // Print information
      //printInfo();
      bool modifyTime = false;
      uint8_t y, mo, d, h, mi, se, wd;
      char filename[64] = { 0 };  // 静态保存上次的文件名
      while (1) {
        if (xSemaphoreTake(xBinarySemaphore1, pdMS_TO_TICKS(1000)) == pdTRUE) {
          if (!isRecording) {
            modifyTime = true;
            isRecording = true;
            rtc.getTime(&y, &mo, &d, &h, &mi, &se, &wd);
            sprintf(filename, "%s/%04d%02d%02d_%02d%02d%02d", VIDEO_FOLDER, y, mo, d, h, mi, se);
            mp4.setRecordingFileName(filename);
            Serial.println("Recording STARTED");
            mp4.begin();
          } else {
            isRecording = false;
            if (mp4.getRecordingState() == 1) {
              mp4.end();
            }
          }
        }
        if (modifyTime && !isRecording && filename[0] != '') {
          if (mp4.getRecordingState() == 0) {
            Serial.print("修改时间");
            char pathBuf[128];
            const char *root = fs.getRootPath();  // 通常是 "/" 或 ""
            snprintf(pathBuf, sizeof(pathBuf), "%s%s.mp4", root, filename);
            Serial.print(fs.setLastModTime(pathBuf, 2000 + y, mo, d, h, mi, se));
            modifyTime = false;
          }
        }
        vTaskDelay(pdMS_TO_TICKS(100));
      }
    }
    void printInfo(void) {
      Serial.println("------------------------------");
      Serial.println("- Summary of Streaming -");
      Serial.println("------------------------------");
      Camera.printInfo();
      Serial.println("- Audio Information -");
      audio.printInfo();
      Serial.println("- MP4 Recording Information -");
      mp4.printInfo();
    }
    void snapShot(void *pvParameters) {
      FlashMemory.begin(FLASH_MEMORY_APP_BASE, 0x1000);
      photoCount = FlashMemory.readWord(PHOTO_COUNTER_OFFSET);
      Serial.print("读取到已拍摄照片数量: ");
      Serial.println(photoCount);
      while (1) {
        if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) {  //等待拍照通知 xSemaphoreGive(xBinarySemaphore);
          //fs.begin();
          char path[128];
          int n = snprintf(path, sizeof(path), "%s%s/%s%d.jpg",
                           fs.getRootPath(),
                           PHOTO_FOLDER,
                           FILENAME,
                           photoCount);
          if (n < 0) {
            Serial.println("照片路径生成失败!");
          }
          File file = fs.open(path);
          vTaskDelay(pdMS_TO_TICKS(100));
          uint8_t y, mo, d, h, mi, se, wd;
          rtc.getTime(&y, &mo, &d, &h, &mi, &se, &wd);
          if (LEDON) {
            analogWrite(FLASH_PIN, LED_BRIGHTNESS);
          }
          snapAnamiton = true;
          Camera.getImage(CHANNEL_RECORD, &rec_addr, &rec_len);
          //vTaskSuspend(displayTaskHandle);
          if (LEDON) {
            analogWrite(FLASH_PIN, 0);
          }
          file.write((uint8_t *)rec_addr, rec_len);
          file.close();
          fs.setLastModTime(path, 2000 + y, mo, d, h, mi, se);
          photoCount++;
          FlashMemory.writeWord(PHOTO_COUNTER_OFFSET, photoCount);
          unsigned int checkValue = FlashMemory.readWord(PHOTO_COUNTER_OFFSET);
          if (checkValue == photoCount) {
            Serial.print(" 照片编号已更新: ");
            Serial.println(photoCount);
          } else {
            Serial.println(" Flash 写入失败!");
          }
          //vTaskResume(displayTaskHandle);
        } else {
          vTaskDelay(pdMS_TO_TICKS(100));
        }
      }
    }
    void displayTask(void *pvParameters) {
      unsigned long previousMillis = 0;  // 上次刷新时间
      int frameCount = 0;                // 帧计数器
      float fps = 0.0f;                  // 存储 FPS
      unsigned long lastVolMeTime = 0;
      int lastVoltagePercent = 0;
      lastVoltagePercent = batteryVoltConvert();  // 先读一次
      while (1) {
        if (!setMenuFlag) {
          unsigned long currentMillis = millis();  // 获取当前时间
          Camera.getImage(CHANNEL_SCREEN, &img_addr, &img_len);
          bool next_buffer = !current_buffer;
          tft.setFrontBufferIndex(next_buffer);
          // uint16_t jpgWidth, jpgHeight;
          // // 仅获取 JPEG 图像尺寸(不显示)
          // bool inform = TJpgDec.getJpgSize(&jpgWidth, &jpgHeight, (const uint8_t*)img_addr, img_len);
          // if (!inform) {
          //     printf("JPEG Size: %dx%dn", jpgWidth, jpgHeight);
          // } else {
          //     Serial.println("Failed to parse JPEG header");
          // }
          tft.fillScreen(ST7789_BLACK);
          TJpgDec.drawJpg(0, 0, (uint8_t *)img_addr, img_len);
          // 更新帧计数器
          frameCount++;
          if (currentMillis - previousMillis >= 1000) {
            fps = frameCount * 1000.0f / (currentMillis - previousMillis);  // 计算每秒帧数
            frameCount = 0;                                                 // 重置帧计数器
            previousMillis = currentMillis;                                 // 更新上次刷新时间
          }
          char fpsStr[32];
          sprintf(fpsStr, "FPS: %.1f", fps);  // 格式化 FPS 显示
          tft.setCursor(5, 5);
          tft.drawString(fpsStr);  // 在屏幕中央上方显示 FPS
          //Serial.println(fpsStr);
          if (isRecording) {
            static int reverseTime = 0;
            if (reverseTime < 30) {
              tft.fillCircle(20, 120, 15, ST7789_GREEN);
            }
            reverseTime++;
            if (reverseTime >= 60) {
              reverseTime = 0;
            }
          }
          if (millis() - lastVolMeTime > 30000) {
            lastVoltagePercent = batteryVoltConvert();
            lastVolMeTime = millis();
          }
          drawBattery(280, 5, 20, 10, lastVoltagePercent);
          drawLightningBolt(110, 2, 5, LEDON);
          if (snapAnamiton) {
            snapAnamiton = false;
            tft.drawRect(0, 0, 320, 240, ST7789_WHITE, 10);
          }
          if(!g_bleReady){                                        //连接未连接状态切换
            drawBluetoothSymbol(180, 8, 10,ST7789_BLACK,enableBLE); 
          }else{
            drawBluetoothSymbol(180, 8, 10,ST7789_WHITE,enableBLE);  
          }
    
          tft.flush();
          current_buffer = next_buffer;
        } else {
          vTaskDelay(pdMS_TO_TICKS(100));
        }
        vTaskDelay(pdMS_TO_TICKS(1));
      }
    }
    bool buttonPressed(int pin) {
      if (digitalRead(pin) == HIGH) {
        vTaskDelay(pdMS_TO_TICKS(20));  // 简单消抖
        if (digitalRead(pin) == HIGH) {
          while (digitalRead(pin) == HIGH) {
            vTaskDelay(pdMS_TO_TICKS(10));
          }
          return true;
        }
      }
      return false;
    }
    /*RTC相关函数*/
    void setupButtons() {
      pinMode(PIN_BUTTON_UP, INPUT_PULLDOWN);
      pinMode(PIN_BUTTON_DOWN, INPUT_PULLDOWN);
      pinMode(PIN_BUTTON_SELECT, INPUT_PULLDOWN);
      pinMode(REC_BTN, INPUT_PULLDOWN);
      pinMode(SNAP_BTN, INPUT_PULLDOWN);
      pinMode(PIN_STORAGE, INPUT_PULLDOWN);
      pinMode(BTN_PREV, INPUT_PULLDOWN);
      pinMode(BTN_NEXT, INPUT_PULLDOWN);
    }
    void displayTimeSetting(uint8_t y, uint8_t mo, uint8_t d, uint8_t h, uint8_t mi, uint8_t se, int8_t highlight) {
      tft.fillScreen(ST7789_WHITE);
      tft.setCursor(50, 20);
      tft.setFontColor(ST7789_BLACK);
      tft.setFontSize(2);
      tft.print("Set Time");
      tft.setFontSize(1);
      const char *labels[] = { "Year:", "Month:", "Day:", "Hour:", "Minute:", "Second:" };
      int values[] = { 2000 + y, mo, d, h, mi, se };
      int x_pos = 40, y_pos = 60;
      for (int i = 0; i < 6; i++) {
        tft.setCursor(x_pos, y_pos + i * 20);
        tft.setFontColor(i == highlight ? ST7789_RED : ST7789_BLACK);
        tft.print(labels[i]);
        tft.print(" ");
        tft.print(values[i]);
      }
      tft.setCursor(40, y_pos + 6 * 20);
      tft.setFontColor(ST7789_BLUE);
      tft.print("Press SELECT to save");
      tft.flush();
    }
    // 菜单项定义
    enum MainMenu {
      MENU_PHOTO_BROWSER,
      MENU_BRIGHTNESS_SCREEN,
      MENU_BRIGHTNESS_LED,
      MENU_SET_TIME,
      MENU_EXIT,
      MENU_COUNT
    };
    // 当前选中的主菜单项
    int8_t currentMenuIndex = 0;
    // ==================== 显示主菜单 ====================
    void displayMainMenu() {
      tft.fillScreen(ST7789_WHITE);
      tft.setCursor(50, 20);
      tft.setFontColor(ST7789_BLACK);
      tft.setFontSize(2);
      tft.print("Main Menu");
      tft.setFontSize(1);
      const char *menuItems[] = {
        "Photo Browser",
        "Screen Brightness",
        "LED Brightness",
        "Set Time",
        "Exit Menu"
      };
      int x_pos = 40, y_pos = 60;
      for (int i = 0; i < MENU_COUNT; i++) {
        tft.setCursor(x_pos, y_pos + i * 25);
        tft.setFontColor(i == currentMenuIndex ? ST7789_RED : ST7789_BLACK);
        tft.print(" > ");
        tft.print(menuItems[i]);
      }
      tft.setCursor(40, y_pos + MENU_COUNT * 25);
      tft.setFontColor(ST7789_BLUE);
      tft.print("SELECT to enter");
      tft.flush();
    }
    // ==================== 主菜单导航与执行 ====================
    void navigateMainMenu() {
      buttonPressed(PIN_BUTTON_SELECT);
      currentMenuIndex = 0;  // 默认选中第一项
      while (true) {
        displayMainMenu();
        if (buttonPressed(PIN_BUTTON_UP)) {
          currentMenuIndex = (currentMenuIndex - 1 + MENU_COUNT) % MENU_COUNT;
        }
        if (buttonPressed(PIN_BUTTON_DOWN)) {
          currentMenuIndex = (currentMenuIndex + 1) % MENU_COUNT;
        }
        if (buttonPressed(PIN_BUTTON_SELECT)) {
          // 根据选择进入第二级操作
          switch (currentMenuIndex) {
            case MENU_PHOTO_BROWSER:
              enterPhotoBrowser();  // 你需要实现这个函数
              break;
            case MENU_BRIGHTNESS_SCREEN:
              adjustScreenBrightness();  // 下面提供示例
              break;
            case MENU_BRIGHTNESS_LED:
              adjustLEDBrightness();  // 下面提供示例
              break;
            case MENU_SET_TIME:
              setTimeWithButtons();  // 你已有的函数
              break;
            case MENU_EXIT:
              tft.setFontColor(ST7789_WHITE);
              tft.setFontSize(1);
              setMenuFlag = false;  // 新增:退出菜单
              return;               // 退出 navigateMainMenu 函数,回到主循环
          }
        }
        vTaskDelay(pdMS_TO_TICKS(100));
      }
    }
    void adjustScreenBrightness() {
      uint8_t brightness = getCurrentBrightness();  // 你需要实现:读取当前亮度值(0-255)
      bool exiting = false;
      while (!exiting) {
        tft.fillScreen(ST7789_WHITE);
        tft.setCursor(50, 100);
        tft.setFontColor(ST7789_BLACK);
        tft.setFontSize(2);
        tft.print("Screen Brightness:");
        tft.setCursor(100, 140);
        tft.print(brightness);
        tft.setCursor(40, 180);
        tft.setFontColor(ST7789_BLUE);
        tft.print("UP/DOWN: Adjust");
        tft.setCursor(40, 200);
        tft.print("SELECT: Back");
        tft.flush();
        if (buttonPressed(PIN_BUTTON_UP)) {
          brightness = min(255, brightness + 5);
          setScreenBrightness(brightness);  // 你需要实现这个函数(如通过PWM)
          while (buttonPressed(PIN_BUTTON_UP))
            ;
        }
        if (buttonPressed(PIN_BUTTON_DOWN)) {
          brightness = max(0, brightness - 5);
          setScreenBrightness(brightness);
          while (buttonPressed(PIN_BUTTON_DOWN))
            ;
        }
        if (buttonPressed(PIN_BUTTON_SELECT)) {
          while (buttonPressed(PIN_BUTTON_SELECT))
            ;
          exiting = true;
        }
        vTaskDelay(pdMS_TO_TICKS(50));
      }
    }
    void adjustLEDBrightness() {
      uint8_t ledBrightness = getCurrentLEDBrightness();  // 读取当前LED亮度
      bool exiting = false;
      uint8_t count = 0;
      while (!exiting) {
        tft.fillScreen(ST7789_WHITE);
        tft.setCursor(50, 100);
        if (count == 0) {
          tft.setFontColor(ST7789_GREEN);
        } else if (count == 1) {
          tft.setFontColor(ST7789_BLACK);
        }
        tft.setFontSize(2);
        tft.print("LED Brightness state:");
        tft.setCursor(100, 140);
        tft.print(ledBrightness);
        tft.setCursor(40, 180);
        tft.setFontColor(ST7789_BLUE);
        tft.print("UP/DOWN: Adjust");
        tft.setCursor(40, 200);
        tft.print("SELECT: Back");
        tft.setCursor(150, 140);
        if (count == 0) {
          tft.setFontColor(ST7789_BLACK);
        } else if (count == 1) {
          tft.setFontColor(ST7789_GREEN);
        }
        if (LEDON) {
          tft.print("ON");
        } else {
          tft.print("OFF");
        }
        tft.flush();
        if (count == 0) {
          if (buttonPressed(PIN_BUTTON_UP)) {
            ledBrightness = min(255, ledBrightness + 5);
            setLEDBrightness(ledBrightness);  // 你需要实现:控制LED(如PWM)
          }
          if (buttonPressed(PIN_BUTTON_DOWN)) {
            ledBrightness = max(0, ledBrightness - 5);
            setLEDBrightness(ledBrightness);
          }
        }
        if (count == 1) {
          if (buttonPressed(PIN_BUTTON_UP)) {
            LEDON = !LEDON;
          }
          if (buttonPressed(PIN_BUTTON_DOWN)) {
            LEDON = !LEDON;
          }
        }
        if (count >= 2) {
          exiting = true;
        }
        if (buttonPressed(PIN_BUTTON_SELECT)) {
          count++;
        }
        vTaskDelay(pdMS_TO_TICKS(50));
      }
    }
    void enterPhotoBrowser() {
      tft.setFontSize(1);
      tft.setFontColor(ST7789_WHITE);
      tft.fillScreen(ST7789_BLACK);
      tft.flush();
      setScale();
      if (!listJpgFiles(PHOTO_FOLDER)) {
        tft.setCursor(50, 50);
        tft.print("No JPG files found!");
        vTaskDelay(pdMS_TO_TICKS(1000));
        return;
      }
      Serial.print("Found");
      Serial.print(imageCount);
      Serial.println("JPG files.");
      if (imageCount > 0) {
        loadAndDisplayJpg(imageList[currentImageIndex]);
      } else {
        vTaskDelay(pdMS_TO_TICKS(1000));
        return;
      }
      bool selectWasPressed = false;
      unsigned long selectPressStartime = 0;
      const uint16_t LONG_PRESS_THRESHOLD = 1000;
      while (1) {
        bool selectIsPressed = digitalRead(PIN_BUTTON_SELECT) == HIGH;
        if (selectIsPressed && !selectWasPressed) {
          selectWasPressed = true;
          selectPressStartime = millis();
        }
        if (!selectIsPressed && selectWasPressed) {
          selectWasPressed = false;
          unsigned long pressDuration = millis() - selectPressStartime;
        }
        if (selectIsPressed && selectWasPressed) {
          if (millis() - selectPressStartime >= LONG_PRESS_THRESHOLD) {
            buttonPressed(PIN_BUTTON_SELECT);
            resetPictureView();
            resetScale();
            Serial.println("exit photo review");
            return;
          }
        }
          if (buttonPressed(BTN_PREV)) {
            prevImage();
          }
          if (buttonPressed(BTN_NEXT)) {
            nextImage();
          }
          if (buttonPressed(PIN_BUTTON_DOWN)) {
            decreaseScale();
          }
          if (buttonPressed(PIN_BUTTON_UP)) {
            increaseScale();
          }
          bool recButtonIsPressed = digitalRead(REC_BTN) == HIGH;  // 假设按钮连接到GND
          static unsigned long recPressStartTime = 0;
          if (recButtonIsPressed) {
            if (recPressStartTime == 0) {
              recPressStartTime = millis();  // 记录按下开始时间
            } else if (millis() - recPressStartTime >= 200) {
              // 长按超过阈值,执行删除
              deleteCurrentImage();
              recPressStartTime = 0;  // 重置计时器
              continue;               // 跳过本次循环剩余逻辑
            }
          } else {
            recPressStartTime = 0;  // 如果按键释放,重置计时器
          }
        vTaskDelay(pdMS_TO_TICKS(100));
      }
      // 等待用户按 SELECT 返回
    }
    void setLEDBrightness(uint8_t ledBrightness) {
      LED_BRIGHTNESS = ledBrightness;
    }
    uint8_t getCurrentLEDBrightness() {
      return LED_BRIGHTNESS;
    }
    void setScreenBrightness(uint8_t brightness) {
      TFT_BRIGHTNESS = brightness;
      analogWrite(BL_PIN, TFT_BRIGHTNESS);
    }
    uint8_t getCurrentBrightness() {
      return TFT_BRIGHTNESS;
    }
    void setTimeWithButtons() {
      uint8_t y, mo, d, h, mi, se, wd;
      rtc.getTime(&y, &mo, &d, &h, &mi, &se, &wd);
      setTimeState = SET_YEAR;  // 从年份开始
      while (setTimeState < SET_DONE) {
        displayTimeSetting(y, mo, d, h, mi, se, setTimeState);
        if (buttonPressed(PIN_BUTTON_UP)) {
          switch (setTimeState) {
            case SET_YEAR: y = (y + 1) % 100; break;
            case SET_MONTH: mo = (mo % 12) + 1; break;
            case SET_DAY: d = (d % 31) + 1; break;
            case SET_HOUR: h = (h + 1) % 24; break;
            case SET_MINUTE: mi = (mi + 1) % 60; break;
            case SET_SECOND: se = (se + 1) % 60; break;
          }
        }
        if (buttonPressed(PIN_BUTTON_DOWN)) {
          switch (setTimeState) {
            case SET_YEAR: y = (y + 99) % 100; break;  // -1
            case SET_MONTH: mo = ((mo + 10) % 12) + 1; break;
            case SET_DAY: d = ((d + 29) % 31) + 1; break;
            case SET_HOUR: h = (h + 23) % 24; break;
            case SET_MINUTE: mi = (mi + 59) % 60; break;
            case SET_SECOND: se = (se + 59) % 60; break;
          }
        }
        if (buttonPressed(PIN_BUTTON_SELECT)) {
          setTimeState++;  // 进入下一项
          if (setTimeState == SET_DONE) {
            break;  // 结束
          }
        }
        vTaskDelay(pdMS_TO_TICKS(100));  // 防止太快
      }
      rtc.setTimeAutoWeekday(2000 + y, mo, d, h, mi, se);  // 自动计算星期
      // 显示成功
      tft.fillScreen(ST7789_BLACK);
      tft.flush();
      tft.setCursor(50, 100);
      tft.setFontColor(ST7789_WHITE);
      tft.setFontSize(2);
      tft.print("Time Set!");
      tft.flush();
      vTaskDelay(pdMS_TO_TICKS(1000));
    }
    // 扫描 JPG 文件
    bool listJpgFiles(const char *path) {
      char result_buf[1024];  // 必须足够大
      char fullPath[128];
      char *p;
      sprintf(fullPath, "%s%s", fs.getRootPath(), path);
      int count = fs.readDir(fullPath, result_buf, sizeof(result_buf));
      if (count != 0) {
        Serial.println("Failed to read directory or it's empty");
        return false;
      }
      imageCount = 0;
      p = result_buf;
      while (strlen(p) > 0) {
        String filename = String(p);
        // 判断是否为 JPG 文件
        if (filename.endsWith(".jpg") || filename.endsWith(".JPG") || filename.endsWith(".jpeg") || filename.endsWith(".JPEG")) {
          // 复制到 imageList,注意不要超出缓冲区
          if (imageCount < MAX_IMAGES) {
            strlcpy(imageList[imageCount], p, 32);
            Serial.print("Found: ");
            Serial.println(imageList[imageCount]);
            imageCount++;
          } else {
            Serial.println("Max image limit reached!");
            break;
          }
        }
        p += strlen(p) + 1;  // 移动到下一个文件名
      }
      return (imageCount > 0);
    }
    // 加载并显示 JPG
    bool loadAndDisplayJpg(const char *filename) {
      static char fullPath[128];
      sprintf(fullPath, "%s%s/%s", fs.getRootPath(), PHOTO_FOLDER, filename);
      File file = fs.open(fullPath);
      if (!file) {
        Serial.print("Failed to open ");
        Serial.println(filename);
        return false;
      } else {
        Serial.print("open ");
        Serial.println(filename);
      }
      size_t fileSize = file.size();
      if (fileSize == 0 || fileSize >= MAX_JPG_SIZE) {
        Serial.println("File too large or empty!");
        file.close();
        return false;
      } else {
        Serial.print("fileSize:");
        Serial.println(fileSize);
      }
      size_t bytesRead = file.read(jpgBuffer, fileSize);
      file.close();
      if (bytesRead != fileSize) {
        Serial.println("Read error!");
        return false;
      } else {
        Serial.print("bytesRead:");
        Serial.println(bytesRead);
      }
      // 解码前先获取图片尺寸(不绘制)
      uint16_t width, height;
      bool result = TJpgDec.getJpgSize(&width, &height, jpgBuffer, bytesRead);
      if (result != false) {
        Serial.println("Failed to get JPG size");
        return false;
      }
      currentJpgWidth = width;
      currentJpgHeight = height;
      tft.fillScreen(ST7789_BLACK);
      // 使用 reviewX, reviewY 作为偏移绘制
      TJpgDec.drawJpg(reviewX, reviewY, jpgBuffer, bytesRead);  // 内部会根据 scale 和偏移绘制
      // 显示信息
      char timeStr[64];
      uint16_t y, mo, d, h, mi, se;
      fs.getLastModTime(fullPath, &y, &mo, &d, &h, &mi, &se);
      snprintf(timeStr, sizeof(timeStr), "%02u-%02u-%02u %02u:%02u:%02u", y, mo, d, h, mi, se);
      tft.setCursor(5, 5);
      tft.print(timeStr);
      tft.setCursor(290, 220);
      tft.print(TFTshowScale().c_str());
      tft.flush();
      return true;
    }
    // 切换图片
    void prevImage() {
      if (imageCount == 0) return;
      currentImageIndex = (currentImageIndex - 1 + imageCount) % imageCount;
      loadAndDisplayJpg(imageList[currentImageIndex]);
    }
    void nextImage() {
      if (imageCount == 0) return;
      currentImageIndex = (currentImageIndex + 1) % imageCount;
      loadAndDisplayJpg(imageList[currentImageIndex]);
    }
    void increaseScale() {
      currentScale = 2 * currentScale;
      if (currentScale > 8) {
        currentScale = 8;
      }
      TJpgDec.setJpgScale(currentScale);
      loadAndDisplayJpg(imageList[currentImageIndex]);
    }
    void decreaseScale() {
      currentScale = currentScale / 2;
      if (currentScale < 1) {
        currentScale = 1;
      }
      TJpgDec.setJpgScale(currentScale);
      loadAndDisplayJpg(imageList[currentImageIndex]);
    }
    String TFTshowScale() {
      switch (currentScale) {
        case 1: return "4X";
        case 2: return "2X";
        case 4: return "1X";
        case 8: return "1/2X";
        default: return "?X";  // 必须有 default
      }
    }
    void setCamera() {
      //config3.setRotation(1);//右转90度
      //config3.setJpegQuality(9);
      config1.setRotation(1);  //右转90度
      config1.setJpegQuality(7);
      //config3.setBitrate(50 * 1024 * 1024);//更改录像码率
      Camera.configVideoChannel(CHANNEL_RECORD, config3);
      Camera.configVideoChannel(CHANNEL_SCREEN, config1);
      Camera.videoInit(CHANNEL_RECORD);
      Camera.videoInit(CHANNEL_SCREEN);
      // Configure audio peripheral for audio data output
      audio.configAudio(configA);
      audio.begin();
      // Configure AAC audio encoder
      aac.configAudio(configA);
      aac.begin();
      // Configure MP4 with identical video format information
      // Configure MP4 recording settings
      mp4.configVideo(config3);
      mp4.configAudio(configA, CODEC_AAC);
      mp4.setRecordingDuration(600);
      mp4.setRecordingFileCount(1);
      //mp4.setRecordingFileName("TestRecordingAudioVideo");
      // Configure StreamIO object to stream data from audio channel to AAC encoder
      audioStreamer.registerInput(audio);
      audioStreamer.registerOutput(aac);
      if (audioStreamer.begin() != 0) {
        Serial.println("StreamIO link start failed");
      }
      // Configure StreamIO object to stream data from video channel and AAC encoder to MP4 recording
      avMixStreamer.registerInput1(Camera.getStream(CHANNEL_RECORD));
      avMixStreamer.registerInput2(aac);
      avMixStreamer.registerOutput(mp4);
      if (avMixStreamer.begin() != 0) {
        Serial.println("StreamIO link start failed");
      }
      // Start data stream from video channel
      Camera.channelBegin(CHANNEL_RECORD);
      Camera.channelBegin(CHANNEL_SCREEN);
      configCam.setContrast(45);  //降低对比度
      // Start recording MP4 data to SD card
    }
    void resetPictureView() {
      reviewX = 0;
      reviewY = 0;
    }
    void resetScale() {
      currentScale = 1;
      TJpgDec.setJpgScale(currentScale);
    }
    void setScale() {
      currentScale = 4;
      TJpgDec.setJpgScale(currentScale);
    }
    void deleteCurrentImage() {
      if (imageCount == 0) return;
      // 构建完整路径
      char fullPath[128];
      sprintf(fullPath, "%s/%s", PHOTO_FOLDER, imageList[currentImageIndex]);
      // 显示确认提示(可选)
      tft.fillRectangle(0, 100, 320, 60, ST7789_BLACK);
      tft.setCursor(10, 110);
      tft.print("Delete this photo?");
      tft.setCursor(10, 130);
      tft.print("Hold REC to confirm");
      tft.setCursor(10, 150);
      tft.print("or release to cancel");
      tft.flush();
      // 等待用户确认
      unsigned long start = millis();
      while (millis() - start < 3000) {  // 5秒超时
        vTaskDelay(pdMS_TO_TICKS(2000));
        if (digitalRead(REC_BTN) == HIGH) {  // 假设按钮连接到GND
          // 用户继续长按确认删除
          if (fs.remove(fullPath)) {
            Serial.println("Deleted: ");
            Serial.println(imageList[currentImageIndex]);
            // 从内存列表中移除
            for (int i = currentImageIndex; i < imageCount - 1; i++) {
              strcpy(imageList[i], imageList[i + 1]);
            }
            imageCount--;
            currentImageIndex = min(currentImageIndex, imageCount - 1);  // 更新currentImageIndex
            // 自动加载新图片
            if (imageCount > 0) {
              loadAndDisplayJpg(imageList[currentImageIndex]);
            } else {
              tft.fillScreen(ST7789_BLACK);
              tft.setCursor(50, 50);
              tft.print("No images left.");
              tft.flush();
              vTaskDelay(pdMS_TO_TICKS(1000));
            }
          } else {
            tft.setCursor(10, 170);
            tft.print("Delete failed!");
            tft.flush();
            vTaskDelay(pdMS_TO_TICKS(1000));
            loadAndDisplayJpg(imageList[currentImageIndex]);  // 重新显示当前图
          }
          while (digitalRead(REC_BTN) == HIGH) {
            vTaskDelay(pdMS_TO_TICKS(10));
          }
          return;
        } else {
          // 取消
          loadAndDisplayJpg(imageList[currentImageIndex]);  // 重新显示当前图
          return;
        }
        vTaskDelay(pdMS_TO_TICKS(50));
      }
      // 超时取消
      loadAndDisplayJpg(imageList[currentImageIndex]);
    }
    int batteryVoltConvert() {
      float voltage = vBatRate * analogRead(PIN_VOLTAGE);
      if (voltage < VOLTAGE_BASE) {
        return 0;
      }
      if (voltage > 4.21){
        PowerMode.begin(DEEPSLEEP_MODE, WAKEUP_SOURCE, RETENTION, WAKUPE_SETTING);
        Serial.print("Enter DeepSleep Mode");
        tft.fillScreen(ST7789_BLACK);
        tft.setFontSize(2);
        tft.setCursor(20, 140);
        tft.print("Camera will Enter DeepSleep Mode");
        tft.flush();
        delay(2000);
        PowerMode.start();
      }
      int voltagePercent = (voltage - VOLTAGE_BASE) * 100;
      return voltagePercent;
    }
    void drawBattery(int x, int y, int width, int height, int level) {
      // 绘制电池外框
      uint16_t color;
      if (level < 30) {
        color = ST7789_RED;
      } else {
        color = ST7789_BLUE;
      }
      tft.drawRect(x, y, width + 2, height, ST7789_WHITE);
      tft.fillRectangle(x + width + 2, y + height / 4, 4, height / 2, ST7789_WHITE);  // 电池正极头
      // 根据电量level计算需要填充的线条数量
      int lines = map(level, 0, 100, 0, height / (height / 10));  // 将电量映射到线条数
      for (int i = 0; i < lines; i++) {
        tft.drawFastVLine(x + 2 + i * 2, y + 2, height - 4, color);  // 竖线,留边距
      }
    }
    void drawLightningBolt(int x, int y, int size, bool on) {
        // 定义闪电标志的各个点
         // 定义第一个三角形的顶点坐标(闪电的上部)
        int x0_1 = x;  // 右上角x坐标
        int y0_1 = y;  // 右上角y坐标
        int x1_1 = x0_1-size;  // 左下角x坐标
        int y1_1 = size+y0_1;  // 左下角y坐标
        int x2_1 = x0_1;  // 底部x坐标
        int y2_1 = y1_1;  // 底部y坐标
        // 定义第二个三角形的顶点坐标(闪电的下部)
        int x0_2 = x+1;  // 上部x坐标108
        int y0_2 = y+size+1;  // 上部y坐标13
        int x1_2 = x0_2+1;  // 左下角x坐标108
        int y1_2 = y0_2+size; // 左下角y坐标23
        int x2_2 = x0_2+size;   // 右上角x坐标118
        int y2_2 = y0_2; // 右上角y坐标13
        tft.fillTriangle(x0_1, y0_1, x1_1, y1_1, x2_1, y2_1, ST7789_WHITE);
        tft.fillTriangle(x0_2, y0_2, x1_2, y1_2, x2_2, y2_2, ST7789_WHITE);
        if(!on){
          tft.drawCircle(x0_2, y0_2, size+1, ST7789_GREEN);
          tft.drawFastHLine(x1_1, y1_1, size*2+1, ST7789_GREEN);
        }
    }
    void setBLEcontrol(){
      bool exiting = false;
      bool currentBLEState = enableBLE;
      while (!exiting) {
        tft.fillScreen(ST7789_WHITE);
        tft.setCursor(50, 100);
        tft.setFontColor(ST7789_BLACK);
        tft.setFontSize(2);
        tft.print("BLE Setting:");
        tft.setCursor(100, 140);
        if(currentBLEState){
          tft.print("ON");
        }else{
          tft.print("OFF");
        } 
        tft.setCursor(40, 180);
        tft.setFontColor(ST7789_BLUE);
        tft.print("UP/DOWN: Adjust");
        tft.setCursor(40, 200);
        tft.print("SELECT: Back");
        tft.flush();
        if (buttonPressed(PIN_BUTTON_UP)) {
          currentBLEState = !currentBLEState;
        }
        if (buttonPressed(PIN_BUTTON_DOWN)) {
          currentBLEState = !currentBLEState;
        }
        if (buttonPressed(PIN_BUTTON_SELECT)) {
          exiting = true;
        }
        vTaskDelay(pdMS_TO_TICKS(50));
      }
      if(enableBLE != currentBLEState){
          enableBLE = currentBLEState;
        if(!BLETaskState){
          if(xBLETaskHandle == NULL){
            xTaskCreate(scanAndConnectTask, "ScanConnect", 4096, NULL, 2, &xBLETaskHandle);
          }
          BLETaskState = true;
        }
      }
    }
    void scanCB(T_LE_CB_DATA* p_data) { //BLE扫描回调函数
        foundDevice.parseScanInfo(p_data);
        if (foundDevice.hasName()) {
            if (foundDevice.getName() == TARGET_DEVICE_NAME) {
                Serial.print("Found BLE Device at address ");
                Serial.println(foundDevice.getAddr().str());
                targetDevice = foundDevice;
                g_deviceFound = true; // < << 设置标志位!
            }
        }
    }
    void notificationCB (BLERemoteCharacteristic* chr, uint8_t* data, uint16_t len) {
        char msg[len+1] = {0};
        memcpy(msg, data, len);
        Serial.print("Notification received for chr UUID: ");
        Serial.println(chr- >getUUID().str());
        Serial.print("Received string: ");
        Serial.println(String(msg));
        if (strcmp(msg, "Snapshot") == 0) { 
          Serial.println("shot");
          if(!setMenuFlag && enableBLE){
            xSemaphoreGive(xBinarySemaphore);
          } 
        }
    }
    void scanAndConnectTask(void *pvParameters) {
        Serial.println("scanAndConnectTask started");
        // 初始化BLE
        BLE.init();
        BLE.setScanCallback(scanCB);
        BLE.beginCentral(1);
        while (1) {
            // 重置状态
          if(enableBLE){
            g_bleReady = false;
            client = nullptr;
            UartService = nullptr;
            Rx = nullptr;
            Tx = nullptr;
            g_connID = -1;
            Serial.println("Starting BLE scan...");
            BLE.configScan()->startScan(2000); // 扫描2秒
            // 等待找到目标设备
            while (!g_deviceFound) {
                vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms
                static uint32_t scanStartTime = millis();
                if (millis() - scanStartTime > 5000) { // 扫描超过5秒未找到
                    Serial.println("Scan timeout. Retrying...");
                    break;
                }
            }
            if (!g_deviceFound) {
                Serial.println("Device not found in this scan cycle. Retrying...");
                vTaskDelay(pdMS_TO_TICKS(100));
                continue;
            }
    
            // 连接设备
            if (BLE.configConnection()->connect(targetDevice, 2000) == 0) {
                g_connID = BLE.configConnection()->getConnId(targetDevice);
                if (g_connID >= 0 && BLE.connected(g_connID)) {
                    Serial.println("BLE Connected successfully!");
    
                    // 配置客户端
                    BLE.configClient();
                    client = BLE.addClient(g_connID);
                    if (client == nullptr) {
                        Serial.println("Failed to create BLE client");
                        continue; // 重新开始循环
                    }
                    Serial.println("Discovering services...");
                    client->discoverServices();
    
                    // 等待服务发现完成
                    while (!client->discoveryDone()) {
                        Serial.print(".");
                        vTaskDelay(pdMS_TO_TICKS(1000));
                    }
                    Serial.println("nService discovery completed.");
                    // 获取UART服务和特征
                    UartService = client->getService(UART_SERVICE_UUID);
                    if (UartService != nullptr) {
                        Tx = UartService->getCharacteristic(CHARACTERISTIC_UUID_TX);
                        if (Tx != nullptr) {
                            Serial.println("TX characteristic found");
                            Tx->setBufferLen(STRING_BUF_SIZE);
                            Tx->setNotifyCallback(notificationCB);
                            Tx->enableNotifyIndicate(); // 启用通知
                        } else {
                            Serial.println("TX characteristic not found!");
                        }
                        Rx = UartService->getCharacteristic(CHARACTERISTIC_UUID_RX);
                        if (Rx != nullptr) {
                            Serial.println("RX characteristic found");
                            Rx->setBufferLen(STRING_BUF_SIZE);
                        } else {
                            Serial.println("RX characteristic not found!");
                        }
                    } else {
                        Serial.println("UART Service not found!");
                    }
                    // 如果所有关键组件都就绪,设置标志
                    if (Tx != nullptr && Rx != nullptr) {
                        g_bleReady = true;
                        Serial.println("BLE UART ready. Tasks can now operate.");
                    } else {
                        Serial.println("BLE setup incomplete. Reconnecting...");
                    }
                } else {
                    Serial.println("Connection failed or not established.");
                }
            } else {
                Serial.println("Connect command failed.");
            }
            // 如果连接失败或断开,等待一段时间后重试
            if (!g_bleReady) {
                Serial.println("Retrying connection in 5 seconds...");
                vTaskDelay(pdMS_TO_TICKS(5000));
            } else {
                // 连接成功,但需要监听断开事件(简化处理:如果断开,外层循环会重试)
                // 在实际应用中,应监听BLE断开事件
                while (g_bleReady && BLE.connected(g_connID)) {
                    vTaskDelay(pdMS_TO_TICKS(100)); // 保持任务运行,监听通知
                }
                Serial.println("BLE disconnected. Reconnecting...");
                // 当连接断开时,g_bleReady 会在下次循环开始时被重置
            }
          }
          vTaskDelay(pdMS_TO_TICKS(100));
        }
    }
    void drawBluetoothSymbol(int16_t centerX, int16_t centerY, int16_t size, uint16_t color, bool enable) {
        // 计算蓝牙标志的各点坐标
        float offset = sin(45)*sin(45)*size/2;
        uint16_t x1 = centerX - offset;
        uint16_t y1 = centerY -size/2 +offset;//左上角
        uint16_t x2 = centerX + offset;
        uint16_t y2 = centerY +offset;//右下角
        uint16_t x3 = centerX + offset;
        uint16_t y3 = centerY -size/2 +offset;//右上角
        uint16_t x4 = centerX - offset;
        uint16_t y4 = centerY + offset;//左下角
        if(enable){
          tft.fillCircle(centerX, centerY, size-2, ST7789_RED);
        }else{
          tft.fillCircle(centerX, centerY, size-2, ST7789_GREEN);
        }
    
        tft.drawFastVLine(centerX, centerY-size/2, size,color);
        tft.drawLine(x1, y1, x2, y2, color);
        tft.drawLine(x3, y3, x4, y4, color);
        tft.drawLine(centerX, centerY-size/2, x3, y3, color);
        tft.drawLine(centerX, centerY+size/2, x2, y2, color);
        // 绘制右上部分
    }

    Ai-M61-32S开发板的代码

    #include "shell.h"
    #include < FreeRTOS.h >
    #include "task.h"
    #include "board.h"
    #include "bluetooth.h"
    #include "conn.h"
    #include "conn_internal.h"
    #if defined(BL702) || defined(BL602)
    #include "ble_lib_api.h"
    #elif defined(BL616)
    #include "btble_lib_api.h"
    #include "bl616_glb.h"
    #include "rfparam_adapter.h"
    #elif defined(BL808)
    #include "btble_lib_api.h"
    #include "bl808_glb.h"
    #endif
    #include "gatt.h"
    #include "ble_tp_svc.h"
    #include "hci_driver.h"
    #include "hci_core.h"
    #include "bflb_gpio.h" //包含GPIO库文件
    static struct bflb_device_s *uart0;
    struct bflb_device_s *gpio;
    extern void shell_init_with_task(struct bflb_device_s *shell);
    void led_task(void *pvParameters); 
    void init_LED_GPIO(void);
    #define BUTTON_PIN    GPIO_PIN_2
    #define GREEN_LED_PIN      GPIO_PIN_14
    #define BLUE_LED_PIN     GPIO_PIN_15
    #define RED_LED_PIN     GPIO_PIN_12
    TaskHandle_t xLedTaskHandle = NULL;  // 按键任务全局句柄,初始为 NULL
    bool ble_connected_flag = false; // 按键任务运行标志
    // 定义 NUS 服务 UUID
    #define BT_UUID_NUS_SERVICE 
        BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x6E400001, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E))
    // 定义 TX 特征 UUID(设备发送数据,我们接收)
    #define BT_UUID_NUS_TX 
        BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x6E400003, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E))
    // 定义 RX 特征 UUID(我们发送数据,设备接收)
    #define BT_UUID_NUS_RX 
        BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x6E400002, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E))
    // 声明 Characteristic 值存储空间
    static uint8_t custom_rx_value[20] = {0}; // 接收缓冲区
    static uint8_t custom_tx_value[20] = {0}; // 发送缓冲区
    static uint16_t custom_rx_len = 0;
    static uint16_t custom_tx_len = 0;
    // 前向声明回调函数
    static ssize_t custom_char_rx_write(struct bt_conn *conn,
                                        const struct bt_gatt_attr *attr,
                                        const void *buf, uint16_t len,
                                        uint16_t offset, uint8_t flags);
    // 函数声明
    int ble_send_data(const uint8_t *data, uint16_t len);
    // 定义 GATT 属性表
    // 回调函数:当 CCCD 被修改时调用
    static void custom_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
    {
        ARG_UNUSED(attr);
        bool enabled = (value == BT_GATT_CCC_NOTIFY);
        printf("TX notifications %sn", enabled ? "ON" : "OFF");
    }
    static ssize_t custom_char_tx_read(struct bt_conn *conn,
                                       const struct bt_gatt_attr *attr,
                                       void *buf, uint16_t len,
                                       uint16_t offset)
    {
        const char *value = "Hello from BL616!";  // 你想返回的数据
        uint16_t value_len = strlen(value);
        // 使用 GATT 工具函数安全返回数据
        return bt_gatt_attr_read(conn, attr, buf, len, offset, value, value_len);
    }
    static ssize_t custom_char_rx_read(struct bt_conn *conn,
                                       const struct bt_gatt_attr *attr,
                                       void *buf, uint16_t len,
                                       uint16_t offset)
    {
        return bt_gatt_attr_read(conn, attr, buf, len, offset,
                                 custom_rx_value, custom_rx_len);
    }
    static struct bt_gatt_attr custom_service_attrs[] = {
        // 1. 服务声明 (Service Declaration)
        BT_GATT_PRIMARY_SERVICE(BT_UUID_NUS_SERVICE),
        // 2. RX Characteristic: 手机 → 设备 (写入)
        BT_GATT_CHARACTERISTIC(BT_UUID_NUS_RX,
                               BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP,
                               BT_GATT_PERM_WRITE | BT_GATT_PERM_READ,
                               custom_char_rx_read,  // 可选:允许手机读回
                               custom_char_rx_write,
                               NULL),
        // 3. TX Characteristic: 设备 → 手机 (通知)
        BT_GATT_CHARACTERISTIC(BT_UUID_NUS_TX,
                               BT_GATT_CHRC_NOTIFY,
                               BT_GATT_PERM_READ,
                               custom_char_tx_read,  // 允许手机读取当前值
                               NULL,
                               NULL),
        // 4. CCCD: 客户端特征配置描述符 (必须紧跟在 TX 特征后)
        BT_GATT_CCC(custom_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
    };
    // 定义 GATT 服务
    static struct bt_gatt_service custom_service =
        BT_GATT_SERVICE(custom_service_attrs);
    // 保存连接句柄,用于 notify
    static struct bt_conn *current_conn = NULL;
    // 写回调函数实现
    static ssize_t custom_char_rx_write(struct bt_conn *conn,
                                        const struct bt_gatt_attr *attr,
                                        const void *buf, uint16_t len,
                                        uint16_t offset, uint8_t flags)
    {
        if (offset + len > sizeof(custom_rx_value)) {
            return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
        }
        // 拷贝数据
        memcpy(custom_rx_value + offset, buf, len);
        custom_rx_len = offset + len;
        printf("Received from phone: %.*sn", custom_rx_len, custom_rx_value);
        // 回显给手机(可选)
        if (current_conn) {
            memcpy(custom_tx_value, custom_rx_value, custom_rx_len);
            custom_tx_len = custom_rx_len;
            bt_gatt_notify(current_conn, &custom_service.attrs[3], custom_tx_value, custom_tx_len);
        }
        return len;
    }
    static int btblecontroller_em_config(void)
    {
        extern uint8_t __LD_CONFIG_EM_SEL;
        volatile uint32_t em_size;
        em_size = (uint32_t)&__LD_CONFIG_EM_SEL;
        if (em_size == 0) {
            GLB_Set_EM_Sel(GLB_WRAM160KB_EM0KB);
        } else if (em_size == 32*1024) {
            GLB_Set_EM_Sel(GLB_WRAM128KB_EM32KB);
        } else if (em_size == 64*1024) {
            GLB_Set_EM_Sel(GLB_WRAM96KB_EM64KB);
        } else {
            GLB_Set_EM_Sel(GLB_WRAM96KB_EM64KB);
        }
        return 0;
    }
    static void ble_connected(struct bt_conn *conn, u8_t err)
    {
        if(err || conn->type != BT_CONN_TYPE_LE)
        {
            return;
        }
        printf("%s",__func__);
        bflb_gpio_set(gpio, GREEN_LED_PIN);   // 点亮绿色 LED
        bflb_gpio_reset(gpio, RED_LED_PIN); // 熄灭红色LED
        current_conn = bt_conn_ref(conn); // 保存连接句柄
        ble_connected_flag = true;
    }
    static void ble_disconnected(struct bt_conn *conn, u8_t reason)
    { 
        int ret;
        if(conn->type != BT_CONN_TYPE_LE)
        {
            return;
        }
        printf("%s",__func__);
        bflb_gpio_reset(gpio, GREEN_LED_PIN);   // 点亮绿色 LED
        bflb_gpio_set(gpio, RED_LED_PIN); // 熄灭红色LED
        ble_connected_flag = false;
        // enable adv
        if (current_conn) {
            bt_conn_unref(current_conn);
            current_conn = NULL;
        }
        ret = set_adv_enable(true);
        if(ret) {
            printf("Restart adv fail. rn");
        }
    }
    static struct bt_conn_cb ble_conn_callbacks = {
        .connected  =   ble_connected,
        .disconnected   =   ble_disconnected,
    };
    static void ble_start_adv(void)
    {
        struct bt_le_adv_param param;
        int err = -1;
        struct bt_data adv_data[1] = {
            BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_NO_BREDR | BT_LE_AD_GENERAL)
        };
        struct bt_data adv_rsp[1] = {
            BT_DATA_BYTES(BT_DATA_MANUFACTURER_DATA, "BL616")
        };
        memset(¶m, 0, sizeof(param));
        // Set advertise interval
        param.interval_min = BT_GAP_ADV_FAST_INT_MIN_2;
        param.interval_max = BT_GAP_ADV_FAST_INT_MAX_2;
        /*Get adv type, 0:adv_ind,  1:adv_scan_ind, 2:adv_nonconn_ind 3: adv_direct_ind*/
        param.options = (BT_LE_ADV_OPT_CONNECTABLE | BT_LE_ADV_OPT_USE_NAME | BT_LE_ADV_OPT_ONE_TIME); 
        err = bt_le_adv_start(¶m, adv_data, ARRAY_SIZE(adv_data), adv_rsp, ARRAY_SIZE(adv_rsp));
        if(err){
            printf("Failed to start advertising (err %d) rn", err);
        }
        printf("Start advertising success.rn");
    }
    void bt_enable_cb(int err)
    {
        if (!err) {
            bt_addr_le_t bt_addr;
            bt_get_local_public_address(&bt_addr);
            printf("BD_ADDR:(MSB)%02x:%02x:%02x:%02x:%02x:%02x(LSB) rn",
                bt_addr.a.val[5], bt_addr.a.val[4], bt_addr.a.val[3], bt_addr.a.val[2], bt_addr.a.val[1], bt_addr.a.val[0]);
            bt_conn_cb_register(&ble_conn_callbacks);
            bt_set_name("Ble_cam_control");
            bt_gatt_service_register(&custom_service); // 注册自定义服务
            //ble_tp_init();
    
            // start advertising
            ble_start_adv();
        }
    }
    int main(void)
    {
        board_init();
        init_LED_GPIO();
        configASSERT((configMAX_PRIORITIES > 4));
    
        uart0 = bflb_device_get_by_name("uart0");
        shell_init_with_task(uart0);
        /* set ble controller EM Size */
        btblecontroller_em_config();
    #if defined(BL616)
        /* Init rf */
        if (0 != rfparam_init(0, NULL, 0)) {
            printf("PHY RF init failed!rn");
            return 0;
        }
    #endif
        // Initialize BLE controller
        #if defined(BL702) || defined(BL602)
        ble_controller_init(configMAX_PRIORITIES - 1);
        #else
        btble_controller_init(configMAX_PRIORITIES - 1);
        #endif
        // Initialize BLE Host stack
        hci_driver_init();
        bt_enable(bt_enable_cb);
        xTaskCreate(led_task, "LED_Task", 512, NULL, configMAX_PRIORITIES - 2, &xLedTaskHandle);
        vTaskStartScheduler();
        while (1) {
        }
    }
    void init_LED_GPIO(void)
    {gpio = bflb_device_get_by_name("gpio");
    bflb_gpio_init(gpio, GREEN_LED_PIN, GPIO_OUTPUT | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_0);
    bflb_gpio_init(gpio, BLUE_LED_PIN, GPIO_OUTPUT | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_0);
    bflb_gpio_init(gpio, RED_LED_PIN, GPIO_OUTPUT | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_0);
    bflb_gpio_init(gpio, BUTTON_PIN, GPIO_INPUT | GPIO_PULLDOWN | GPIO_SMT_EN | GPIO_DRV_0);
    }
    void led_task(void *pvParameters)
    {
        uint8_t button_last_state = 0;  // 上一次按键状态(0:释放,1:按下)
        while (1) {
            uint8_t button_current = bflb_gpio_read(gpio, BUTTON_PIN);
            if(!ble_connected_flag) {
                // 如果未连接蓝牙,则保持红色LED点亮,绿色和蓝色LED熄灭
                bflb_gpio_set(gpio, RED_LED_PIN);   // 点亮红色 LED
                bflb_gpio_reset(gpio, GREEN_LED_PIN); // 熄灭绿色LED
                bflb_gpio_reset(gpio, BLUE_LED_PIN); // 熄灭蓝色LED
                vTaskDelay(100 / portTICK_PERIOD_MS); // 延时,避免CPU占用过高
                continue; // 跳过按键检测,继续循环
            }
            // 检测从“按下”到“释放”的跳变(上升沿)
            if (button_last_state == 1 && button_current == 0) {
                // 消抖:确认释放状态
                vTaskDelay(10 / portTICK_PERIOD_MS);
                if (bflb_gpio_read(gpio, BUTTON_PIN) == 0) {
                    // 确认按键已释放,触发动作
                    printf("Button Released! Turn on Green LED.n");
                    bflb_gpio_set(gpio, GREEN_LED_PIN);   // 点亮绿色 LED
                    bflb_gpio_reset(gpio, BLUE_LED_PIN); // 熄灭蓝色LED
                }
            }else if (button_last_state == 0 && button_current == 1) {
                // 消抖:确认按下状态
                vTaskDelay(10 / portTICK_PERIOD_MS);
                if (bflb_gpio_read(gpio, BUTTON_PIN) == 1) {
                    // 确认按键已按下,触发动作
                    printf("Button Pressed! Turn on blue LED.n");
                    bflb_gpio_set(gpio, BLUE_LED_PIN);   // 点亮蓝色LED
                    bflb_gpio_reset(gpio, GREEN_LED_PIN); // 熄灭绿色LED
                    ble_send_data((uint8_t*)"Snapshot", 8); // 发送BLE数据
                }
            }
            // 更新按键状态
            button_last_state = button_current;
            // 主循环延时,避免 CPU 占用过高
            vTaskDelay(20 / portTICK_PERIOD_MS);
        }
    }
    int ble_send_data(const uint8_t *data, uint16_t len)
    {
        if (!current_conn || !data || len == 0 || len > sizeof(custom_tx_value)) {
            return -1;
        }
        memcpy(custom_tx_value, data, len);
        custom_tx_len = len;
        // 发送通知
        int err = bt_gatt_notify(current_conn, &custom_service.attrs[3], custom_tx_value, custom_tx_len);
        if (err) {
            printf("Notify failed: %dn", err);
            return -1;
        }
        printf("Sent to phone: %.*sn", len, data);
        return 0;
    }

    视频演示

    https://www.bilibili.com/video/BV1rFYPzyE8e/?spm_id_from=888.80997.embed_other.whitelist&t=139.890003&bvid=BV1rFYPzyE8e&vd_source=54c5db21948db2378659b7e8e42bafbf

    wKgZO2jJGmuALIWXAAA1nOJ8xRY57.webp

    审核编辑 黄宇