20constexpr const char *
const TAG =
"snapshot";
23constexpr size_t MAX_NAME_LENGTH = 200;
25constexpr unsigned MAX_NAME_ATTEMPTS = 1000;
27constexpr size_t BMP_HEADER_SIZE = 54;
28constexpr size_t BMP_INFO_HEADER_SIZE = 40;
29constexpr int BMP_BITS_PER_PIXEL = 24;
33bool has_bmp_suffix(
const std::string &name) {
34 return name.size() >= 4 && strcasecmp(
name.c_str() +
name.size() - 4,
".bmp") == 0;
40std::string sanitise_filename(
const char *
const name,
bool *name_changed) {
44 for (
const char *p = name; *p !=
'\0'; p++) {
45 if (result.size() >= MAX_NAME_LENGTH) {
50 if (!(std::isalnum(
static_cast<unsigned char>(c)) || c ==
'.' || c ==
'_' || c ==
'-')) {
62 if (!has_bmp_suffix(result))
64 *name_changed = changed;
69std::string add_suffix(
const std::string &name,
unsigned attempt) {
71 snprintf(suffix,
sizeof(suffix),
"-%u", attempt);
72 auto dot =
name.rfind(
'.');
73 if (dot == std::string::npos)
75 return name.substr(0, dot) + suffix +
name.substr(dot);
80const char *snapshot_dir() {
81 const char *dir = getenv(
"ESPHOME_SNAPSHOT_DIR");
82 return dir !=
nullptr && dir[0] !=
'\0' ? dir : ESPHOME_SNAPSHOT_DIR;
87void put_le(uint8_t *&dest,
uint32_t value,
size_t bytes) {
88 for (
size_t i = 0; i !=
bytes; i++)
89 *dest++ =
static_cast<uint8_t
>(value >> (8 * i));
94size_t bmp_row_size(
int width) {
return (
static_cast<size_t>(width) * 3 + 3) & ~size_t{3}; }
99bool write_bmp(FILE *file,
const uint8_t *pixels,
int width,
int height,
size_t row_stride) {
100 const size_t row_size = bmp_row_size(width);
101 const size_t pixel_bytes = row_size * height;
103 uint8_t header[BMP_HEADER_SIZE];
104 uint8_t *
pos = header;
107 put_le(
pos,
static_cast<uint32_t>(BMP_HEADER_SIZE + pixel_bytes), 4);
109 put_le(
pos, BMP_HEADER_SIZE, 4);
110 put_le(
pos, BMP_INFO_HEADER_SIZE, 4);
114 put_le(
pos, BMP_BITS_PER_PIXEL, 2);
116 put_le(
pos,
static_cast<uint32_t>(pixel_bytes), 4);
122 if (fwrite(header, 1,
sizeof(header), file) !=
sizeof(header))
124 for (
int y = height - 1;
y >= 0;
y--) {
125 if (fwrite(pixels +
static_cast<size_t>(
y) * row_stride, 1, row_size, file) != row_size)
134bool write_snapshot_file(
const uint8_t *pixels,
int width,
int height,
size_t row_stride,
const std::string &name,
136 const std::string dir = snapshot_dir();
138 std::filesystem::create_directories(dir, ec);
140 ESP_LOGE(TAG,
"Could not create snapshot directory %s: %s", dir.c_str(), ec.message().c_str());
147 for (
unsigned attempt = 0; attempt < MAX_NAME_ATTEMPTS; attempt++) {
148 path = dir +
"/" + (attempt == 0 ?
name : add_suffix(name, attempt));
149 fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0644);
152 if (errno != EEXIST) {
153 ESP_LOGE(TAG,
"Could not create %s: %s", path.c_str(), strerror(errno));
159 ESP_LOGE(TAG,
"Snapshot %s already exists, not overwriting", path.c_str());
164 ESP_LOGE(TAG,
"Could not find an unused name for %s in %s",
name.c_str(), dir.c_str());
168 FILE *file = fdopen(fd,
"wb");
169 if (file ==
nullptr) {
170 ESP_LOGE(TAG,
"Could not open %s: %s", path.c_str(), strerror(errno));
172 ::unlink(path.c_str());
175 bool ok = write_bmp(file, pixels, width, height, row_stride);
176 int saved_errno = ok ? 0 : errno;
178 if (fclose(file) != 0) {
184 ESP_LOGE(TAG,
"Could not write %s: %s", path.c_str(), strerror(saved_errno));
186 ::unlink(path.c_str());
189 ESP_LOGI(TAG,
"Snapshot written to %s", path.c_str());
201 if (width <= 0 || height <= 0) {
202 ESP_LOGE(TAG,
"Snapshot requested but the display is %dx%d", width, height);
208 if (filename !=
nullptr) {
209 bool name_changed =
false;
210 name = sanitise_filename(filename, &name_changed);
211 exact = !name.empty();
213 ESP_LOGW(TAG,
"Requested snapshot name '%s' is not an acceptable file name, using '%s' instead", filename,
214 name.empty() ?
"a name made from the time" : name.c_str());
218 struct timespec now {};
219 if (clock_gettime(CLOCK_REALTIME, &now) != 0)
226 if (::strftime(stamp,
sizeof(stamp),
"%Y%m%d-%H%M%S", &tm_buf) == 0)
227 snprintf(stamp,
sizeof(stamp),
"unknown-time");
228 char buffer[MAX_NAME_LENGTH];
230 snprintf(buffer,
sizeof(buffer),
"%s-%s-%03ld.bmp", this->
snapshot_prefix_, stamp, now.tv_nsec / 1000000);
231 if (
written < 0 ||
static_cast<size_t>(
written) >=
sizeof(buffer)) {
232 ESP_LOGW(TAG,
"Could not build a timestamped snapshot name, using a fallback");
233 snprintf(buffer,
sizeof(buffer),
"snapshot.bmp");
240 const size_t row_stride = bmp_row_size(width);
241 auto pixels = std::make_unique<uint8_t[]>(row_stride * height);
244 return write_snapshot_file(pixels.get(), width, height, row_stride, name, exact);
virtual bool capture_bgr(uint8_t *dest, size_t row_stride)=0
Fill in the picture: three bytes per pixel in blue, green, red order, topmost row first,...
const char * snapshot_prefix_
virtual int snapshot_width()=0
Width of the picture in pixels.
virtual int snapshot_height()=0
Height of the picture in pixels.
static void log_action_failed()
Log that an action-triggered snapshot did not write a file.
bool take_snapshot(const char *filename)
Write the current picture to a BMP file in the snapshot directory.
std::vector< uint8_t > bytes
struct tm * localtime_r(const time_t *timer, struct tm *result)