发帖
9 0 0

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

dzy7455339
金牌会员

5

主题

22

回帖

1077

积分

金牌会员

积分
1077
电子DIY 658 9 2025-9-9 09:32:42
[i=s] 本帖最后由 dzy7455339 于 2025-9-9 09:32 编辑 [/i]

一直想自己DIY一个相机,但是奈何个人水平有限,虽然有各种强大的芯片,但是自己用不了,后来有了ESP32但是拍摄出的画面质量不是很满意,所以这个想法被我一直搁置。看到安信可新出的BW21支持摄像头、还支持1080p录像,支持SD卡,最最关键的是它还支持Arduino编程,让我做相机的想法得以快速实现。

f476496d-c747-415c-b0e4-8b4a696621ac.jpg

73a1812d-502f-47c6-b761-11758727a52d.jpg

一、硬件准备

有了想法和合适的平台那就动起来。考虑到个人硬件水平有限,这里直接使用了BW21-CBV-KIT开发板作为核心。围绕核心功能相机,需要准备的外部设备还有电源、屏幕、闪光灯、计时器、按键这些功能。另外板子虽然板载了一个模拟麦克风,但是实际使用起来比较差强人意,所以数字麦克风也加入了外设的清单里。

以BW21-CBV-KIT为基础做了2个扩展版,一个扩展版主要承载屏幕、按键以及BW21-CBV-KIT,第二个扩展版则集成了充电、RTC、闪光灯以及数字麦克风。

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

2ea95b42-272d-403e-bced-6f077c0ca6e0.jpg

板子回来之后进行了单项基础功能测试,发现一个是我选择引脚的时候没有避开SWD引脚导致I2C通讯失败,另一个是板子和外壳留孔对不上。没办法只能重新来过,幸好第二次板子功能正常,和外壳也搭配的可以使用。

7f239e62-1e0b-415c-8ca1-8f86c29d3bc3.jpg

二、软件

其实这个板子在Arduino中为大家准备了很多使用的例子进行使用,我需要做的就是把这些给有机组合起来。

这里用到的核心例程主要有Camera_2_LCD, SingleVideoWithAudio以及SDCARDsaveJPG几个示例。第一个示例实现了摄像头画面到屏幕,第二个则实现了录制MP4视频到SD卡,第三个示例实现了拍摄图像到SD卡。这是相机的3个核心任务,我在arduino中使用RTOS建立了3个程序并用相应的按键来控制这3个任务的启动或者停止。

IMG_1677.JPG

核心功能之外,相机还需要一些简单的设置和显示功能,比如设置时间、屏幕亮度、闪光灯开关、浏览相片、蓝牙遥控等。这里使用裸机直接写了一个简单的目录界面,使用按键进行控制,在界面中可以进行相片浏览、屏幕亮度调整以及蓝牙遥控的开关等相关的功能。

IMG_1675.JPG

蓝牙控制这里考虑到如果使用手机谁还使用这个相机(手机像素肯定比这个好),这里使用了一个AI-M61-32S开发板来充当蓝牙遥控,当然手机搜索对应的广播名称并发送指令Snapshot也是能够控制的。BW21-CBV充当主机设备扫描并连接特定名称设备,AI-M61-32S作为从机进行广播。

20250827193600_05_0320250909-090829.png

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

d21e93c7-b7f9-4179-810e-667fbce1e419.jpg

电源使用的充放电一体芯片,但是这个芯片充电的时候不能关机,会导致充电时相机还处于运行状态。这里使用ADC对电压检测,如果检测到电压高于4.2V就进入睡眠模式来实现假关机。

2025_09_03_08_32_IMG00_01_5920250909-090229.png

下面是的代码供参考,比较乱。

BW21开发板的代码

#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\"\r\n", path);
    } else {
      printf("创建文件夹失败: \"%s\"\r\n", path);
    }
  } else {
    printf("文件夹已存在: \"%s\"\r\n", 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] != '\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%d\n", 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 %s\n", 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: %.*s\n", 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. \r\n");
    }
}

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) \r\n", err);
    }
    printf("Start advertising success.\r\n");
}

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) \r\n",
            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!\r\n");
        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: %d\n", err);
        return -1;
    }

    printf("Sent to phone: %.*s\n", len, data);
    return 0;
}

视频演示及功能说明

──── 0人觉得很赞 ────

使用道具 举报

2025-9-9 09:40:48
厉害👍
2025-9-9 10:00:38
不错不错!
2025-9-9 10:17:46

谢谢大佬的鼓励!
2025-9-9 10:21:20
哇哇, 好酷呀
2025-9-9 17:32:58
厉害
2025-9-9 20:17:05
哇,好棒,我也打算搞一个这个!
厉害了,大佬
2025-9-16 21:37:33
现在的年轻人强的可怕!
2025-9-18 11:38:42
酷啊
您需要登录后才可以回帖 立即登录
高级模式
返回
统计信息
  • 会员数: 30101 个
  • 话题数: 44217 篇