From 78ac54bff8f43318d84b4888f9ce50eae74978c2 Mon Sep 17 00:00:00 2001 From: Zdzislaw Goik Date: Wed, 13 Jul 2022 23:43:56 +0200 Subject: [PATCH] Rewrite the streaming back to http while keeping AsyncWebServer in charge of API, remove OTA update time limit (#19) --- ESP/include/GlobalVars.h | 1 + ESP/include/OTA.h | 14 +- ESP/include/streamServer.h | 22 +++ ESP/include/webServer/asyncJPGResponse.h | 163 ----------------------- ESP/include/webServer/webserverHandler.h | 20 --- ESP/include/webserverHandler.h | 21 +++ ESP/src/cameraHandler.cpp | 8 +- ESP/src/main.cpp | 15 ++- ESP/src/streamServer.cpp | 109 +++++++++++++++ ESP/src/webServer/webserverHandler.cpp | 94 ------------- ESP/src/webserverHandler.cpp | 39 ++++++ ESP/src/wifiHandler.cpp | 2 +- 12 files changed, 210 insertions(+), 298 deletions(-) create mode 100644 ESP/include/streamServer.h delete mode 100644 ESP/include/webServer/asyncJPGResponse.h delete mode 100644 ESP/include/webServer/webserverHandler.h create mode 100644 ESP/include/webserverHandler.h create mode 100644 ESP/src/streamServer.cpp delete mode 100644 ESP/src/webServer/webserverHandler.cpp create mode 100644 ESP/src/webserverHandler.cpp diff --git a/ESP/include/GlobalVars.h b/ESP/include/GlobalVars.h index fd4c5ae..f21b526 100644 --- a/ESP/include/GlobalVars.h +++ b/ESP/include/GlobalVars.h @@ -7,6 +7,7 @@ #include "cameraHandler.h" extern char *MDSNTrackerName; extern int STREAM_SERVER_PORT; +extern int CONTROL_SERVER_PORT; extern OpenIris::LEDManager ledManager; extern OpenIris::StateManager stateManager; diff --git a/ESP/include/OTA.h b/ESP/include/OTA.h index 009e0d0..d0d9f0d 100644 --- a/ESP/include/OTA.h +++ b/ESP/include/OTA.h @@ -6,8 +6,7 @@ namespace OpenIris class OTA { private: - unsigned long bootTimestamp = 0; - bool isOtaEnabled = true; + bool isOTAEnabled = false; public: void SetupOTA(const char *OTAPassword, uint16_t OTAServerPort) @@ -20,6 +19,7 @@ namespace OpenIris return; } ArduinoOTA.setPort(OTAServerPort); + isOTAEnabled = true; ArduinoOTA .onStart([]() @@ -44,20 +44,12 @@ namespace OpenIris log_i("Starting up basic OTA server"); log_i("OTA will be live for 30s, after which it will be disabled until restart"); ArduinoOTA.begin(); - bootTimestamp = millis(); } void HandleOTAUpdate() { - if (isOtaEnabled) + if (isOTAEnabled) { - if (bootTimestamp + 30000 < millis()) - { - // we're disabling ota after first 30sec so that nothing bad happens during playtime - isOtaEnabled = false; - log_i("From now on, OTA is disabled"); - return; - } ArduinoOTA.handle(); } } diff --git a/ESP/include/streamServer.h b/ESP/include/streamServer.h new file mode 100644 index 0000000..b2a7af3 --- /dev/null +++ b/ESP/include/streamServer.h @@ -0,0 +1,22 @@ +#pragma once +#define PART_BOUNDARY "123456789000000000000987654321" +#include +#include "esp_camera.h" +#include "esp_http_server.h" + +namespace OpenIris +{ + namespace StreamHelpers + { + esp_err_t stream(httpd_req_t *req); + } + class StreamServer + { + + private: + httpd_handle_t camera_stream = nullptr; + + public: + int startStreamServer(); + }; +} \ No newline at end of file diff --git a/ESP/include/webServer/asyncJPGResponse.h b/ESP/include/webServer/asyncJPGResponse.h deleted file mode 100644 index 9287bc3..0000000 --- a/ESP/include/webServer/asyncJPGResponse.h +++ /dev/null @@ -1,163 +0,0 @@ -#pragma once -#include -#include -#include "esp_camera.h" - -#define PART_BOUNDARY "123456789000000000000987654321" - -namespace OpenIris -{ - namespace WebServerHelpers - { - 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: %s\r\nContent-Length: %u\r\n\r\n"; - - static const char *JPG_CONTENT_TYPE = "image/jpeg"; - } - - typedef struct - { - camera_fb_t *fb; - size_t index; - } camera_frame_t; - - class AsyncJpegStreamResponse : public AsyncAbstractResponse - { - private: - camera_frame_t _frame; - size_t _index; - size_t _jpg_buf_len; - uint8_t *_jpg_buf; - long lastAsyncRequest; - - public: - AsyncJpegStreamResponse() - { - _callback = nullptr; - _code = 200; - _contentLength = 0; - _contentType = OpenIris::WebServerHelpers::STREAM_CONTENT_TYPE; - _sendContentLength = false; - _chunked = true; - _index = 0; - _jpg_buf_len = 0; - _jpg_buf = NULL; - lastAsyncRequest = 0; - memset(&_frame, 0, sizeof(camera_frame_t)); - } - ~AsyncJpegStreamResponse() - { - if (_frame.fb) - { - if (_frame.fb->format != PIXFORMAT_JPEG) - { - free(_jpg_buf); - } - esp_camera_fb_return(_frame.fb); - } - } - bool _sourceValid() const - { - return true; - } - virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override - { - size_t ret = _content(buf, maxLen, _index); - if (ret != RESPONSE_TRY_AGAIN) - { - _index += ret; - } - return ret; - } - - size_t _content(uint8_t *buffer, size_t maxLen, size_t index) - { - if (!_frame.fb || _frame.index == _jpg_buf_len) - { - if (index && _frame.fb) - { - long end = millis(); - int fp = (end - lastAsyncRequest); - log_d("Size: %uKB, Time: %ums (%ifps)\n", _jpg_buf_len / 1024, fp, 1000 / fp); - lastAsyncRequest = end; - if (_frame.fb->format != PIXFORMAT_JPEG) - { - free(_jpg_buf); - } - esp_camera_fb_return(_frame.fb); - _frame.fb = NULL; - _jpg_buf_len = 0; - _jpg_buf = NULL; - } - if (maxLen < (strlen(OpenIris::WebServerHelpers::STREAM_BOUNDARY) + strlen(OpenIris::WebServerHelpers::STREAM_PART) + strlen(OpenIris::WebServerHelpers::JPG_CONTENT_TYPE) + 8)) - { - // log_w("Not enough space for headers"); - return RESPONSE_TRY_AGAIN; - } - // get frame - _frame.index = 0; - - _frame.fb = esp_camera_fb_get(); - if (_frame.fb == NULL) - { - log_e("Camera frame failed"); - return 0; - } - - if (_frame.fb->format != PIXFORMAT_JPEG) - { - unsigned long st = millis(); - bool jpeg_converted = frame2jpg(_frame.fb, 40, &_jpg_buf, &_jpg_buf_len); - if (!jpeg_converted) - { - log_e("JPEG compression failed"); - esp_camera_fb_return(_frame.fb); - _frame.fb = NULL; - _jpg_buf_len = 0; - _jpg_buf = NULL; - return 0; - } - log_i("JPEG: %lums, %uB", millis() - st, _jpg_buf_len); - } - else - { - _jpg_buf_len = _frame.fb->len; - _jpg_buf = _frame.fb->buf; - } - - // send boundary - size_t blen = 0; - if (index) - { - blen = strlen(OpenIris::WebServerHelpers::STREAM_BOUNDARY); - memcpy(buffer, OpenIris::WebServerHelpers::STREAM_BOUNDARY, blen); - buffer += blen; - } - // send header - size_t hlen = sprintf((char *)buffer, OpenIris::WebServerHelpers::STREAM_PART, OpenIris::WebServerHelpers::JPG_CONTENT_TYPE, _jpg_buf_len); - buffer += hlen; - // send frame - hlen = maxLen - hlen - blen; - if (hlen > _jpg_buf_len) - { - maxLen -= hlen - _jpg_buf_len; - hlen = _jpg_buf_len; - } - memcpy(buffer, _jpg_buf, hlen); - _frame.index += hlen; - return maxLen; - } - - size_t available = _jpg_buf_len - _frame.index; - if (maxLen > available) - { - maxLen = available; - } - memcpy(buffer, _jpg_buf + _frame.index, maxLen); - _frame.index += maxLen; - - return maxLen; - } - }; -} diff --git a/ESP/include/webServer/webserverHandler.h b/ESP/include/webServer/webserverHandler.h deleted file mode 100644 index 0d87c4a..0000000 --- a/ESP/include/webServer/webserverHandler.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once -#include -#include -#include "esp_camera.h" - -namespace OpenIris -{ - class HTTPDHandler - { - private: - void command_handler(AsyncWebServerRequest *request); - void stream_handler(AsyncWebServerRequest *request); - void roi_crop_command_handler(AsyncWebServerRequest *request); - AsyncWebServer *server; - - public: - HTTPDHandler(); - void startStreamServer(); - }; -} \ No newline at end of file diff --git a/ESP/include/webserverHandler.h b/ESP/include/webserverHandler.h new file mode 100644 index 0000000..57c9795 --- /dev/null +++ b/ESP/include/webserverHandler.h @@ -0,0 +1,21 @@ +#pragma once + +#define WEBSERVER_H +#define HTTP_ANY 0b01111111 +#define HTTP_GET 0b00000001 +#include +#include + +namespace OpenIris +{ + class APIServer + { + private: + void command_handler(AsyncWebServerRequest *request); + AsyncWebServer *server; + + public: + APIServer(); + void startAPIServer(); + }; +} \ No newline at end of file diff --git a/ESP/src/cameraHandler.cpp b/ESP/src/cameraHandler.cpp index 4eda03d..e470983 100644 --- a/ESP/src/cameraHandler.cpp +++ b/ESP/src/cameraHandler.cpp @@ -22,9 +22,9 @@ int OpenIris::CameraHandler::setupCamera() config.pin_sscb_scl = SIOC_GPIO_NUM; config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM; - config.xclk_freq_hz = 20000000; //10000000 stable, - //16500000 optimal, - //20000000 max fps + config.xclk_freq_hz = 20000000; // 10000000 stable, + // 16500000 optimal, + // 20000000 max fps config.pixel_format = PIXFORMAT_JPEG; if (psramFound()) @@ -52,7 +52,7 @@ int OpenIris::CameraHandler::setupCamera() camera_sensor->set_contrast(camera_sensor, 2); // -2 to 2 camera_sensor->set_saturation(camera_sensor, -2); // -2 to 2 camera_sensor->set_whitebal(camera_sensor, 1); // 0 = disable , 1 = enable - camera_sensor->set_awb_gain(camera_sensor, 1); // 0 = disable , 1 = enable + camera_sensor->set_awb_gain(camera_sensor, 1); // 0 = disable , 1 = enable camera_sensor->set_wb_mode(camera_sensor, 0); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home) camera_sensor->set_exposure_ctrl(camera_sensor, 1); // 0 = disable , 1 = enable camera_sensor->set_aec2(camera_sensor, 0); // 0 = disable , 1 = enable diff --git a/ESP/src/main.cpp b/ESP/src/main.cpp index 8bd2ee9..85ff0d2 100644 --- a/ESP/src/main.cpp +++ b/ESP/src/main.cpp @@ -5,29 +5,34 @@ #include "MDNSManager.h" #include "cameraHandler.h" #include "LEDManager.h" -#include "webServer/webserverHandler.h" +#include "streamServer.h" +#include "webserverHandler.h" + #include "OTA.h" #include "StateManager.h" -char *MDSNTrackerName = "OpenIrisTracker"; +const char *MDSNTrackerName = "OpenIrisTracker"; + int STREAM_SERVER_PORT = 80; +int CONTROL_SERVER_PORT = 81; auto ota = OpenIris::OTA(); auto ledManager = OpenIris::LEDManager(33); auto cameraHandler = OpenIris::CameraHandler(); auto stateManager = OpenIris::StateManager(); -auto httpdHandler = OpenIris::HTTPDHandler(); +auto apiServer = OpenIris::APIServer(); +auto streamServer = OpenIris::StreamServer(); void setup() { Serial.begin(115200); Serial.setDebugOutput(true); - Serial.println(); ledManager.setupLED(); cameraHandler.setupCamera(); OpenIris::WiFiHandler::setupWifi(ssid, password); OpenIris::MDNSHandler::setupMDNS(); - httpdHandler.startStreamServer(); + apiServer.startAPIServer(); + streamServer.startStreamServer(); ledManager.on(); ota.SetupOTA(OTAPassword, OTAServerPort); diff --git a/ESP/src/streamServer.cpp b/ESP/src/streamServer.cpp new file mode 100644 index 0000000..f566ba8 --- /dev/null +++ b/ESP/src/streamServer.cpp @@ -0,0 +1,109 @@ +#include "streamServer.h" +#include "GlobalVars.h" + +constexpr static char *STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY; +constexpr static char *STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n"; +constexpr static char *STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\nX-Timestamp: %d.%06d\r\n\r\n"; + +esp_err_t OpenIris::StreamHelpers::stream(httpd_req_t *req) +{ + long last_request_time = 0; + camera_fb_t *fb = NULL; + struct timeval _timestamp; + + esp_err_t res = ESP_OK; + + size_t _jpg_buf_len = 0; + uint8_t *_jpg_buf = NULL; + + char *part_buf[128]; + + static int64_t last_frame = 0; + if (!last_frame) + last_frame = esp_timer_get_time(); + + res = httpd_resp_set_type(req, STREAM_CONTENT_TYPE); + if (res != ESP_OK) + return res; + + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin; Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321\r\n", "*"); + httpd_resp_set_hdr(req, "X-Framerate", "60"); + + while (true) + { + fb = esp_camera_fb_get(); + if (!fb) + { + log_e("Camera capture failed"); + res = ESP_FAIL; + } + else + { + _timestamp.tv_sec = fb->timestamp.tv_sec; + _timestamp.tv_usec = fb->timestamp.tv_usec; + _jpg_buf_len = fb->len; + _jpg_buf = fb->buf; + } + + if (res == ESP_OK) + res = httpd_resp_send_chunk(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY)); + + if (res == ESP_OK) + { + size_t hlen = snprintf((char *)part_buf, 128, STREAM_PART, _jpg_buf_len, _timestamp.tv_sec, _timestamp.tv_usec); + res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen); + } + if (res == ESP_OK) + res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len); + + if (fb) + { + esp_camera_fb_return(fb); + fb = NULL; + _jpg_buf = NULL; + } + + else if (_jpg_buf) + { + free(_jpg_buf); + _jpg_buf = NULL; + } + + if (res != ESP_OK) + break; + + long request_end = millis(); + long latency = (request_end - last_request_time); + last_request_time = request_end; + log_d("Size: %uKB, Time: %ums (%ifps)\n", _jpg_buf_len / 1024, latency, 1000 / latency); + } + + last_frame = 0; + + return res; +} + +int OpenIris::StreamServer::startStreamServer() +{ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.max_uri_handlers = 1; + config.server_port = STREAM_SERVER_PORT; + config.ctrl_port = STREAM_SERVER_PORT; + + httpd_uri_t stream_page = { + .uri = "/", + .method = HTTP_GET, + .handler = &OpenIris::StreamHelpers::stream, + .user_ctx = nullptr}; + + int status = httpd_start(&camera_stream, &config); + + if (status != ESP_OK) + return -1; + else + { + httpd_register_uri_handler(camera_stream, &stream_page); + log_d("Stream server initialized"); + return 0; + } +} diff --git a/ESP/src/webServer/webserverHandler.cpp b/ESP/src/webServer/webserverHandler.cpp deleted file mode 100644 index 90a0ce3..0000000 --- a/ESP/src/webServer/webserverHandler.cpp +++ /dev/null @@ -1,94 +0,0 @@ -#include "GlobalVars.h" -#include "webServer/asyncJPGResponse.h" -#include "webServer/webserverHandler.h" - -OpenIris::HTTPDHandler::HTTPDHandler() -{ - this->server = new AsyncWebServer(STREAM_SERVER_PORT); -} - -void OpenIris::HTTPDHandler::startStreamServer() -{ - this->server->on( - "/", - HTTP_GET, - std::bind(&OpenIris::HTTPDHandler::stream_handler, this, std::placeholders::_1)); - this->server->on( - "/control", - HTTP_GET, - std::bind(&OpenIris::HTTPDHandler::command_handler, this, std::placeholders::_1)); - - this->server->on( - "/crop-roi", - HTTP_GET, - std::bind(&OpenIris::HTTPDHandler::roi_crop_command_handler, this, std::placeholders::_1)); - - log_d("Initializing web server"); - this->server->begin(); -} - -void OpenIris::HTTPDHandler::roi_crop_command_handler(AsyncWebServerRequest *request) -{ - int offsetX = 0; - int offsetY = 0; - int outputX = 0; - int outputY = 0; - - if (request->hasParam("offsetX")) - { - AsyncWebParameter *offsetX_param = request->getParam("offsetX"); - offsetX = atoi(offsetX_param->value().c_str()); - } - if (request->hasParam("offsetY")) - { - AsyncWebParameter *offsetY_param = request->getParam("offsetY"); - offsetY = atoi(offsetY_param->value().c_str()); - } - if (request->hasParam("outputX")) - { - AsyncWebParameter *outputX_param = request->getParam("outputX"); - outputX = atoi(outputX_param->value().c_str()); - } - if (request->hasParam("outputY")) - { - AsyncWebParameter *outputY_param = request->getParam("outputY"); - outputY = atoi(outputY_param->value().c_str()); - } - - if (offsetX != 0 && offsetY != 0 && outputX != 0 && outputY != 0) - cameraHandler.setVieWindow(offsetX, offsetY, outputX, outputY); - request->send(200); -} - -void OpenIris::HTTPDHandler::command_handler(AsyncWebServerRequest *request) -{ - if (request->hasParam("framesize")) - { - AsyncWebParameter *framesize_param = request->getParam("framesize"); - cameraHandler.setCameraResolution((framesize_t)atoi(framesize_param->value().c_str())); - } - if (request->hasParam("hmirror")) - { - AsyncWebParameter *hmirror_param = request->getParam("hmirror"); - cameraHandler.setHFlip(atoi(hmirror_param->value().c_str())); - } - if (request->hasParam("vflip")) - { - AsyncWebParameter *vflip_param = request->getParam("vflip"); - cameraHandler.setVFlip(atoi(vflip_param->value().c_str())); - } - - request->send(200); -} - -void OpenIris::HTTPDHandler::stream_handler(AsyncWebServerRequest *request) -{ - OpenIris::AsyncJpegStreamResponse *response = new OpenIris::AsyncJpegStreamResponse(); - if (!response) - { - request->send(501); - return; - } - response->addHeader("Access-Control-Allow-Origin", "*"); - request->send(response); -} \ No newline at end of file diff --git a/ESP/src/webserverHandler.cpp b/ESP/src/webserverHandler.cpp new file mode 100644 index 0000000..45de038 --- /dev/null +++ b/ESP/src/webserverHandler.cpp @@ -0,0 +1,39 @@ +#include "GlobalVars.h" +#include "webserverHandler.h" + +OpenIris::APIServer::APIServer() +{ + this->server = new AsyncWebServer(CONTROL_SERVER_PORT); +} + +void OpenIris::APIServer::startAPIServer() +{ + this->server->on( + "/control", + HTTP_GET, + std::bind(&OpenIris::APIServer::command_handler, this, std::placeholders::_1)); + + log_d("Initializing web server"); + this->server->begin(); +} + +void OpenIris::APIServer::command_handler(AsyncWebServerRequest *request) +{ + if (request->hasParam("framesize")) + { + AsyncWebParameter *framesize_param = request->getParam("framesize"); + cameraHandler.setCameraResolution((framesize_t)atoi(framesize_param->value().c_str())); + } + if (request->hasParam("hmirror")) + { + AsyncWebParameter *hmirror_param = request->getParam("hmirror"); + cameraHandler.setHFlip(atoi(hmirror_param->value().c_str())); + } + if (request->hasParam("vflip")) + { + AsyncWebParameter *vflip_param = request->getParam("vflip"); + cameraHandler.setVFlip(atoi(vflip_param->value().c_str())); + } + + request->send(200); +} \ No newline at end of file diff --git a/ESP/src/wifiHandler.cpp b/ESP/src/wifiHandler.cpp index cb1203a..6a3b494 100644 --- a/ESP/src/wifiHandler.cpp +++ b/ESP/src/wifiHandler.cpp @@ -27,7 +27,7 @@ void OpenIris::WiFiHandler::setupWifi(const char *ssid, const char *password) delay(1600); log_i("\n\rWiFi connected\n\r"); log_i("ESP will be streaming under 'http://%s:80/\r\n", WiFi.localIP().toString().c_str()); - log_i("ESP will be accepting commands under 'http://%s:80/control\r\n", WiFi.localIP().toString().c_str()); + log_i("ESP will be accepting commands under 'http://%s:81/control\r\n", WiFi.localIP().toString().c_str()); } else {