#include "robot_link.h" #include #include #include #include #include "esp_camera.h" #include "esp_event.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 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"; /* * Browser MJPEG () buffers heavily. * Polling /capture with JS is much closer to real-time for viewing. * For robot code, prefer /capture or /stream from OpenCV/Python (not a browser). */ static const char INDEX_HTML[] = "" "linefollower-eye" "" "

OV3660 (low-latency)

" "\"stream\"" "

polling /capture — /stream still available

" ""; static esp_err_t init_camera(void) { camera_config_t config = { .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 = PIXFORMAT_JPEG, .frame_size = CAM_FRAME_SIZE, .jpeg_quality = CAM_JPEG_QUALITY, .fb_count = 2, .fb_location = CAMERA_FB_IN_PSRAM, .grab_mode = CAMERA_GRAB_LATEST, /* always newest frame */ }; esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { ESP_LOGE(TAG, "Camera init failed: %s", esp_err_to_name(err)); return err; } sensor_t *sensor = esp_camera_sensor_get(); if (sensor) { ESP_LOGI(TAG, "Camera PID=0x%04x", sensor->id.PID); sensor->set_vflip(sensor, 1); sensor->set_hmirror(sensor, 0); } ESP_LOGI(TAG, "Camera ready"); return ESP_OK; } 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 init_wifi_sta(void) { s_wifi_event_group = xEventGroupCreate(); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_event_handler_instance_register( WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL)); ESP_ERROR_CHECK(esp_event_handler_instance_register( IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL)); wifi_config_t wifi_config = {0}; strncpy((char *)wifi_config.sta.ssid, WIFI_SSID, sizeof(wifi_config.sta.ssid)); strncpy((char *)wifi_config.sta.password, WIFI_PASS, sizeof(wifi_config.sta.password)); wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); ESP_ERROR_CHECK(esp_wifi_start()); /* Modem sleep adds tens–hundreds of ms of jitter */ ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); ESP_LOGI(TAG, "Connecting to Wi‑Fi SSID:%s ...", WIFI_SSID); EventBits_t bits = xEventGroupWaitBits( s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY); if (bits & WIFI_FAIL_BIT) { ESP_LOGE(TAG, "Failed to connect to Wi‑Fi — check WIFI_SSID / WIFI_PASS"); } } 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"); return httpd_resp_send(req, INDEX_HTML, HTTPD_RESP_USE_STRLEN); } static esp_err_t capture_handler(httpd_req_t *req) { camera_fb_t *fb = esp_camera_fb_get(); if (!fb) { httpd_resp_send_500(req); return ESP_FAIL; } set_no_cache(req); httpd_resp_set_type(req, "image/jpeg"); esp_err_t res = httpd_resp_send(req, (const char *)fb->buf, fb->len); esp_camera_fb_return(fb); 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 *fb = esp_camera_fb_get(); if (!fb) { ESP_LOGE(TAG, "Camera capture failed"); res = ESP_FAIL; break; } if (fb->format != PIXFORMAT_JPEG) { esp_camera_fb_return(fb); ESP_LOGE(TAG, "Non-JPEG frame"); res = ESP_FAIL; break; } 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, fb->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 *)fb->buf, fb->len); } esp_camera_fb_return(fb); 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); } static bool json_get_string(const char *json, const char *key, char *out, size_t outlen) { char pattern[32]; snprintf(pattern, sizeof(pattern), "\"%s\"", key); const char *p = strstr(json, pattern); if (!p) { return false; } p = strchr(p + strlen(pattern), ':'); 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 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 { 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 httpd_handle_t start_webserver(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = 80; config.lru_purge_enable = true; config.stack_size = 8192; config.max_uri_handlers = 12; 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 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, &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)"); return server; } 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()); } if (init_camera() != ESP_OK) { ESP_LOGW(TAG, "Continuing without camera so Wi‑Fi stays up for debugging"); } robot_link_init(); init_wifi_sta(); start_webserver(); }