vision/main/main.c

497 lines
15 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 "robot_link.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#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 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 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 (<img src=/stream>) 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[] =
"<!DOCTYPE html><html><head><meta name=\"viewport\" "
"content=\"width=device-width,initial-scale=1\">"
"<title>linefollower-eye</title>"
"<style>body{margin:0;background:#111;color:#eee;font-family:sans-serif;"
"display:flex;flex-direction:column;align-items:center}"
"img{max-width:100%;height:auto}</style></head>"
"<body><h1>OV3660 (low-latency)</h1>"
"<img id=\"v\" alt=\"stream\">"
"<p style=\"opacity:.6;font-size:12px\">polling /capture — /stream still available</p>"
"<script>"
"const img=document.getElementById('v');"
"let busy=false,prev=null;"
"async function tick(){"
" if(busy)return; busy=true;"
" try{"
" const r=await fetch('/capture?t='+Date.now(),{cache:'no-store'});"
" if(!r.ok)throw 0;"
" const b=await r.blob();"
" const u=URL.createObjectURL(b);"
" img.src=u;"
" if(prev)URL.revokeObjectURL(prev);"
" prev=u;"
" }catch(e){}"
" busy=false;"
"}"
"setInterval(tick," STR(1000 / STREAM_FPS) ");"
"tick();"
"</script></body></html>";
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 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 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 tenshundreds of ms of jitter */
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
ESP_LOGI(TAG, "Connecting to WiFi 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 WiFi — 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 WiFi stays up for debugging");
}
robot_link_init();
init_wifi_sta();
start_webserver();
}