Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,93 @@ https://user-images.githubusercontent.com/213467/236601550-ba1a5ba1-4f1c-4dfa-9b
To facilitate easy connection to the camera, it also runs a mDNS server to
advertise the camera's IP address / port that the RTSP server is running on.

<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
**Table of Contents**

- [camera-streamer](#camera-streamer)
- [Use](#use)
- [Program](#program)
- [Configure](#configure)
- [Hardware](#hardware)
- [Full pin-out:](#full-pin-out)
- [Camera Interface (OV3660):](#camera-interface-ov3660)
- [RTC (BM8563):](#rtc-bm8563)
- [Battery:](#battery)
- [Input / Output:](#input--output)
- [Additional References](#additional-references)

<!-- markdown-toc end -->

## Use

You must first program your hardware. Afterwards, you can configure it via a USB
connection using its built-in CLI.

### Program

The ESP32-TimerCam will require one-time programming to function.

Download the release `programmer` executable from the latest [releases
page](https://github.com/esp-cpp/camera-streamer/releases) for `windows`,
`macos`, or `linux` - depending on which computer you want to use to perform the
one-time programming.

1. Download the programmer
2. Unzip it
3. Double click the `exe` (if windows), or open a terminal and execute it from
the command line `./camera-streamer_programmer_v2.0.0_macos.bin`.

### Configure

To configure it, simply connect it to your computer via USB and open the serial
port in a terminal (e.g. `screen`, `PuTTY`, etc.) at 115200 baud. Once there,
you can use it as you would any other CLI - and the `help` command will provide
info about the commands available.

Any SSID/Password you set will be securely saved in the ESP32-TimerCam's NVS,
which is managed by the ESP-IDF WiFi subsystem.

![CleanShot 2025-06-22 at 22 38 41](https://github.com/user-attachments/assets/4b698b21-c66e-469e-9c49-a12d9f0ae65b)

```console
sta> help
Commands available:
- help
This help message
- exit
Quit the session
- log <verbosity>
Set the log verbosity for the wifi sta.
- connect
Connect to a WiFi network with the given SSID and password.
- connect <ssid> <password>
Connect to a WiFi network with the given SSID and password.
- disconnect
Disconnect from the current WiFi network.
- ssid
Get the current SSID (Service Set Identifier) of the WiFi connection.
- rssi
Get the current RSSI (Received Signal Strength Indicator) of the WiFi connection.
- ip
Get the current IP address of the WiFi connection.
- connected
Check if the WiFi is connected.
- mac
Get the current MAC address of the WiFi connection.
- bssid
Get the current BSSID (MAC addressof the access point) of the WiFi connection.
- channel
Get the current WiFi channel of the connection.
- config
Get the current WiFi configuration.
- scan <int>
Scan for available WiFi networks.
- memory
Display minimum free memory.
- battery
Display the current battery voltage.
```

## Hardware

This sample is designed to run on the ESP32 TimerCam ([Amazon Link](https://www.amazon.com/dp/B09W2RSPGL?psc=1&ref=ppx_yo2ov_dt_b_product_details)).
Expand Down
4 changes: 2 additions & 2 deletions main/Kconfig.projbuild
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ menu "Camera Streamer Configuration"

config ESP_WIFI_SSID
string "WiFi SSID"
default "myssid"
default ""
help
SSID (network name) for the camera streamer to connect to.

config ESP_WIFI_PASSWORD
string "WiFi Password"
default "mypassword"
default ""
help
WiFi password (WPA or WPA2) for the camera streamer to use.

Expand Down
210 changes: 125 additions & 85 deletions main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,33 @@
#include <esp_heap_caps.h>
#include <mdns.h>

#include "cli.hpp"
#include "esp32-timer-cam.hpp"
#include "nvs.hpp"
#include "task.hpp"
#include "tcp_socket.hpp"
#include "udp_socket.hpp"
#include "wifi_sta.hpp"
#include "wifi_sta_menu.hpp"

#include "esp_camera.h"

#include "rtsp_server.hpp"

using namespace std::chrono_literals;

static espp::Logger logger({.tag = "Camera Streamer", .level = espp::Logger::Verbosity::INFO});

std::recursive_mutex server_mutex;
std::unique_ptr<espp::Task> camera_task;
std::shared_ptr<espp::RtspServer> rtsp_server;

esp_err_t initialize_camera(void);
void start_rtsp_server(std::string_view server_address, int server_port);
bool camera_task_fn(const std::mutex &m, const std::condition_variable &cv);

extern "C" void app_main(void) {
esp_err_t err;
espp::Logger logger({.tag = "Camera Streamer", .level = espp::Logger::Verbosity::INFO});
logger.info("Bootup");

#if CONFIG_ESP32_WIFI_NVS_ENABLED
Expand All @@ -35,38 +46,92 @@ extern "C" void app_main(void) {

auto &timer_cam = espp::EspTimerCam::get();

// initialize RTC
if (!timer_cam.initialize_rtc()) {
logger.error("Could not initialize RTC");
return;
}

// initialize LED
static constexpr float led_breathing_period = 3.5f;
if (!timer_cam.initialize_led(led_breathing_period)) {
static constexpr float disconnected_led_breathing_period = 1.0f;
static constexpr float connected_led_breathing_period = 3.5f;
if (!timer_cam.initialize_led(disconnected_led_breathing_period)) {
logger.error("Could not initialize LED");
return;
}
timer_cam.start_led_breathing();

// initialize camera
logger.info("Initializing camera");
err = initialize_camera();
if (err != ESP_OK) {
logger.error("Could not initialize camera: {} '{}'", err, esp_err_to_name(err));
}

// initialize WiFi
logger.info("Initializing WiFi");
std::string server_address;
espp::WifiSta wifi_sta({.ssid = CONFIG_ESP_WIFI_SSID,
.password = CONFIG_ESP_WIFI_PASSWORD,
.num_connect_retries = CONFIG_ESP_MAXIMUM_RETRY,
.on_connected = nullptr,
.on_disconnected = nullptr,
.on_got_ip = [&logger, &server_address](ip_event_got_ip_t *eventdata) {
server_address =
fmt::format("{}.{}.{}.{}", IP2STR(&eventdata->ip_info.ip));
logger.info("got IP: {}", server_address);
}});
// wait for network
float duty = 0.0f;
while (!wifi_sta.is_connected()) {
logger.info("waiting for wifi connection...");
logger.move_up();
logger.clear_line();
timer_cam.set_led_brightness(duty);
duty = duty == 0.0f ? 0.5f : 0.0f;
std::this_thread::sleep_for(1s);
}
espp::WifiSta wifi_sta(
{.ssid = CONFIG_ESP_WIFI_SSID,
.password = CONFIG_ESP_WIFI_PASSWORD,
.num_connect_retries = CONFIG_ESP_MAXIMUM_RETRY,
.on_connected =
[]() {
static auto &timer_cam = espp::EspTimerCam::get();
timer_cam.set_led_breathing_period(connected_led_breathing_period);
},
.on_disconnected =
[]() {
static auto &timer_cam = espp::EspTimerCam::get();
timer_cam.set_led_breathing_period(disconnected_led_breathing_period);
std::lock_guard<std::recursive_mutex> lock(server_mutex);
logger.info("Stopping camera task");
camera_task.reset();
logger.info("Stopping RTSP server");
rtsp_server.reset();
},
.on_got_ip =
[](ip_event_got_ip_t *eventdata) {
auto server_address = fmt::format("{}.{}.{}.{}", IP2STR(&eventdata->ip_info.ip));
logger.info("got IP: {}", server_address);
// create the camera and rtsp server, and the cv/m
// they'll use to communicate
std::lock_guard<std::recursive_mutex> lock(server_mutex);
start_rtsp_server(server_address, CONFIG_RTSP_SERVER_PORT);
// initialize the camera
logger.info("Creating camera task");
camera_task = espp::Task::make_unique(
espp::Task::Config{.callback = camera_task_fn,
.task_config = {.name = "Camera Task", .priority = 10}});
camera_task->start();
}});

// initialize camera
espp::WifiStaMenu sta_menu(wifi_sta);
auto root_menu = sta_menu.get();
root_menu->Insert(
"memory",
[](std::ostream &out) {
out << "Minimum free memory: " << heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT)
<< std::endl;
},
"Display minimum free memory.");
root_menu->Insert(
"battery",
[](std::ostream &out) {
static auto &timer_cam = espp::EspTimerCam::get();
out << fmt::format("Battery voltage: {:.2f}\n", timer_cam.get_battery_voltage());
},
"Display the current battery voltage.");

cli::Cli cli(std::move(root_menu));
cli::SetColor();
cli.ExitAction([](auto &out) { out << "Goodbye and thanks for all the fish.\n"; });

espp::Cli input(cli);
input.SetInputHistorySize(10);
input.Start();
}

esp_err_t initialize_camera(void) {
/**
* @note display sizes supported:
* * QVGA: 320x240
Expand All @@ -85,7 +150,7 @@ extern "C" void app_main(void) {
* * UXGA: 1600x1200
*/

logger.info("Initializing camera");
auto &timer_cam = espp::EspTimerCam::get();
static camera_config_t camera_config = {
.pin_pwdn = -1,
.pin_reset = timer_cam.get_camera_reset_pin(),
Expand Down Expand Up @@ -121,33 +186,23 @@ extern "C" void app_main(void) {
.grab_mode =
CAMERA_GRAB_LATEST // CAMERA_GRAB_WHEN_EMPTY // . Sets when buffers should be filled
};
err = esp_camera_init(&camera_config);
if (err != ESP_OK) {
logger.error("Could not initialize camera: {} '{}'", err, esp_err_to_name(err));
}

timer_cam.start_led_breathing();

// initialize RTC
if (!timer_cam.initialize_rtc()) {
logger.error("Could not initialize RTC");
return;
}
return esp_camera_init(&camera_config);
}

// create the camera and rtsp server, and the cv/m they'll use to
// communicate
int server_port = CONFIG_RTSP_SERVER_PORT;
void start_rtsp_server(std::string_view server_address, int server_port) {
logger.info("Creating RTSP server at {}:{}", server_address, server_port);
espp::RtspServer rtsp_server({.server_address = server_address,
.port = server_port,
.path = "mjpeg/1",
.log_level = espp::Logger::Verbosity::WARN});
rtsp_server.set_session_log_level(espp::Logger::Verbosity::WARN);
rtsp_server.start();
std::lock_guard<std::recursive_mutex> lock(server_mutex);
rtsp_server = std::make_shared<espp::RtspServer>(
espp::RtspServer::Config{.server_address = std::string(server_address),
.port = server_port,
.path = "mjpeg/1",
.log_level = espp::Logger::Verbosity::WARN});
rtsp_server->set_session_log_level(espp::Logger::Verbosity::WARN);
rtsp_server->start();

// initialize mDNS
logger.info("Initializing mDNS");
err = mdns_init();
esp_err_t err = mdns_init();
if (err != ESP_OK) {
logger.error("Could not initialize mDNS: {}", err);
return;
Expand All @@ -173,47 +228,32 @@ extern "C" void app_main(void) {
return;
}
logger.info("mDNS initialized");
}

// initialize the camera
logger.info("Creating camera task");
auto camera_task_fn = [&rtsp_server, &logger](const auto &m, const auto &cv) -> bool {
// take image
static camera_fb_t *fb = NULL;
static size_t _jpg_buf_len;
static uint8_t *_jpg_buf;

fb = esp_camera_fb_get();
if (!fb) {
logger.error("Camera capture failed");
return false;
}

_jpg_buf_len = fb->len;
_jpg_buf = fb->buf;
bool camera_task_fn(const std::mutex &m, const std::condition_variable &cv) {
// take image
static camera_fb_t *fb = NULL;
static size_t _jpg_buf_len;
static uint8_t *_jpg_buf;

if (!(_jpg_buf[_jpg_buf_len - 1] != 0xd9 || _jpg_buf[_jpg_buf_len - 2] != 0xd9)) {
esp_camera_fb_return(fb);
return false;
}
fb = esp_camera_fb_get();
if (!fb) {
logger.error("Camera capture failed");
return false;
}

espp::JpegFrame image(reinterpret_cast<const char *>(_jpg_buf), _jpg_buf_len);
rtsp_server.send_frame(image);
_jpg_buf_len = fb->len;
_jpg_buf = fb->buf;

if (!(_jpg_buf[_jpg_buf_len - 1] != 0xd9 || _jpg_buf[_jpg_buf_len - 2] != 0xd9)) {
esp_camera_fb_return(fb);
return false;
};

auto camera_task = espp::Task::make_unique(
{.callback = camera_task_fn, .task_config = {.name = "Camera Task", .priority = 10}});
camera_task->start();

while (true) {
std::this_thread::sleep_for(100ms);
// print out some stats (battery, framerate)
logger.info("Minimum free memory: {}, Battery voltage: {:.2f}",
heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT),
timer_cam.get_battery_voltage());
logger.move_up();
logger.clear_line();
}
}

espp::JpegFrame image(reinterpret_cast<const char *>(_jpg_buf), _jpg_buf_len);
std::lock_guard<std::recursive_mutex> lock(server_mutex);
rtsp_server->send_frame(image);

esp_camera_fb_return(fb);
return false;
};
3 changes: 3 additions & 0 deletions sdkconfig.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240

# ESP32-Camera specific
CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY=y

# the cli library requires exceptions right now...
CONFIG_COMPILER_CXX_EXCEPTIONS=y