commit b0522218f6f1574bf0ff6e337facfc100f650988 Author: Rene Zeldenthuis Date: Sun Jul 3 23:42:14 2022 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/include/esp32cam.h b/include/esp32cam.h new file mode 100644 index 0000000..dcdece0 --- /dev/null +++ b/include/esp32cam.h @@ -0,0 +1,12 @@ +#pragma once + +#define APP_TITLE "ESP32CAM-RTSP" +#define APP_VERSION "1.0" + +#define WIFI_SSID "ESP32CAM-RTSP" +#define WIFI_PASSWORD nullptr +#define CONFIG_VERSION "1.1" + +#define RTSP_PORT 554 +#define DEFAULT_FRAMERATE "20" +#define DEFAULT_FRAMESIZE "SVGA (800x600)" \ No newline at end of file diff --git a/include/frame_size.h b/include/frame_size.h new file mode 100644 index 0000000..f87babe --- /dev/null +++ b/include/frame_size.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +typedef char frame_size_name_t[18]; + +typedef struct frame_size_entry +{ + frame_size_name_t name; + framesize_t frame_size; +} frame_size_entry_t; + +constexpr const frame_size_entry_t frame_sizes[] = { + + {"96x96", FRAMESIZE_96X96}, + {"QQVGA (160x120)", FRAMESIZE_QQVGA}, + {"QCIF (176x144)", FRAMESIZE_QCIF}, + {"HQVGA (240x176)", FRAMESIZE_HQVGA}, + {"240x240", FRAMESIZE_240X240}, + {"QVGA (320x240)", FRAMESIZE_QVGA}, + {"CIF (400x296)", FRAMESIZE_CIF}, + {"HVGA (480x320)", FRAMESIZE_HVGA}, + {"VGA (640x480)", FRAMESIZE_VGA}, + {"SVGA (800x600)", FRAMESIZE_SVGA}, + {"XGA (1024x768)", FRAMESIZE_XGA}, + {"HD (1280x720)", FRAMESIZE_HD}, + {"SXGA (1280x1024)", FRAMESIZE_SXGA}, + {"UXGA (1600x1200)", FRAMESIZE_UXGA}, + {"FHD (1920x1080)", FRAMESIZE_FHD}, + {"P HD (2560x1440)", FRAMESIZE_P_HD}, + {"P 3MP (2560x1600)", FRAMESIZE_P_3MP}, + {"QXGA (2560x1920)", FRAMESIZE_QXGA}, + {"QHD (2560x1440)", FRAMESIZE_QHD}, + {"WQXGA (2560x1600)", FRAMESIZE_WQXGA}, + {"P FHD (1080x1920)", FRAMESIZE_P_FHD}, + {"QSXGA (2560x1920)", FRAMESIZE_QSXGA}, + {"", FRAMESIZE_INVALID}}; + +framesize_t lookup_frame_size(const char *pin) +{ + // Lookup table for the frame name to framesize_t + auto entry = &frame_sizes[0]; + while (*entry->name) + { + if (strncmp(entry->name, pin, sizeof(frame_size_name_t)) == 0) + return entry->frame_size; + + entry++; + } + + return FRAMESIZE_INVALID; +} \ No newline at end of file diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/rtsp_server/library.json b/lib/rtsp_server/library.json new file mode 100644 index 0000000..69739e1 --- /dev/null +++ b/lib/rtsp_server/library.json @@ -0,0 +1,8 @@ +{ + "name": "RTSPServer", + "version": "1.0.0", + "description": "", + "dependencies": { + "contrem/arduino-timer": "^2.3.1" + } +} \ No newline at end of file diff --git a/lib/rtsp_server/rtsp_server.cpp b/lib/rtsp_server/rtsp_server.cpp new file mode 100644 index 0000000..e569063 --- /dev/null +++ b/lib/rtsp_server/rtsp_server.cpp @@ -0,0 +1,51 @@ +#include "rtsp_server.h" +#include +#include +#include + +// URI: e.g. rtsp://192.168.178.27:554/mjpeg/1 +rtsp_server::rtsp_server(OV2640 &cam, unsigned long interval, int port /*= 554*/) + : WiFiServer(port), cam_(cam) +{ + log_i("Starting RTSP server"); + WiFiServer::begin(); + // Add service to mDNS - rtsp + MDNS.addService("rtsp", "tcp", 554); + + timer_.every(interval, client_handler, this); +} + +void rtsp_server::doLoop() +{ + timer_.tick(); +} + +rtsp_server::rtsp_client::rtsp_client(const WiFiClient &client, OV2640 &cam) +{ + wifi_client = client; + streamer = std::shared_ptr(new OV2640Streamer(&wifi_client, cam)); + session = std::shared_ptr(new CRtspSession(&wifi_client, streamer.get())); +} + +bool rtsp_server::client_handler(void *arg) +{ + auto self = static_cast(arg); + // Check if a client wants to connect + auto new_client = self->accept(); + if (new_client) + self->clients_.push_back(std::unique_ptr(new rtsp_client(new_client, self->cam_))); + + auto now = millis(); + for (const auto &client : self->clients_) + { + // Handle requests + client->session->handleRequests(0); + // Send the frame. For now return the uptime as time marker, currMs + client->session->broadcastCurrentFrame(now); + } + + self->clients_.remove_if([](std::unique_ptr const &c) + { return c->session->m_stopped; }); + + return true; +} \ No newline at end of file diff --git a/lib/rtsp_server/rtsp_server.h b/lib/rtsp_server/rtsp_server.h new file mode 100644 index 0000000..0ac2041 --- /dev/null +++ b/lib/rtsp_server/rtsp_server.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class rtsp_server : public WiFiServer +{ +public: + rtsp_server(OV2640 &cam, unsigned long interval, int port = 554); + + void doLoop(); + +private: + struct rtsp_client + { + public: + rtsp_client(const WiFiClient &client, OV2640 &cam); + + WiFiClient wifi_client; + // Streamer for UDP/TCP based RTP transport + std::shared_ptr streamer; + // RTSP session and state + std::shared_ptr session; + }; + + OV2640 cam_; + std::list> clients_; + uintptr_t task_; + Timer<> timer_; + + static bool client_handler(void *); +}; \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..f6094f1 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,26 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32cam] +platform = espressif32 +board = esp32cam +framework = arduino + +monitor_speed = 115200 +monitor_filters = log2file, time, default + +build_flags = + -O2 + -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE + -D LED_BUILTIN=4 + +lib_deps = + prampec/IotWebConf @ ^3.2.1 + geeksville/Micro-RTSP @ ^0.1.6 \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..445305d --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,160 @@ +#include + +#include + +#include +#include + +#include + +#include + +#include + +#include + +char frame_rate_val[6]; +char frame_size_val[sizeof(frame_size_entry_t)]; + +auto config_group_stream_settings = iotwebconf::ParameterGroup("settings", "Streaming settings"); +auto config_frame_rate = iotwebconf::NumberParameter("Frame rate (ms)", "fr", frame_rate_val, sizeof(frame_rate_val), DEFAULT_FRAMERATE); +auto config_frame_size = iotwebconf::SelectParameter("Frame size", "fs", frame_size_val, sizeof(frame_size_val), (const char *)frame_sizes, (const char *)frame_sizes, sizeof(frame_sizes) / sizeof(frame_sizes[0]), sizeof(frame_sizes[0]), DEFAULT_FRAMESIZE); + +// Camera +OV2640 cam; + +// DNS Server +DNSServer dnsServer; + +// RTSP Server +std::unique_ptr camera_server; + +// Web server +WebServer web_server(80); +IotWebConf iotWebConf(WIFI_SSID, &dnsServer, &web_server, WIFI_PASSWORD, CONFIG_VERSION); + +void handleRoot() +{ + log_v("Handle root"); + // Let IotWebConf test and handle captive portal requests. + if (iotWebConf.handleCaptivePortal()) + return; + + String html; + html += ""; + html += "" APP_TITLE " v" APP_VERSION ""; + html += ""; + html += "

Status page for " + String(iotWebConf.getThingName()) + "


"; + html += "

ESP32

"; + html += "
    "; + html += "
  • CPU model: " + String(ESP.getChipModel()) + "
  • "; + html += "
  • CPU speed: " + String(ESP.getCpuFreqMHz()) + "Mhz
  • "; + html += "
  • Mac address: " + WiFi.macAddress() + "
  • "; + html += "
"; + html += "

Settings

"; + html += "
    "; + html += "
  • Frame size: " + String(frame_size_val) + "
  • "; + html += "
  • Frame rate: " + String(frame_rate_val) + "
  • "; + html += "
"; + html += "
Go to configure page to change settings."; + html += ""; + web_server.send(200, "text/html", html); +} + +void initialize_camera() +{ + log_v("Initialize camera"); + log_i("Frame size: %s", frame_size_val); + auto frame_size = lookup_frame_size(frame_size_val); + // ESP32CAM + log_d("Looking for ESP32CAM"); + esp32cam_config.frame_size = frame_size; + if (cam.init(esp32cam_aithinker_config) == ESP_OK) + { + log_i("Found ESP32CAM"); + return; + } + + // AI Thinker + log_d("Looking for AI Thinker"); + esp32cam_aithinker_config.frame_size = frame_size; + if (cam.init(esp32cam_aithinker_config) == ESP_OK) + { + log_i("Found AI Thinker"); + return; + } + + // TTGO T-Cam + log_d("Looking for TTGO T-Cam"); + esp32cam_ttgo_t_config.frame_size = frame_size; + if (cam.init(esp32cam_ttgo_t_config) == ESP_OK) + { + log_i("Found TTGO T-Cam"); + return; + } + + log_e("No camera found"); +} + +void start_rtsp_server() +{ + log_v("start_rtsp_server"); + initialize_camera(); + auto frame_rate = atol(frame_rate_val); + camera_server = std::unique_ptr(new rtsp_server(cam, frame_rate, RTSP_PORT)); +} + +void on_config_saved() +{ + log_v("on_config_saved"); + start_rtsp_server(); +} + +void on_connected() +{ + log_v("on_connected"); + start_rtsp_server(); +} + +void setup() +{ + // Disable brownout + WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); + + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, false); + +#ifdef CORE_DEBUG_LEVEL + Serial.begin(115200); + Serial.setDebugOutput(true); + +#endif + + log_i("CPU Freq = %d Mhz", getCpuFrequencyMhz()); + log_i("Free heap: %d bytes", ESP.getFreeHeap()); + log_i("Starting " APP_TITLE "..."); + + config_group_stream_settings.addItem(&config_frame_rate); + config_group_stream_settings.addItem(&config_frame_size); + iotWebConf.addParameterGroup(&config_group_stream_settings); + iotWebConf.getApTimeoutParameter()->visible = true; + iotWebConf.setConfigSavedCallback(on_config_saved); + iotWebConf.setWifiConnectionCallback(on_connected); + + iotWebConf.init(); + + // Set up required URL handlers on the web server + web_server.on("/", HTTP_GET, handleRoot); + web_server.on("/config", [] + { iotWebConf.handleConfig(); }); + web_server.onNotFound([]() + { iotWebConf.handleNotFound(); }); +} + +void loop() +{ + iotWebConf.doLoop(); + + if (camera_server) + camera_server->doLoop(); +} \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html