Skip to content

Commit f6bbffe

Browse files
authored
feat: Update to espp 1.0.10 and use CLI + WiFi STA menu to allow runtime WiFi configuration (#12)
* feat: Update to espp 1.0.10 and use CLI + WiFi STA menu to allow runtime WiFi configuration * readme: update * add mutex protection
1 parent a4633fc commit f6bbffe

File tree

4 files changed

+217
-87
lines changed

4 files changed

+217
-87
lines changed

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,93 @@ https://user-images.githubusercontent.com/213467/236601550-ba1a5ba1-4f1c-4dfa-9b
99
To facilitate easy connection to the camera, it also runs a mDNS server to
1010
advertise the camera's IP address / port that the RTSP server is running on.
1111

12+
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
13+
**Table of Contents**
14+
15+
- [camera-streamer](#camera-streamer)
16+
- [Use](#use)
17+
- [Program](#program)
18+
- [Configure](#configure)
19+
- [Hardware](#hardware)
20+
- [Full pin-out:](#full-pin-out)
21+
- [Camera Interface (OV3660):](#camera-interface-ov3660)
22+
- [RTC (BM8563):](#rtc-bm8563)
23+
- [Battery:](#battery)
24+
- [Input / Output:](#input--output)
25+
- [Additional References](#additional-references)
26+
27+
<!-- markdown-toc end -->
28+
29+
## Use
30+
31+
You must first program your hardware. Afterwards, you can configure it via a USB
32+
connection using its built-in CLI.
33+
34+
### Program
35+
36+
The ESP32-TimerCam will require one-time programming to function.
37+
38+
Download the release `programmer` executable from the latest [releases
39+
page](https://github.com/esp-cpp/camera-streamer/releases) for `windows`,
40+
`macos`, or `linux` - depending on which computer you want to use to perform the
41+
one-time programming.
42+
43+
1. Download the programmer
44+
2. Unzip it
45+
3. Double click the `exe` (if windows), or open a terminal and execute it from
46+
the command line `./camera-streamer_programmer_v2.0.0_macos.bin`.
47+
48+
### Configure
49+
50+
To configure it, simply connect it to your computer via USB and open the serial
51+
port in a terminal (e.g. `screen`, `PuTTY`, etc.) at 115200 baud. Once there,
52+
you can use it as you would any other CLI - and the `help` command will provide
53+
info about the commands available.
54+
55+
Any SSID/Password you set will be securely saved in the ESP32-TimerCam's NVS,
56+
which is managed by the ESP-IDF WiFi subsystem.
57+
58+
![CleanShot 2025-06-22 at 22 38 41](https://github.com/user-attachments/assets/4b698b21-c66e-469e-9c49-a12d9f0ae65b)
59+
60+
```console
61+
sta> help
62+
Commands available:
63+
- help
64+
This help message
65+
- exit
66+
Quit the session
67+
- log <verbosity>
68+
Set the log verbosity for the wifi sta.
69+
- connect
70+
Connect to a WiFi network with the given SSID and password.
71+
- connect <ssid> <password>
72+
Connect to a WiFi network with the given SSID and password.
73+
- disconnect
74+
Disconnect from the current WiFi network.
75+
- ssid
76+
Get the current SSID (Service Set Identifier) of the WiFi connection.
77+
- rssi
78+
Get the current RSSI (Received Signal Strength Indicator) of the WiFi connection.
79+
- ip
80+
Get the current IP address of the WiFi connection.
81+
- connected
82+
Check if the WiFi is connected.
83+
- mac
84+
Get the current MAC address of the WiFi connection.
85+
- bssid
86+
Get the current BSSID (MAC addressof the access point) of the WiFi connection.
87+
- channel
88+
Get the current WiFi channel of the connection.
89+
- config
90+
Get the current WiFi configuration.
91+
- scan <int>
92+
Scan for available WiFi networks.
93+
- memory
94+
Display minimum free memory.
95+
- battery
96+
Display the current battery voltage.
97+
```
98+
1299
## Hardware
13100

14101
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)).

main/Kconfig.projbuild

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ menu "Camera Streamer Configuration"
88

99
config ESP_WIFI_SSID
1010
string "WiFi SSID"
11-
default "myssid"
11+
default ""
1212
help
1313
SSID (network name) for the camera streamer to connect to.
1414

1515
config ESP_WIFI_PASSWORD
1616
string "WiFi Password"
17-
default "mypassword"
17+
default ""
1818
help
1919
WiFi password (WPA or WPA2) for the camera streamer to use.
2020

main/main.cpp

Lines changed: 125 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,33 @@
88
#include <esp_heap_caps.h>
99
#include <mdns.h>
1010

11+
#include "cli.hpp"
1112
#include "esp32-timer-cam.hpp"
1213
#include "nvs.hpp"
1314
#include "task.hpp"
1415
#include "tcp_socket.hpp"
1516
#include "udp_socket.hpp"
1617
#include "wifi_sta.hpp"
18+
#include "wifi_sta_menu.hpp"
1719

1820
#include "esp_camera.h"
1921

2022
#include "rtsp_server.hpp"
2123

2224
using namespace std::chrono_literals;
2325

26+
static espp::Logger logger({.tag = "Camera Streamer", .level = espp::Logger::Verbosity::INFO});
27+
28+
std::recursive_mutex server_mutex;
29+
std::unique_ptr<espp::Task> camera_task;
30+
std::shared_ptr<espp::RtspServer> rtsp_server;
31+
32+
esp_err_t initialize_camera(void);
33+
void start_rtsp_server(std::string_view server_address, int server_port);
34+
bool camera_task_fn(const std::mutex &m, const std::condition_variable &cv);
35+
2436
extern "C" void app_main(void) {
2537
esp_err_t err;
26-
espp::Logger logger({.tag = "Camera Streamer", .level = espp::Logger::Verbosity::INFO});
2738
logger.info("Bootup");
2839

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

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

49+
// initialize RTC
50+
if (!timer_cam.initialize_rtc()) {
51+
logger.error("Could not initialize RTC");
52+
return;
53+
}
54+
3855
// initialize LED
39-
static constexpr float led_breathing_period = 3.5f;
40-
if (!timer_cam.initialize_led(led_breathing_period)) {
56+
static constexpr float disconnected_led_breathing_period = 1.0f;
57+
static constexpr float connected_led_breathing_period = 3.5f;
58+
if (!timer_cam.initialize_led(disconnected_led_breathing_period)) {
4159
logger.error("Could not initialize LED");
4260
return;
4361
}
62+
timer_cam.start_led_breathing();
63+
64+
// initialize camera
65+
logger.info("Initializing camera");
66+
err = initialize_camera();
67+
if (err != ESP_OK) {
68+
logger.error("Could not initialize camera: {} '{}'", err, esp_err_to_name(err));
69+
}
4470

4571
// initialize WiFi
4672
logger.info("Initializing WiFi");
47-
std::string server_address;
48-
espp::WifiSta wifi_sta({.ssid = CONFIG_ESP_WIFI_SSID,
49-
.password = CONFIG_ESP_WIFI_PASSWORD,
50-
.num_connect_retries = CONFIG_ESP_MAXIMUM_RETRY,
51-
.on_connected = nullptr,
52-
.on_disconnected = nullptr,
53-
.on_got_ip = [&logger, &server_address](ip_event_got_ip_t *eventdata) {
54-
server_address =
55-
fmt::format("{}.{}.{}.{}", IP2STR(&eventdata->ip_info.ip));
56-
logger.info("got IP: {}", server_address);
57-
}});
58-
// wait for network
59-
float duty = 0.0f;
60-
while (!wifi_sta.is_connected()) {
61-
logger.info("waiting for wifi connection...");
62-
logger.move_up();
63-
logger.clear_line();
64-
timer_cam.set_led_brightness(duty);
65-
duty = duty == 0.0f ? 0.5f : 0.0f;
66-
std::this_thread::sleep_for(1s);
67-
}
73+
espp::WifiSta wifi_sta(
74+
{.ssid = CONFIG_ESP_WIFI_SSID,
75+
.password = CONFIG_ESP_WIFI_PASSWORD,
76+
.num_connect_retries = CONFIG_ESP_MAXIMUM_RETRY,
77+
.on_connected =
78+
[]() {
79+
static auto &timer_cam = espp::EspTimerCam::get();
80+
timer_cam.set_led_breathing_period(connected_led_breathing_period);
81+
},
82+
.on_disconnected =
83+
[]() {
84+
static auto &timer_cam = espp::EspTimerCam::get();
85+
timer_cam.set_led_breathing_period(disconnected_led_breathing_period);
86+
std::lock_guard<std::recursive_mutex> lock(server_mutex);
87+
logger.info("Stopping camera task");
88+
camera_task.reset();
89+
logger.info("Stopping RTSP server");
90+
rtsp_server.reset();
91+
},
92+
.on_got_ip =
93+
[](ip_event_got_ip_t *eventdata) {
94+
auto server_address = fmt::format("{}.{}.{}.{}", IP2STR(&eventdata->ip_info.ip));
95+
logger.info("got IP: {}", server_address);
96+
// create the camera and rtsp server, and the cv/m
97+
// they'll use to communicate
98+
std::lock_guard<std::recursive_mutex> lock(server_mutex);
99+
start_rtsp_server(server_address, CONFIG_RTSP_SERVER_PORT);
100+
// initialize the camera
101+
logger.info("Creating camera task");
102+
camera_task = espp::Task::make_unique(
103+
espp::Task::Config{.callback = camera_task_fn,
104+
.task_config = {.name = "Camera Task", .priority = 10}});
105+
camera_task->start();
106+
}});
68107

69-
// initialize camera
108+
espp::WifiStaMenu sta_menu(wifi_sta);
109+
auto root_menu = sta_menu.get();
110+
root_menu->Insert(
111+
"memory",
112+
[](std::ostream &out) {
113+
out << "Minimum free memory: " << heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT)
114+
<< std::endl;
115+
},
116+
"Display minimum free memory.");
117+
root_menu->Insert(
118+
"battery",
119+
[](std::ostream &out) {
120+
static auto &timer_cam = espp::EspTimerCam::get();
121+
out << fmt::format("Battery voltage: {:.2f}\n", timer_cam.get_battery_voltage());
122+
},
123+
"Display the current battery voltage.");
124+
125+
cli::Cli cli(std::move(root_menu));
126+
cli::SetColor();
127+
cli.ExitAction([](auto &out) { out << "Goodbye and thanks for all the fish.\n"; });
128+
129+
espp::Cli input(cli);
130+
input.SetInputHistorySize(10);
131+
input.Start();
132+
}
133+
134+
esp_err_t initialize_camera(void) {
70135
/**
71136
* @note display sizes supported:
72137
* * QVGA: 320x240
@@ -85,7 +150,7 @@ extern "C" void app_main(void) {
85150
* * UXGA: 1600x1200
86151
*/
87152

88-
logger.info("Initializing camera");
153+
auto &timer_cam = espp::EspTimerCam::get();
89154
static camera_config_t camera_config = {
90155
.pin_pwdn = -1,
91156
.pin_reset = timer_cam.get_camera_reset_pin(),
@@ -121,33 +186,23 @@ extern "C" void app_main(void) {
121186
.grab_mode =
122187
CAMERA_GRAB_LATEST // CAMERA_GRAB_WHEN_EMPTY // . Sets when buffers should be filled
123188
};
124-
err = esp_camera_init(&camera_config);
125-
if (err != ESP_OK) {
126-
logger.error("Could not initialize camera: {} '{}'", err, esp_err_to_name(err));
127-
}
128-
129-
timer_cam.start_led_breathing();
130-
131-
// initialize RTC
132-
if (!timer_cam.initialize_rtc()) {
133-
logger.error("Could not initialize RTC");
134-
return;
135-
}
189+
return esp_camera_init(&camera_config);
190+
}
136191

137-
// create the camera and rtsp server, and the cv/m they'll use to
138-
// communicate
139-
int server_port = CONFIG_RTSP_SERVER_PORT;
192+
void start_rtsp_server(std::string_view server_address, int server_port) {
140193
logger.info("Creating RTSP server at {}:{}", server_address, server_port);
141-
espp::RtspServer rtsp_server({.server_address = server_address,
142-
.port = server_port,
143-
.path = "mjpeg/1",
144-
.log_level = espp::Logger::Verbosity::WARN});
145-
rtsp_server.set_session_log_level(espp::Logger::Verbosity::WARN);
146-
rtsp_server.start();
194+
std::lock_guard<std::recursive_mutex> lock(server_mutex);
195+
rtsp_server = std::make_shared<espp::RtspServer>(
196+
espp::RtspServer::Config{.server_address = std::string(server_address),
197+
.port = server_port,
198+
.path = "mjpeg/1",
199+
.log_level = espp::Logger::Verbosity::WARN});
200+
rtsp_server->set_session_log_level(espp::Logger::Verbosity::WARN);
201+
rtsp_server->start();
147202

148203
// initialize mDNS
149204
logger.info("Initializing mDNS");
150-
err = mdns_init();
205+
esp_err_t err = mdns_init();
151206
if (err != ESP_OK) {
152207
logger.error("Could not initialize mDNS: {}", err);
153208
return;
@@ -173,47 +228,32 @@ extern "C" void app_main(void) {
173228
return;
174229
}
175230
logger.info("mDNS initialized");
231+
}
176232

177-
// initialize the camera
178-
logger.info("Creating camera task");
179-
auto camera_task_fn = [&rtsp_server, &logger](const auto &m, const auto &cv) -> bool {
180-
// take image
181-
static camera_fb_t *fb = NULL;
182-
static size_t _jpg_buf_len;
183-
static uint8_t *_jpg_buf;
184-
185-
fb = esp_camera_fb_get();
186-
if (!fb) {
187-
logger.error("Camera capture failed");
188-
return false;
189-
}
190-
191-
_jpg_buf_len = fb->len;
192-
_jpg_buf = fb->buf;
233+
bool camera_task_fn(const std::mutex &m, const std::condition_variable &cv) {
234+
// take image
235+
static camera_fb_t *fb = NULL;
236+
static size_t _jpg_buf_len;
237+
static uint8_t *_jpg_buf;
193238

194-
if (!(_jpg_buf[_jpg_buf_len - 1] != 0xd9 || _jpg_buf[_jpg_buf_len - 2] != 0xd9)) {
195-
esp_camera_fb_return(fb);
196-
return false;
197-
}
239+
fb = esp_camera_fb_get();
240+
if (!fb) {
241+
logger.error("Camera capture failed");
242+
return false;
243+
}
198244

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

248+
if (!(_jpg_buf[_jpg_buf_len - 1] != 0xd9 || _jpg_buf[_jpg_buf_len - 2] != 0xd9)) {
202249
esp_camera_fb_return(fb);
203250
return false;
204-
};
205-
206-
auto camera_task = espp::Task::make_unique(
207-
{.callback = camera_task_fn, .task_config = {.name = "Camera Task", .priority = 10}});
208-
camera_task->start();
209-
210-
while (true) {
211-
std::this_thread::sleep_for(100ms);
212-
// print out some stats (battery, framerate)
213-
logger.info("Minimum free memory: {}, Battery voltage: {:.2f}",
214-
heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT),
215-
timer_cam.get_battery_voltage());
216-
logger.move_up();
217-
logger.clear_line();
218251
}
219-
}
252+
253+
espp::JpegFrame image(reinterpret_cast<const char *>(_jpg_buf), _jpg_buf_len);
254+
std::lock_guard<std::recursive_mutex> lock(server_mutex);
255+
rtsp_server->send_frame(image);
256+
257+
esp_camera_fb_return(fb);
258+
return false;
259+
};

sdkconfig.defaults

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240
3333

3434
# ESP32-Camera specific
3535
CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY=y
36+
37+
# the cli library requires exceptions right now...
38+
CONFIG_COMPILER_CXX_EXCEPTIONS=y

0 commit comments

Comments
 (0)