esp32_camera.cc 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. #include "esp32_camera.h"
  2. #include "mcp_server.h"
  3. #include "display.h"
  4. #include "board.h"
  5. #include "system_info.h"
  6. #include <esp_log.h>
  7. #include <esp_heap_caps.h>
  8. #include <img_converters.h>
  9. #include <cstring>
  10. #define TAG "Esp32Camera"
  11. Esp32Camera::Esp32Camera(const camera_config_t& config) {
  12. // camera init
  13. esp_err_t err = esp_camera_init(&config); // 配置上面定义的参数
  14. if (err != ESP_OK) {
  15. ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
  16. return;
  17. }
  18. sensor_t *s = esp_camera_sensor_get(); // 获取摄像头型号
  19. if (s->id.PID == GC0308_PID) {
  20. s->set_hmirror(s, 0); // 这里控制摄像头镜像 写1镜像 写0不镜像
  21. }
  22. // 初始化预览图片的内存
  23. memset(&preview_image_, 0, sizeof(preview_image_));
  24. preview_image_.header.magic = LV_IMAGE_HEADER_MAGIC;
  25. preview_image_.header.cf = LV_COLOR_FORMAT_RGB565;
  26. preview_image_.header.flags = LV_IMAGE_FLAGS_ALLOCATED | LV_IMAGE_FLAGS_MODIFIABLE;
  27. switch (config.frame_size) {
  28. case FRAMESIZE_SVGA:
  29. preview_image_.header.w = 800;
  30. preview_image_.header.h = 600;
  31. break;
  32. case FRAMESIZE_VGA:
  33. preview_image_.header.w = 640;
  34. preview_image_.header.h = 480;
  35. break;
  36. case FRAMESIZE_QVGA:
  37. preview_image_.header.w = 320;
  38. preview_image_.header.h = 240;
  39. break;
  40. case FRAMESIZE_128X128:
  41. preview_image_.header.w = 128;
  42. preview_image_.header.h = 128;
  43. break;
  44. case FRAMESIZE_240X240:
  45. preview_image_.header.w = 240;
  46. preview_image_.header.h = 240;
  47. break;
  48. default:
  49. ESP_LOGE(TAG, "Unsupported frame size: %d, image preview will not be shown", config.frame_size);
  50. preview_image_.data_size = 0;
  51. preview_image_.data = nullptr;
  52. return;
  53. }
  54. preview_image_.header.stride = preview_image_.header.w * 2;
  55. preview_image_.data_size = preview_image_.header.w * preview_image_.header.h * 2;
  56. preview_image_.data = (uint8_t*)heap_caps_malloc(preview_image_.data_size, MALLOC_CAP_SPIRAM);
  57. if (preview_image_.data == nullptr) {
  58. ESP_LOGE(TAG, "Failed to allocate memory for preview image");
  59. return;
  60. }
  61. }
  62. Esp32Camera::~Esp32Camera() {
  63. if (fb_) {
  64. esp_camera_fb_return(fb_);
  65. fb_ = nullptr;
  66. }
  67. if (preview_image_.data) {
  68. heap_caps_free((void*)preview_image_.data);
  69. preview_image_.data = nullptr;
  70. }
  71. esp_camera_deinit();
  72. }
  73. void Esp32Camera::SetExplainUrl(const std::string& url, const std::string& token) {
  74. explain_url_ = url;
  75. explain_token_ = token;
  76. }
  77. bool Esp32Camera::Capture() {
  78. if (encoder_thread_.joinable()) {
  79. encoder_thread_.join();
  80. }
  81. int frames_to_get = 2;
  82. // Try to get a stable frame
  83. for (int i = 0; i < frames_to_get; i++) {
  84. if (fb_ != nullptr) {
  85. esp_camera_fb_return(fb_);
  86. }
  87. fb_ = esp_camera_fb_get();
  88. if (fb_ == nullptr) {
  89. ESP_LOGE(TAG, "Camera capture failed");
  90. return false;
  91. }
  92. }
  93. // 如果预览图片 buffer 为空,则跳过预览
  94. // 但仍返回 true,因为此时图像可以上传至服务器
  95. if (preview_image_.data_size == 0) {
  96. ESP_LOGW(TAG, "Skip preview because of unsupported frame size");
  97. return true;
  98. }
  99. if (preview_image_.data == nullptr) {
  100. ESP_LOGE(TAG, "Preview image data is not initialized");
  101. return true;
  102. }
  103. // 显示预览图片
  104. auto display = Board::GetInstance().GetDisplay();
  105. if (display != nullptr) {
  106. auto src = (uint16_t*)fb_->buf;
  107. auto dst = (uint16_t*)preview_image_.data;
  108. size_t pixel_count = fb_->len / 2;
  109. for (size_t i = 0; i < pixel_count; i++) {
  110. // 交换每个16位字内的字节
  111. dst[i] = __builtin_bswap16(src[i]);
  112. }
  113. display->SetPreviewImage(&preview_image_);
  114. }
  115. return true;
  116. }
  117. bool Esp32Camera::SetHMirror(bool enabled) {
  118. sensor_t *s = esp_camera_sensor_get();
  119. if (s == nullptr) {
  120. ESP_LOGE(TAG, "Failed to get camera sensor");
  121. return false;
  122. }
  123. esp_err_t err = s->set_hmirror(s, enabled);
  124. if (err != ESP_OK) {
  125. ESP_LOGE(TAG, "Failed to set horizontal mirror: %d", err);
  126. return false;
  127. }
  128. ESP_LOGI(TAG, "Camera horizontal mirror set to: %s", enabled ? "enabled" : "disabled");
  129. return true;
  130. }
  131. bool Esp32Camera::SetVFlip(bool enabled) {
  132. sensor_t *s = esp_camera_sensor_get();
  133. if (s == nullptr) {
  134. ESP_LOGE(TAG, "Failed to get camera sensor");
  135. return false;
  136. }
  137. esp_err_t err = s->set_vflip(s, enabled);
  138. if (err != ESP_OK) {
  139. ESP_LOGE(TAG, "Failed to set vertical flip: %d", err);
  140. return false;
  141. }
  142. ESP_LOGI(TAG, "Camera vertical flip set to: %s", enabled ? "enabled" : "disabled");
  143. return true;
  144. }
  145. /**
  146. * @brief 将摄像头捕获的图像发送到远程服务器进行AI分析和解释
  147. *
  148. * 该函数将当前摄像头缓冲区中的图像编码为JPEG格式,并通过HTTP POST请求
  149. * 以multipart/form-data的形式发送到指定的解释服务器。服务器将根据提供的
  150. * 问题对图像进行AI分析并返回结果。
  151. *
  152. * 实现特点:
  153. * - 使用独立线程编码JPEG,与主线程分离
  154. * - 采用分块传输编码(chunked transfer encoding)优化内存使用
  155. * - 通过队列机制实现编码线程和发送线程的数据同步
  156. * - 支持设备ID、客户端ID和认证令牌的HTTP头部配置
  157. *
  158. * @param question 要向AI提出的关于图像的问题,将作为表单字段发送
  159. * @return std::string 服务器返回的JSON格式响应字符串
  160. * 成功时包含AI分析结果,失败时包含错误信息
  161. * 格式示例:{"success": true, "result": "分析结果"}
  162. * {"success": false, "message": "错误信息"}
  163. *
  164. * @note 调用此函数前必须先调用SetExplainUrl()设置服务器URL
  165. * @note 函数会等待之前的编码线程完成后再开始新的处理
  166. * @warning 如果摄像头缓冲区为空或网络连接失败,将返回错误信息
  167. */
  168. std::string Esp32Camera::Explain(const std::string& question) {
  169. if (explain_url_.empty()) {
  170. return "{\"success\": false, \"message\": \"Image explain URL or token is not set\"}";
  171. }
  172. // 创建局部的 JPEG 队列, 40 entries is about to store 512 * 40 = 20480 bytes of JPEG data
  173. QueueHandle_t jpeg_queue = xQueueCreate(40, sizeof(JpegChunk));
  174. if (jpeg_queue == nullptr) {
  175. ESP_LOGE(TAG, "Failed to create JPEG queue");
  176. return "{\"success\": false, \"message\": \"Failed to create JPEG queue\"}";
  177. }
  178. // We spawn a thread to encode the image to JPEG
  179. encoder_thread_ = std::thread([this, jpeg_queue]() {
  180. frame2jpg_cb(fb_, 80, [](void* arg, size_t index, const void* data, size_t len) -> unsigned int {
  181. auto jpeg_queue = (QueueHandle_t)arg;
  182. JpegChunk chunk = {
  183. .data = (uint8_t*)heap_caps_aligned_alloc(16, len, MALLOC_CAP_SPIRAM),
  184. .len = len
  185. };
  186. memcpy(chunk.data, data, len);
  187. xQueueSend(jpeg_queue, &chunk, portMAX_DELAY);
  188. return len;
  189. }, jpeg_queue);
  190. });
  191. auto network = Board::GetInstance().GetNetwork();
  192. auto http = network->CreateHttp(3);
  193. // 构造multipart/form-data请求体
  194. std::string boundary = "----ESP32_CAMERA_BOUNDARY";
  195. // 配置HTTP客户端,使用分块传输编码
  196. http->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
  197. http->SetHeader("Client-Id", Board::GetInstance().GetUuid().c_str());
  198. if (!explain_token_.empty()) {
  199. http->SetHeader("Authorization", "Bearer " + explain_token_);
  200. }
  201. http->SetHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
  202. http->SetHeader("Transfer-Encoding", "chunked");
  203. if (!http->Open("POST", explain_url_)) {
  204. ESP_LOGE(TAG, "Failed to connect to explain URL");
  205. // Clear the queue
  206. encoder_thread_.join();
  207. JpegChunk chunk;
  208. while (xQueueReceive(jpeg_queue, &chunk, portMAX_DELAY) == pdPASS) {
  209. if (chunk.data != nullptr) {
  210. heap_caps_free(chunk.data);
  211. } else {
  212. break;
  213. }
  214. }
  215. vQueueDelete(jpeg_queue);
  216. return "{\"success\": false, \"message\": \"Failed to connect to explain URL\"}";
  217. }
  218. {
  219. // 第一块:question字段
  220. std::string question_field;
  221. question_field += "--" + boundary + "\r\n";
  222. question_field += "Content-Disposition: form-data; name=\"question\"\r\n";
  223. question_field += "\r\n";
  224. question_field += question + "\r\n";
  225. http->Write(question_field.c_str(), question_field.size());
  226. }
  227. {
  228. // 第二块:文件字段头部
  229. std::string file_header;
  230. file_header += "--" + boundary + "\r\n";
  231. file_header += "Content-Disposition: form-data; name=\"file\"; filename=\"camera.jpg\"\r\n";
  232. file_header += "Content-Type: image/jpeg\r\n";
  233. file_header += "\r\n";
  234. http->Write(file_header.c_str(), file_header.size());
  235. }
  236. // 第三块:JPEG数据
  237. size_t total_sent = 0;
  238. while (true) {
  239. JpegChunk chunk;
  240. if (xQueueReceive(jpeg_queue, &chunk, portMAX_DELAY) != pdPASS) {
  241. ESP_LOGE(TAG, "Failed to receive JPEG chunk");
  242. break;
  243. }
  244. if (chunk.data == nullptr) {
  245. break; // The last chunk
  246. }
  247. http->Write((const char*)chunk.data, chunk.len);
  248. total_sent += chunk.len;
  249. heap_caps_free(chunk.data);
  250. }
  251. // Wait for the encoder thread to finish
  252. encoder_thread_.join();
  253. // 清理队列
  254. vQueueDelete(jpeg_queue);
  255. {
  256. // 第四块:multipart尾部
  257. std::string multipart_footer;
  258. multipart_footer += "\r\n--" + boundary + "--\r\n";
  259. http->Write(multipart_footer.c_str(), multipart_footer.size());
  260. }
  261. // 结束块
  262. http->Write("", 0);
  263. if (http->GetStatusCode() != 200) {
  264. ESP_LOGE(TAG, "Failed to upload photo, status code: %d", http->GetStatusCode());
  265. return "{\"success\": false, \"message\": \"Failed to upload photo\"}";
  266. }
  267. std::string result = http->ReadAll();
  268. http->Close();
  269. // Get remain task stack size
  270. size_t remain_stack_size = uxTaskGetStackHighWaterMark(nullptr);
  271. ESP_LOGI(TAG, "Explain image size=%dx%d, compressed size=%d, remain stack size=%d, question=%s\n%s",
  272. fb_->width, fb_->height, total_sent, remain_stack_size, question.c_str(), result.c_str());
  273. return result;
  274. }