以下作品由安信可社區(qū)用戶
dzy7455339制作
原貼地址
【電子DIY作品】BW21數(shù)碼相機+BW21-CBV-KIT
一直想自己DIY一個相機,但是奈何筆者個人水平有限,雖然有各種強大的芯片,但是自己用不了,后來有了ESP32但是拍攝出的畫面質量不是很滿意,所以這個想法一直擱置。
看到安信可新出的BW21-CBV-Kit支持攝像頭、1080p錄像和SD卡,最關鍵的是它還支持Arduino編程,讓筆者做相機的想法得以快速實現(xiàn)。
1硬件準備
考慮到個人硬件水平有限,這里直接使用了BW21-CBV-Kit開發(fā)板作為核心。圍繞核心功能相機,需要準備的外部設備還有電源、屏幕、閃光燈、計時器、按鍵這些功能。
另外板子雖然板載了一個模擬麥克風,但是實際使用起來比較差強人意,所以數(shù)字麥克風也加入了外設的清單里。
以BW21-CBV-Kit為基礎做了2個擴展版:
第一個擴展版主要承載屏幕、按鍵以及BW21-CBV-Kit,
第二個擴展版則集成了充電、RTC、閃光燈以及數(shù)字麥克風。
因為不會使用3D軟件,這里直接在擴展版的基礎上利用立創(chuàng)EDA的制作外殼功能畫了一個簡單的外殼。
先對開發(fā)板進行了單項基礎功能測試,發(fā)現(xiàn)2個問題:
一是選擇引腳的時候沒有避開SWD引腳,導致I2C通訊失敗。
二是板子和外殼留孔對不上,沒辦法只能重新來過。
幸好第二次板子功能正常,和外殼也很搭配。
2軟件
其實開發(fā)板在Arduino中準備了很多使用的例子進行使用,需要做的就是把這些組合起來。
這里用到的核心例程主要有Camera_2_LCD, SingleVideoWithAudio以及SDCARDsaveJPG幾個示例。
第一個示例實現(xiàn)了攝像頭畫面到屏幕
第二個示例實現(xiàn)了錄制MP4視頻到SD卡
第三個示例實現(xiàn)了拍攝圖像到SD卡
這是相機的3個核心任務,在arduino中使用RTOS建立了3個程序,并用相應的按鍵來控制這3個任務的啟動或者停止。
核心功能之外,相機還需要一些簡單的設置和顯示功能。比如設置時間、屏幕亮度、閃光燈開關、瀏覽相片、藍牙遙控等。
這里使用裸機直接寫了一個簡單的目錄界面,使用按鍵進行控制,在界面中可以進行相片瀏覽、屏幕亮度調整以及藍牙遙控的開關等功能。
藍牙控制使用了一個Ai-M61-32S開發(fā)板來充當藍牙遙控,當然手機搜索對應的廣播名稱并發(fā)送指令Snapshot也是能夠控制的。
BW21-CBV-Kit充當主機設備掃描并連接特定名稱設備,Ai-M61-32S作為從機進行廣播。
板子被封在殼子里不能像常規(guī)相機一樣直接拿取SD卡拷貝照片和視頻,參考官方例程實現(xiàn)了通過USB來讀取照片和視頻,功能的打開通過按鍵實現(xiàn)。
電源使用的充放電一體芯片,但是芯片充電的時候不能關機,會導致充電時相機還處于運行狀態(tài)。
這里使用ADC檢測電壓,如果檢測到電壓高于4.2V就進入睡眠模式來實現(xiàn)假關機。
BW21-CBV-Kit開發(fā)板的代碼
#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" //系統(tǒng)調用 #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; // << 新增:標志位,表示目標設備已被發(fā)現(xiàn) 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 // 下一張 // 當前設置狀態(tài)枚舉 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 圖像緩沖區(qū) 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 //flash unsigned int photoCount = 0; //照片編號 #define PHOTO_COUNTER_OFFSET 0x1E00 // Flash 偏移地址,用于存儲照片計數(shù) #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("? 信號量創(chuàng)建失??!"); 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系統(tǒng)重啟 } 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("創(chuàng)建文件夾: "%s"rn", path); } else { printf("創(chuàng)建文件夾失敗: "%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 }; // 靜態(tài)保存上次的文件名 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("讀取到已拍攝照片數(shù)量: "); 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; // 幀計數(shù)器 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); // 更新幀計數(shù)器 frameCount++; if (currentMillis - previousMillis >= 1000) { fps = frameCount * 1000.0f / (currentMillis - previousMillis); // 計算每秒幀數(shù) frameCount = 0; // 重置幀計數(shù)器 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){ //連接未連接狀態(tài)切換 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相關函數(shù)*/ 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(); } // ==================== 主菜單導航與執(zhí)行 ==================== 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)) { // 根據(jù)選擇進入第二級操作 switch (currentMenuIndex) { case MENU_PHOTO_BROWSER: enterPhotoBrowser(); // 你需要實現(xiàn)這個函數(shù) break; case MENU_BRIGHTNESS_SCREEN: adjustScreenBrightness(); // 下面提供示例 break; case MENU_BRIGHTNESS_LED: adjustLEDBrightness(); // 下面提供示例 break; case MENU_SET_TIME: setTimeWithButtons(); // 你已有的函數(shù) break; case MENU_EXIT: tft.setFontColor(ST7789_WHITE); tft.setFontSize(1); setMenuFlag = false; // 新增:退出菜單 return; // 退出 navigateMainMenu 函數(shù),回到主循環(huán) } } vTaskDelay(pdMS_TO_TICKS(100)); } } void adjustScreenBrightness() { uint8_t brightness = getCurrentBrightness(); // 你需要實現(xiàn):讀取當前亮度值(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); // 你需要實現(xiàn)這個函數(shù)(如通過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); // 你需要實現(xiàn):控制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) { // 長按超過閾值,執(zhí)行刪除 deleteCurrentImage(); recPressStartTime = 0; // 重置計時器 continue; // 跳過本次循環(huán)剩余邏輯 } } 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,注意不要超出緩沖區(qū) 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); // 內部會根據(jù) 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 // 用戶繼續(xù)長按確認刪除 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); // 電池正極頭 // 根據(jù)電量level計算需要填充的線條數(shù)量 int lines = map(level, 0, 100, 0, height / (height / 10)); // 將電量映射到線條數(shù) 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掃描回調函數(shù) 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) { // 重置狀態(tài) 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; // 重新開始循環(huán) } Serial.println("Discovering services..."); client->discoverServices(); // 等待服務發(fā)現(xiàn)完成 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 { // 連接成功,但需要監(jiān)聽斷開事件(簡化處理:如果斷開,外層循環(huán)會重試) // 在實際應用中,應監(jiān)聽BLE斷開事件 while (g_bleReady && BLE.connected(g_connID)) { vTaskDelay(pdMS_TO_TICKS(100)); // 保持任務運行,監(jiān)聽通知 } Serial.println("BLE disconnected. Reconnecting..."); // 當連接斷開時,g_bleReady 會在下次循環(huán)開始時被重置 } } 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開發(fā)板的代碼
#include "shell.h" #include #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(設備發(fā)送數(shù)據(jù),我們接收) #define BT_UUID_NUS_TX BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x6E400003, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E)) // 定義 RX 特征 UUID(我們發(fā)送數(shù)據(jù),設備接收) #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}; // 接收緩沖區(qū) static uint8_t custom_tx_value[20] = {0}; // 發(fā)送緩沖區(qū) static uint16_t custom_rx_len = 0; static uint16_t custom_tx_len = 0; // 前向聲明回調函數(shù) 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); // 函數(shù)聲明 int ble_send_data(const uint8_t *data, uint16_t len); // 定義 GATT 屬性表 // 回調函數(shù):當 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!"; // 你想返回的數(shù)據(jù) uint16_t value_len = strlen(value); // 使用 GATT 工具函數(shù)安全返回數(shù)據(jù) 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; // 寫回調函數(shù)實現(xiàn) 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); } // 拷貝數(shù)據(jù) 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; // 上一次按鍵狀態(tài)(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; // 跳過按鍵檢測,繼續(xù)循環(huán) } // 檢測從“按下”到“釋放”的跳變(上升沿) if (button_last_state == 1 && button_current == 0) { // 消抖:確認釋放狀態(tài) vTaskDelay(10 / portTICK_PERIOD_MS); if (bflb_gpio_read(gpio, BUTTON_PIN) == 0) { // 確認按鍵已釋放,觸發(fā)動作 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) { // 消抖:確認按下狀態(tài) vTaskDelay(10 / portTICK_PERIOD_MS); if (bflb_gpio_read(gpio, BUTTON_PIN) == 1) { // 確認按鍵已按下,觸發(fā)動作 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); // 發(fā)送BLE數(shù)據(jù) } } // 更新按鍵狀態(tài) button_last_state = button_current; // 主循環(huán)延時,避免 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; // 發(fā)送通知 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
審核編輯 黃宇
-
AI
+關注
關注
88文章
37117瀏覽量
291157 -
開發(fā)板
+關注
關注
25文章
6017瀏覽量
110435 -
Arduino
+關注
關注
190文章
6509瀏覽量
195121
發(fā)布評論請先 登錄
【技術討論】智能戒指手勢交互:如何優(yōu)化PCBA成本與實現(xiàn)<20ms低延遲?

用Wi-Fi藍牙模組Ai-M62-CBS做一個電子沙漏

基于LockAI視覺識別模塊:C++人臉識別
【BPI-CanMV-K230D-Zero開發(fā)板體驗】人臉檢測、手勢識別、車牌識別
OBOO鷗柏丨AI數(shù)字人觸摸屏查詢觸控人臉識別語音交互一體機上市

評論