vision/main/main.c

1007 lines
30 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include "board_svc.h"
#include "eye_i2c.h"
#include "robot_link.h"
#include "status_led.h"
#include "vision.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#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 WiFi 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 WiFi 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, "WiFi 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 WiFi 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, "WiFi 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, "WiFi 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 WiFi */
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 WiFi 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, "WiFi 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+WiFi off — I2C CMD 5=cam on, 6=off, 7=wifi on, 8=off");
}