#include "board_svc.h" #include "eye_i2c.h" #include "robot_link.h" #include "status_led.h" #include "vision.h" #include #include #include #include #include "esp_camera.h" #include "esp_check.h" #include "esp_event.h" #include "esp_heap_caps.h" #include "esp_http_server.h" #include "esp_log.h" #include "esp_netif.h" #include "esp_timer.h" #include "esp_wifi.h" #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" #include "freertos/task.h" #include "nvs_flash.h" #include "camera_pins.h" static const char *TAG = "cam_stream"; /* -------- put your Wi‑Fi credentials here -------- */ #define WIFI_SSID "Police Surveillance Van" #define WIFI_PASS "ourpassword" /* ------------------------------------------------ */ /* Stream tuning for navigation — lower = steadier / lower latency */ #define CAM_FRAME_SIZE FRAMESIZE_QVGA /* 320x240 */ #define CAM_JPEG_QUALITY 18 /* 0-63, higher = smaller files */ #define STREAM_FPS 10 #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 #define WIFI_MAX_RETRY 10 #define PART_BOUNDARY "123456789000000000000987654321" #define _STR(x) #x #define STR(x) _STR(x) static EventGroupHandle_t s_wifi_event_group; static int s_retry_num = 0; static camera_config_t s_cam_config; static bool s_camera_ok = false; static pixformat_t s_camera_pix_fmt = PIXFORMAT_JPEG; static SemaphoreHandle_t s_cam_mutex; static httpd_handle_t s_httpd = NULL; static bool s_wifi_driver_up = false; static bool s_wifi_netif_ready = false; static volatile bool s_want_camera = false; static volatile bool s_want_wifi = false; static volatile bool s_wifi_restart = false; static char s_wifi_ssid[33]; static char s_wifi_pass[65]; static bool s_wifi_cred_from_i2c = false; static void set_no_cache(httpd_req_t *req); static const char *STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY; static const char *STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n"; static const char *STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n"; /* Embedded UI (main/www/index.html) — video + local vision controls */ extern const uint8_t index_html_start[] asm("_binary_index_html_start"); extern const uint8_t index_html_end[] asm("_binary_index_html_end"); static esp_err_t init_camera_with_format(pixformat_t fmt) { if (s_cam_mutex) { xSemaphoreTake(s_cam_mutex, portMAX_DELAY); } s_cam_config = (camera_config_t){ .pin_pwdn = CAM_PIN_PWDN, .pin_reset = CAM_PIN_RESET, .pin_xclk = CAM_PIN_XCLK, .pin_sccb_sda = CAM_PIN_SIOD, .pin_sccb_scl = CAM_PIN_SIOC, .pin_d7 = CAM_PIN_D7, .pin_d6 = CAM_PIN_D6, .pin_d5 = CAM_PIN_D5, .pin_d4 = CAM_PIN_D4, .pin_d3 = CAM_PIN_D3, .pin_d2 = CAM_PIN_D2, .pin_d1 = CAM_PIN_D1, .pin_d0 = CAM_PIN_D0, .pin_vsync = CAM_PIN_VSYNC, .pin_href = CAM_PIN_HREF, .pin_pclk = CAM_PIN_PCLK, .xclk_freq_hz = 20000000, .ledc_timer = LEDC_TIMER_0, .ledc_channel = LEDC_CHANNEL_0, .pixel_format = fmt, .frame_size = CAM_FRAME_SIZE, .jpeg_quality = CAM_JPEG_QUALITY, .fb_count = 2, .fb_location = CAMERA_FB_IN_PSRAM, .grab_mode = CAMERA_GRAB_LATEST, }; if (s_camera_ok) { esp_camera_deinit(); s_camera_ok = false; } esp_err_t err = esp_camera_init(&s_cam_config); if (err != ESP_OK) { ESP_LOGE(TAG, "Camera init failed (%s): %s", fmt == PIXFORMAT_RGB565 ? "RGB565" : "JPEG", esp_err_to_name(err)); if (s_cam_mutex) { xSemaphoreGive(s_cam_mutex); } return err; } sensor_t *sensor = esp_camera_sensor_get(); if (sensor) { ESP_LOGI(TAG, "Camera PID=0x%04x format=%s", sensor->id.PID, fmt == PIXFORMAT_RGB565 ? "RGB565" : "JPEG"); sensor->set_vflip(sensor, 1); sensor->set_hmirror(sensor, 0); } s_camera_ok = true; s_camera_pix_fmt = fmt; ESP_LOGI(TAG, "Camera ready"); if (s_cam_mutex) { xSemaphoreGive(s_cam_mutex); } return ESP_OK; } static camera_fb_t *camera_capture(void) { if (s_cam_mutex) { xSemaphoreTake(s_cam_mutex, portMAX_DELAY); } camera_fb_t *fb = s_camera_ok ? esp_camera_fb_get() : NULL; if (s_cam_mutex) { xSemaphoreGive(s_cam_mutex); } return fb; } static void camera_release(camera_fb_t *fb) { if (!fb) { return; } if (s_cam_mutex) { xSemaphoreTake(s_cam_mutex, portMAX_DELAY); } esp_camera_fb_return(fb); if (s_cam_mutex) { xSemaphoreGive(s_cam_mutex); } } static esp_err_t capture_send_jpeg(httpd_req_t *req, const camera_fb_t *fb) { uint8_t *jpg_buf = NULL; size_t jpg_len = 0; vision_stats_t stats = {0}; esp_err_t enc = vision_encode_capture_jpeg(fb, CAM_JPEG_QUALITY, &jpg_buf, &jpg_len, &stats); if (enc != ESP_OK || !jpg_buf || jpg_len == 0) { ESP_LOGW(TAG, "capture encode failed: %s", esp_err_to_name(enc)); free(jpg_buf); return ESP_FAIL; } set_no_cache(req); httpd_resp_set_type(req, "image/jpeg"); if (vision_local_enabled()) { vision_add_capture_headers(req, &stats); } esp_err_t res = httpd_resp_send(req, (const char *)jpg_buf, jpg_len); free(jpg_buf); return res; } static camera_fb_t *camera_snapshot_copy(camera_fb_t *snap_out) { camera_fb_t *fb = camera_capture(); if (!fb) { return NULL; } *snap_out = *fb; if (fb->format != PIXFORMAT_RGB565 && fb->format != PIXFORMAT_JPEG) { camera_release(fb); return NULL; } uint8_t *copy = heap_caps_malloc(fb->len, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (!copy) { copy = malloc(fb->len); } if (!copy) { camera_release(fb); return NULL; } memcpy(copy, fb->buf, fb->len); snap_out->buf = copy; camera_release(fb); return snap_out; } static void camera_snapshot_free(camera_fb_t *snap) { if (snap && snap->buf) { heap_caps_free(snap->buf); snap->buf = NULL; } } static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { if (s_retry_num < WIFI_MAX_RETRY) { esp_wifi_connect(); s_retry_num++; ESP_LOGW(TAG, "Retry Wi‑Fi connect (%d/%d)", s_retry_num, WIFI_MAX_RETRY); } else { xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); } } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip)); ESP_LOGI(TAG, "Open http://" IPSTR "/", IP2STR(&event->ip_info.ip)); s_retry_num = 0; xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); } } static void load_wifi_credentials(char *ssid, size_t ssid_len, char *pass, size_t pass_len) { if (s_wifi_cred_from_i2c && s_wifi_ssid[0] != '\0') { strncpy(ssid, s_wifi_ssid, ssid_len - 1); ssid[ssid_len - 1] = '\0'; strncpy(pass, s_wifi_pass, pass_len - 1); pass[pass_len - 1] = '\0'; return; } strncpy(ssid, WIFI_SSID, ssid_len - 1); ssid[ssid_len - 1] = '\0'; strncpy(pass, WIFI_PASS, pass_len - 1); pass[pass_len - 1] = '\0'; nvs_handle_t nvs; if (nvs_open("wifi", NVS_READONLY, &nvs) != ESP_OK) { return; } size_t len = ssid_len; if (nvs_get_str(nvs, "ssid", ssid, &len) == ESP_OK) { ESP_LOGI(TAG, "Wi‑Fi SSID from NVS"); } len = pass_len; if (nvs_get_str(nvs, "pass", pass, &len) != ESP_OK) { /* keep default pass if missing */ } nvs_close(nvs); } static esp_err_t wifi_driver_init_once(void) { if (s_wifi_netif_ready) { return ESP_OK; } s_wifi_event_group = xEventGroupCreate(); ESP_RETURN_ON_ERROR(esp_netif_init(), TAG, "netif"); ESP_RETURN_ON_ERROR(esp_event_loop_create_default(), TAG, "event loop"); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_RETURN_ON_ERROR(esp_wifi_init(&cfg), TAG, "wifi_init"); ESP_RETURN_ON_ERROR(esp_event_handler_instance_register( WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL), TAG, "wifi event"); ESP_RETURN_ON_ERROR(esp_event_handler_instance_register( IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL), TAG, "ip event"); s_wifi_netif_ready = true; return ESP_OK; } static esp_err_t wifi_start_sta(void) { ESP_RETURN_ON_ERROR(wifi_driver_init_once(), TAG, "wifi_driver_init_once"); char ssid[33]; char pass[65]; load_wifi_credentials(ssid, sizeof(ssid), pass, sizeof(pass)); wifi_config_t wifi_config = {0}; strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid)); strncpy((char *)wifi_config.sta.password, pass, sizeof(wifi_config.sta.password)); wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; s_retry_num = 0; xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT); ESP_RETURN_ON_ERROR(esp_wifi_set_mode(WIFI_MODE_STA), TAG, "set_mode"); ESP_RETURN_ON_ERROR(esp_wifi_set_config(WIFI_IF_STA, &wifi_config), TAG, "set_config"); ESP_RETURN_ON_ERROR(esp_wifi_start(), TAG, "wifi_start"); ESP_RETURN_ON_ERROR(esp_wifi_set_ps(WIFI_PS_NONE), TAG, "set_ps"); eye_i2c_load_wifi_bufs(ssid, pass); ESP_LOGI(TAG, "Connecting to Wi‑Fi SSID:%s ...", ssid); EventBits_t bits = xEventGroupWaitBits( s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, pdMS_TO_TICKS(20000)); if (bits & WIFI_CONNECTED_BIT) { s_wifi_driver_up = true; return ESP_OK; } ESP_LOGW(TAG, "Wi‑Fi not connected (timeout/fail) — check WIFI_SSID / WIFI_PASS"); s_wifi_driver_up = true; /* driver running; STATUS_WIFI stays clear until IP */ return ESP_ERR_TIMEOUT; } static void wifi_stop_sta(void) { if (s_httpd) { httpd_stop(s_httpd); s_httpd = NULL; ESP_LOGI(TAG, "HTTP server stopped"); } if (s_wifi_netif_ready) { esp_wifi_stop(); s_retry_num = 0; if (s_wifi_event_group) { xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT); } } s_wifi_driver_up = false; ESP_LOGI(TAG, "Wi‑Fi stopped"); } static void set_no_cache(httpd_req_t *req) { httpd_resp_set_hdr(req, "Cache-Control", "no-store, no-cache, must-revalidate"); httpd_resp_set_hdr(req, "Pragma", "no-cache"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); } static esp_err_t index_handler(httpd_req_t *req) { set_no_cache(req); httpd_resp_set_type(req, "text/html"); const size_t len = (size_t)(index_html_end - index_html_start); return httpd_resp_send(req, (const char *)index_html_start, len); } static esp_err_t capture_handler(httpd_req_t *req) { camera_fb_t snap = {0}; if (!camera_snapshot_copy(&snap)) { httpd_resp_send_500(req); return ESP_FAIL; } esp_err_t res = capture_send_jpeg(req, &snap); camera_snapshot_free(&snap); return res; } static esp_err_t stream_handler(httpd_req_t *req) { esp_err_t res = ESP_OK; char part_buf[64]; const int64_t frame_period_us = 1000000 / STREAM_FPS; res = httpd_resp_set_type(req, STREAM_CONTENT_TYPE); if (res != ESP_OK) { return res; } set_no_cache(req); while (true) { int64_t frame_start = esp_timer_get_time(); camera_fb_t snap = {0}; if (!camera_snapshot_copy(&snap)) { ESP_LOGE(TAG, "Camera capture failed"); res = ESP_FAIL; break; } uint8_t *jpg_buf = NULL; size_t jpg_len = 0; if (vision_encode_capture_jpeg(&snap, CAM_JPEG_QUALITY, &jpg_buf, &jpg_len, NULL) != ESP_OK || !jpg_buf) { camera_snapshot_free(&snap); res = ESP_FAIL; break; } camera_snapshot_free(&snap); res = httpd_resp_send_chunk(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY)); if (res == ESP_OK) { int hlen = snprintf(part_buf, sizeof(part_buf), STREAM_PART, (unsigned)jpg_len); if (hlen < 0 || hlen >= (int)sizeof(part_buf)) { res = ESP_FAIL; } else { res = httpd_resp_send_chunk(req, part_buf, (size_t)hlen); } } if (res == ESP_OK) { res = httpd_resp_send_chunk(req, (const char *)jpg_buf, jpg_len); } free(jpg_buf); if (res != ESP_OK) { break; } int64_t elapsed = esp_timer_get_time() - frame_start; int64_t sleep_us = frame_period_us - elapsed; if (sleep_us > 1000) { vTaskDelay(pdMS_TO_TICKS((uint32_t)(sleep_us / 1000))); } } return res; } static esp_err_t send_json_ack(httpd_req_t *req, const robot_ack_t *ack, bool as_error) { char json[384]; int n = robot_link_format_ack(ack, json, sizeof(json)); if (n < 0 || n >= (int)sizeof(json)) { httpd_resp_send_500(req); return ESP_FAIL; } set_no_cache(req); httpd_resp_set_type(req, "application/json"); if (as_error) { httpd_resp_set_status(req, HTTPD_400); } return httpd_resp_send(req, json, n); } /* Match "key": so short keys like "h" do not hit "h_tol". */ static const char *json_find_key(const char *json, const char *key) { char pattern[40]; snprintf(pattern, sizeof(pattern), "\"%s\":", key); return strstr(json, pattern); } static bool json_get_string(const char *json, const char *key, char *out, size_t outlen) { const char *p = json_find_key(json, key); if (!p) { return false; } p = strchr(p, ':'); if (!p) { return false; } p++; while (*p == ' ' || *p == '\t') { p++; } if (*p != '"') { return false; } p++; size_t i = 0; while (*p && *p != '"' && i + 1 < outlen) { if (*p == '\\' && p[1]) { p++; } out[i++] = *p++; } out[i] = '\0'; return i > 0 || (p && *p == '"'); } static bool json_get_bool(const char *json, const char *key, bool *out) { const char *p = json_find_key(json, key); if (!p) { return false; } p = strchr(p, ':'); if (!p) { return false; } p++; while (*p == ' ' || *p == '\t') { p++; } if (strncmp(p, "true", 4) == 0) { *out = true; return true; } if (strncmp(p, "false", 5) == 0) { *out = false; return true; } return false; } static bool json_get_int(const char *json, const char *key, int *out) { const char *p = json_find_key(json, key); if (!p) { return false; } p = strchr(p, ':'); if (!p) { return false; } p++; while (*p == ' ' || *p == '\t') { p++; } char *end = NULL; long v = strtol(p, &end, 10); if (end == p) { return false; } *out = (int)v; return true; } static esp_err_t ws_handler(httpd_req_t *req) { if (req->method == HTTP_GET) { ESP_LOGI(TAG, "WebSocket /ws connected"); return ESP_OK; } httpd_ws_frame_t ws_pkt; memset(&ws_pkt, 0, sizeof(ws_pkt)); ws_pkt.type = HTTPD_WS_TYPE_TEXT; esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); if (ret != ESP_OK) { return ret; } if (ws_pkt.len == 0 || ws_pkt.len > 512) { return ESP_OK; } uint8_t *buf = calloc(1, ws_pkt.len + 1); if (!buf) { return ESP_ERR_NO_MEM; } ws_pkt.payload = buf; ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); if (ret != ESP_OK) { free(buf); return ret; } char type[16] = {0}; char dir[16] = {0}; char text[256] = {0}; robot_ack_t ack; memset(&ack, 0, sizeof(ack)); json_get_string((const char *)buf, "type", type, sizeof(type)); if (strcmp(type, "cmd") == 0) { json_get_string((const char *)buf, "dir", dir, sizeof(dir)); robot_link_handle_cmd(dir, &ack); } else if (strcmp(type, "msg") == 0) { json_get_string((const char *)buf, "text", text, sizeof(text)); robot_link_handle_msg(text, strlen(text), &ack); } else if (strcmp(type, "vision") == 0) { char json[512]; bool do_get = false; (void)json_get_bool((const char *)buf, "get", &do_get); if (do_get) { /* Read-only: never touch camera format or params. */ vision_format_ack(json, sizeof(json)); free(buf); httpd_ws_frame_t reply = { .final = true, .fragmented = false, .type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)json, .len = strlen(json), }; return httpd_ws_send_frame(req, &reply); } bool local = false; bool show_mask = false; bool has_local = json_get_bool((const char *)buf, "local", &local); bool has_show_mask = json_get_bool((const char *)buf, "show_mask", &show_mask); char method[16] = {0}; bool has_method = json_get_string((const char *)buf, "method", method, sizeof(method)); int h = 0, s = 0, v = 0, h_tol = 0, s_tol = 0, v_tol = 0; int b = 0, g = 0, r = 0; int pick_x = 0, pick_y = 0; int coarse_stride = 0, min_area = 0; bool mask_full = false, morph_roi = false; bool has_h = json_get_int((const char *)buf, "h", &h); bool has_s = json_get_int((const char *)buf, "s", &s); bool has_v = json_get_int((const char *)buf, "v", &v); bool has_h_tol = json_get_int((const char *)buf, "h_tol", &h_tol); bool has_s_tol = json_get_int((const char *)buf, "s_tol", &s_tol); bool has_v_tol = json_get_int((const char *)buf, "v_tol", &v_tol); bool has_b = json_get_int((const char *)buf, "b", &b); bool has_g = json_get_int((const char *)buf, "g", &g); bool has_r = json_get_int((const char *)buf, "r", &r); bool has_pick_x = json_get_int((const char *)buf, "pick_x", &pick_x); bool has_pick_y = json_get_int((const char *)buf, "pick_y", &pick_y); bool has_coarse_stride = json_get_int((const char *)buf, "coarse_stride", &coarse_stride); bool has_min_area = json_get_int((const char *)buf, "min_area", &min_area); bool has_mask_full = json_get_bool((const char *)buf, "mask_full", &mask_full); bool has_morph_roi = json_get_bool((const char *)buf, "morph_roi", &morph_roi); bool target_local = has_local ? local : vision_local_enabled(); pixformat_t want_fmt = target_local ? PIXFORMAT_RGB565 : PIXFORMAT_JPEG; if (want_fmt != s_camera_pix_fmt) { ESP_LOGI(TAG, "Switching camera for local vision -> %s", want_fmt == PIXFORMAT_RGB565 ? "RGB565" : "JPEG"); esp_err_t cam_err = init_camera_with_format(want_fmt); if (cam_err != ESP_OK) { snprintf( json, sizeof(json), "{\"ok\":false,\"type\":\"vision\",\"error\":\"camera_reinit\"}"); free(buf); httpd_ws_frame_t reply = { .final = true, .fragmented = false, .type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)json, .len = strlen(json), }; return httpd_ws_send_frame(req, &reply); } } if (target_local && has_pick_x && has_pick_y) { has_h = false; has_s = false; has_v = false; } vision_handle_update( has_local, local, has_show_mask, show_mask, has_method, method, has_h, h, has_s, s, has_v, v, has_h_tol, h_tol, has_s_tol, s_tol, has_v_tol, v_tol, has_b, b, has_g, g, has_r, r, has_coarse_stride, coarse_stride, has_min_area, min_area, has_mask_full, mask_full, has_morph_roi, morph_roi, json, sizeof(json)); if (target_local && has_pick_x && has_pick_y) { camera_fb_t snap = {0}; if (camera_snapshot_copy(&snap)) { if (vision_sample_hsv_from_frame(&snap, pick_x, pick_y) == ESP_OK) { vision_format_ack(json, sizeof(json)); } camera_snapshot_free(&snap); } } free(buf); httpd_ws_frame_t reply = { .final = true, .fragmented = false, .type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)json, .len = strlen(json), }; return httpd_ws_send_frame(req, &reply); } else { ack.ok = false; ack.seq = 0; ack.type = "cmd"; ack.fwd = "none"; ack.error = "bad_type"; } char json[384]; int n = robot_link_format_ack(&ack, json, sizeof(json)); free(buf); if (n < 0) { return ESP_FAIL; } httpd_ws_frame_t reply = { .final = true, .fragmented = false, .type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)json, .len = (size_t)n, }; return httpd_ws_send_frame(req, &reply); } static esp_err_t cmd_handler(httpd_req_t *req) { char query[64] = {0}; char dir[16] = {0}; if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { httpd_query_key_value(query, "dir", dir, sizeof(dir)); } robot_ack_t ack; esp_err_t err = robot_link_handle_cmd(dir, &ack); return send_json_ack(req, &ack, err != ESP_OK); } static esp_err_t msg_handler(httpd_req_t *req) { if (req->content_len <= 0 || req->content_len > 512) { robot_ack_t ack = { .ok = false, .seq = 0, .type = "msg", .fwd = "none", .error = "bad_length", }; return send_json_ack(req, &ack, true); } char buf[513]; int received = httpd_req_recv(req, buf, req->content_len); if (received <= 0) { httpd_resp_send_500(req); return ESP_FAIL; } buf[received] = '\0'; robot_ack_t ack; esp_err_t err = robot_link_handle_msg(buf, (size_t)received, &ack); return send_json_ack(req, &ack, err != ESP_OK); } static esp_err_t vision_handler(httpd_req_t *req) { camera_fb_t snap = {0}; if (!camera_snapshot_copy(&snap)) { httpd_resp_send_500(req); return ESP_FAIL; } vision_stats_t stats = {0}; esp_err_t err = vision_detect_on_frame(&snap, &stats); camera_snapshot_free(&snap); if (err != ESP_OK) { httpd_resp_send_500(req); return ESP_FAIL; } char json[320]; vision_format_stats_json(&stats, json, sizeof(json)); set_no_cache(req); httpd_resp_set_type(req, "application/json"); return httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN); } static esp_err_t vision_settings_handler(httpd_req_t *req) { char json[512]; vision_format_ack(json, sizeof(json)); set_no_cache(req); httpd_resp_set_type(req, "application/json"); return httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN); } static httpd_handle_t start_webserver(void) { if (s_httpd) { return s_httpd; } httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = 80; config.lru_purge_enable = true; config.stack_size = 20480; config.max_uri_handlers = 12; config.max_resp_headers = 16; httpd_handle_t server = NULL; if (httpd_start(&server, &config) != ESP_OK) { ESP_LOGE(TAG, "Failed to start HTTP server"); return NULL; } const httpd_uri_t index_uri = { .uri = "/", .method = HTTP_GET, .handler = index_handler}; const httpd_uri_t capture_uri = { .uri = "/capture", .method = HTTP_GET, .handler = capture_handler}; const httpd_uri_t vision_uri = { .uri = "/vision", .method = HTTP_GET, .handler = vision_handler}; const httpd_uri_t vision_settings_uri = { .uri = "/vision/settings", .method = HTTP_GET, .handler = vision_settings_handler}; const httpd_uri_t stream_uri = { .uri = "/stream", .method = HTTP_GET, .handler = stream_handler}; const httpd_uri_t cmd_uri = { .uri = "/cmd", .method = HTTP_GET, .handler = cmd_handler}; const httpd_uri_t msg_uri = { .uri = "/msg", .method = HTTP_POST, .handler = msg_handler}; const httpd_uri_t ws_uri = { .uri = "/ws", .method = HTTP_GET, .handler = ws_handler, .is_websocket = true, }; httpd_register_uri_handler(server, &index_uri); httpd_register_uri_handler(server, &capture_uri); httpd_register_uri_handler(server, &vision_uri); httpd_register_uri_handler(server, &vision_settings_uri); httpd_register_uri_handler(server, &stream_uri); httpd_register_uri_handler(server, &cmd_uri); httpd_register_uri_handler(server, &msg_uri); httpd_register_uri_handler(server, &ws_uri); ESP_LOGI(TAG, "HTTP+WS server started (/ws for commands)"); s_httpd = server; return server; } esp_err_t board_camera_set(bool enable) { s_want_camera = enable; return ESP_OK; } esp_err_t board_wifi_set(bool enable) { s_want_wifi = enable; return ESP_OK; } esp_err_t board_wifi_set_credentials(const char *ssid, const char *password) { if (!ssid || ssid[0] == '\0') { return ESP_ERR_INVALID_ARG; } memset(s_wifi_ssid, 0, sizeof(s_wifi_ssid)); memset(s_wifi_pass, 0, sizeof(s_wifi_pass)); strncpy(s_wifi_ssid, ssid, sizeof(s_wifi_ssid) - 1); if (password) { strncpy(s_wifi_pass, password, sizeof(s_wifi_pass) - 1); } s_wifi_cred_from_i2c = true; if (s_wifi_driver_up) { s_wifi_restart = true; } /* Persist so a later CMD 7 / reboot path can reuse them */ nvs_handle_t nvs; if (nvs_open("wifi", NVS_READWRITE, &nvs) == ESP_OK) { (void)nvs_set_str(nvs, "ssid", s_wifi_ssid); (void)nvs_set_str(nvs, "pass", s_wifi_pass); (void)nvs_commit(nvs); nvs_close(nvs); } return ESP_OK; } esp_err_t board_camera_sync_format(void) { if (!s_camera_ok) { return ESP_ERR_INVALID_STATE; } pixformat_t want = vision_local_enabled() ? PIXFORMAT_RGB565 : PIXFORMAT_JPEG; if (want == s_camera_pix_fmt) { return ESP_OK; } ESP_LOGI(TAG, "Switching camera format -> %s", want == PIXFORMAT_RGB565 ? "RGB565" : "JPEG"); return init_camera_with_format(want); } bool board_camera_is_on(void) { return s_camera_ok; } bool board_wifi_is_on(void) { return s_wifi_driver_up; } static void apply_camera_want(void) { if (s_want_camera && !s_camera_ok) { /* RGB565 so I2C vision works without Wi‑Fi */ if (init_camera_with_format(PIXFORMAT_RGB565) != ESP_OK) { ESP_LOGE(TAG, "Camera enable failed"); s_want_camera = false; } } else if (!s_want_camera && s_camera_ok) { if (s_cam_mutex) { xSemaphoreTake(s_cam_mutex, portMAX_DELAY); } esp_camera_deinit(); s_camera_ok = false; if (s_cam_mutex) { xSemaphoreGive(s_cam_mutex); } ESP_LOGI(TAG, "Camera off"); } /* Format switches only via board_camera_sync_format() on FLAGS/WS — not every poll. */ } static void apply_wifi_want(void) { if (s_wifi_restart && s_wifi_driver_up) { ESP_LOGI(TAG, "Restarting Wi‑Fi with new credentials"); wifi_stop_sta(); s_wifi_restart = false; } if (s_want_wifi && !s_wifi_driver_up) { esp_err_t err = wifi_start_sta(); if (err == ESP_OK || err == ESP_ERR_TIMEOUT) { if (!s_httpd) { start_webserver(); } } else { ESP_LOGE(TAG, "Wi‑Fi enable failed: %s", esp_err_to_name(err)); s_want_wifi = false; } } else if (!s_want_wifi && s_wifi_driver_up) { wifi_stop_sta(); } } static void power_task(void *arg) { (void)arg; while (true) { apply_camera_want(); apply_wifi_want(); vTaskDelay(pdMS_TO_TICKS(50)); } } static void vision_loop_task(void *arg) { (void)arg; while (true) { if (!s_camera_ok || !vision_local_enabled()) { vTaskDelay(pdMS_TO_TICKS(40)); continue; } camera_fb_t snap = {0}; if (camera_snapshot_copy(&snap)) { vision_stats_t stats = {0}; (void)vision_detect_on_frame(&snap, &stats); camera_snapshot_free(&snap); } /* Detection dominates period; short yield for I2C/HTTP */ vTaskDelay(pdMS_TO_TICKS(5)); } } void app_main(void) { esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ESP_ERROR_CHECK(nvs_flash_init()); } s_cam_mutex = xSemaphoreCreateMutex(); if (status_led_init() != ESP_OK) { ESP_LOGW(TAG, "Status LED init failed"); } vision_init(); robot_link_init(); eye_i2c_set_cam_mutex(s_cam_mutex); if (eye_i2c_init() != ESP_OK) { ESP_LOGW(TAG, "I2C slave init failed"); } xTaskCreate(power_task, "eye_power", 8192, NULL, 5, NULL); xTaskCreate(vision_loop_task, "eye_vision", 8192, NULL, 4, NULL); ESP_LOGI(TAG, "Boot: camera+Wi‑Fi off — I2C CMD 5=cam on, 6=off, 7=wifi on, 8=off"); }