28constexpr size_t MAX_NAME_LENGTH = 200;
30constexpr unsigned MAX_NAME_ATTEMPTS = 1000;
32constexpr size_t BMP_HEADER_SIZE = 54;
33constexpr size_t BMP_INFO_HEADER_SIZE = 40;
34constexpr int BMP_BITS_PER_PIXEL = 24;
37constexpr int GIF_MAX_SIZE = 0xFFFF;
41bool has_extension(
const std::string &name,
const char *extension) {
42 return name.size() >= 4 && strcasecmp(name.c_str() + name.size() - 4, extension) == 0;
48std::string sanitise_filename(
const char *
const name,
const char *extension,
bool *name_changed) {
52 for (
const char *p = name; *p !=
'\0'; p++) {
53 if (result.size() >= MAX_NAME_LENGTH) {
58 if (!std::isalnum(
static_cast<unsigned char>(c)) && c !=
'.' && c !=
'_' && c !=
'-') {
70 if (!has_extension(result, extension))
72 *name_changed = changed;
77std::string add_suffix(
const std::string &name,
unsigned attempt) {
79 snprintf(suffix,
sizeof(suffix),
"-%u", attempt);
80 auto dot = name.rfind(
'.');
81 if (dot == std::string::npos)
83 return name.substr(0, dot) + suffix + name.substr(dot);
88const char *snapshot_dir() {
89 const char *dir = getenv(
"ESPHOME_SNAPSHOT_DIR");
90 return dir !=
nullptr && dir[0] !=
'\0' ? dir : ESPHOME_SNAPSHOT_DIR;
95void put_le(uint8_t *&dest,
uint32_t value,
size_t bytes) {
96 for (
size_t i = 0; i !=
bytes; i++)
97 *dest++ =
static_cast<uint8_t
>(value >> (8 * i));
102size_t padded_row_size(
int width) {
return (
static_cast<size_t>(width) * 3 + 3) & ~size_t{3}; }
107bool write_bmp(FILE *file,
const uint8_t *pixels,
int width,
int height,
size_t row_stride) {
108 const size_t row_size = padded_row_size(width);
109 const size_t pixel_bytes = row_size * height;
111 uint8_t header[BMP_HEADER_SIZE];
112 uint8_t *
pos = header;
115 put_le(
pos,
static_cast<uint32_t>(BMP_HEADER_SIZE + pixel_bytes), 4);
117 put_le(
pos, BMP_HEADER_SIZE, 4);
118 put_le(
pos, BMP_INFO_HEADER_SIZE, 4);
122 put_le(
pos, BMP_BITS_PER_PIXEL, 2);
124 put_le(
pos,
static_cast<uint32_t>(pixel_bytes), 4);
130 if (fwrite(header, 1,
sizeof(header), file) !=
sizeof(header))
132 for (
int y = height - 1;
y >= 0;
y--) {
133 if (fwrite(pixels +
static_cast<size_t>(
y) * row_stride, 1, row_size, file) != row_size)
142FILE *create_snapshot_file(
const std::string &name,
bool exact, std::string &path) {
143 const std::string dir = snapshot_dir();
145 std::filesystem::create_directories(dir, ec);
147 ESP_LOGE(TAG,
"Could not create snapshot directory %s: %s", dir.c_str(), ec.message().c_str());
153 for (
unsigned attempt = 0; attempt < MAX_NAME_ATTEMPTS; attempt++) {
154 path = dir +
"/" + (attempt == 0 ? name : add_suffix(name, attempt));
155 fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0644);
158 if (errno != EEXIST) {
159 ESP_LOGE(TAG,
"Could not create %s: %s", path.c_str(), strerror(errno));
165 ESP_LOGE(TAG,
"Snapshot %s already exists, not overwriting", path.c_str());
170 ESP_LOGE(TAG,
"Could not find an unused name for %s in %s", name.c_str(), dir.c_str());
174 FILE *file = fdopen(fd,
"wb");
175 if (file ==
nullptr) {
176 ESP_LOGE(TAG,
"Could not open %s: %s", path.c_str(), strerror(errno));
178 ::unlink(path.c_str());
186bool close_snapshot_file(FILE *file,
const std::string &path,
bool ok) {
187 int saved_errno = ok ? 0 : errno;
189 if (fclose(file) != 0) {
195 ESP_LOGE(TAG,
"Could not write %s: %s", path.c_str(), strerror(saved_errno));
196 ::unlink(path.c_str());
202bool write_snapshot_file(
const uint8_t *pixels,
int width,
int height,
size_t row_stride,
const std::string &name,
205 FILE *file = create_snapshot_file(name, exact, path);
208 if (!close_snapshot_file(file, path, write_bmp(file, pixels, width, height, row_stride)))
210 ESP_LOGI(TAG,
"Snapshot written to %s", path.c_str());
216std::string choose_name(
const char *filename,
const char *prefix,
const char *extension,
bool &exact) {
219 if (filename !=
nullptr) {
220 bool name_changed =
false;
221 name = sanitise_filename(filename, extension, &name_changed);
222 exact = !name.empty();
224 ESP_LOGW(TAG,
"Requested snapshot name '%s' is not an acceptable file name, using '%s' instead", filename,
225 name.empty() ? LOG_STR_LITERAL(
"a name made from the time") : name.c_str());
231 struct timespec now{};
232 if (clock_gettime(CLOCK_REALTIME, &now) != 0)
239 if (::strftime(stamp,
sizeof(stamp),
"%Y%m%d-%H%M%S", &tm_buf) == 0)
240 snprintf(stamp,
sizeof(stamp),
"unknown-time");
241 char buffer[MAX_NAME_LENGTH];
242 int written = snprintf(
buffer,
sizeof(
buffer),
"%s-%s-%03ld%s", prefix, stamp, now.tv_nsec / 1000000, extension);
244 ESP_LOGW(TAG,
"Could not build a timestamped snapshot name, using a fallback");
245 snprintf(
buffer,
sizeof(
buffer),
"snapshot%s", extension);
253struct Snapshot::Recording {
254 Recording(FILE *file, std::string path,
int width,
int height,
uint32_t frames,
float frame_rate)
256 path(
std::move(path)),
257 gif(file, width, height),
258 row_stride(padded_row_size(width)),
259 pixels(
std::make_unique<uint8_t[]>(this->row_stride * height)),
261 frame_rate(frame_rate) {}
264 if (this->file !=
nullptr) {
266 ::unlink(this->path.c_str());
272 return static_cast<uint32_t>(std::lround(frame * 1000.0 / this->frame_rate));
277 unsigned delay_centiseconds()
const {
278 auto centiseconds_at = [
this](
uint32_t frame) {
return std::lround(frame * 100.0 / this->frame_rate); };
279 return static_cast<unsigned>(centiseconds_at(this->frames_done + 1) - centiseconds_at(this->frames_done));
286 std::unique_ptr<uint8_t[]> pixels;
298 const char *name = filename[0] !=
'\0' ? filename :
nullptr;
301 ESP_LOGW(TAG,
"snapshot.take did not write a file");
308 if (width <= 0 || height <= 0) {
309 ESP_LOGE(TAG,
"Snapshot requested but the display is %dx%d", width, height);
314 const std::string name = choose_name(filename, this->
snapshot_prefix_,
".bmp", exact);
318 const size_t row_stride = padded_row_size(width);
319 auto pixels = std::make_unique<uint8_t[]>(row_stride * height);
322 return write_snapshot_file(pixels.get(), width, height, row_stride, name, exact);
326 if (this->recording_ !=
nullptr) {
327 ESP_LOGW(TAG,
"Already recording %s, not starting another", this->recording_->path.c_str());
330 if (frames == 0 || !(frame_rate > 0)) {
331 ESP_LOGE(TAG,
"Animation requested with %u frames at %.1f frames a second", frames, frame_rate);
336 if (width <= 0 || height <= 0 || width > GIF_MAX_SIZE || height > GIF_MAX_SIZE) {
337 ESP_LOGE(TAG,
"Animation requested but the display is %dx%d, and a GIF holds up to %d", width, height,
343 const std::string name = choose_name(filename, this->
snapshot_prefix_,
".gif", exact);
345 FILE *file = create_snapshot_file(name, exact, path);
348 auto recording = std::make_unique<Recording>(file, path, width, height, frames, frame_rate);
349 if (!recording->gif.write_header()) {
350 recording->file =
nullptr;
351 close_snapshot_file(file, path,
false);
354 ESP_LOGI(TAG,
"Recording %u frames at %.1f frames a second to %s", frames, frame_rate, path.c_str());
355 this->recording_ = std::move(recording);
356 this->recording_->start_ms =
millis();
357 return this->record_frame_();
360bool Snapshot::record_frame_() {
361 Recording &recording = *this->recording_;
362 if (!this->
capture_bgr(recording.pixels.get(), recording.row_stride)) {
364 this->recording_.reset();
368 bool ok = recording.gif.write_frame(recording.pixels.get(), recording.row_stride, recording.delay_centiseconds());
369 recording.frames_done++;
370 const bool finished = recording.frames_done == recording.frames;
372 ok = recording.gif.write_trailer();
373 if (!ok || finished) {
374 ok = close_snapshot_file(std::exchange(recording.file,
nullptr), recording.path, ok);
376 ESP_LOGI(TAG,
"Animation written to %s", recording.path.c_str());
378 this->recording_.reset();
383 const int32_t wait =
static_cast<int32_t
>(recording.start_ms + recording.due_ms(recording.frames_done) -
millis());
384 App.
scheduler.set_timeout(
this, wait > 0 ?
static_cast<uint32_t>(wait) : 0, [
this]() { this->record_frame_(); });
void take_snapshot_or_log(const char *filename, uint32_t frames, float frame_rate)
Take a snapshot for an automation: an empty name means a generated one, and a snapshot that wrote not...
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_
bool take_animation(const char *filename, uint32_t frames, float frame_rate)
Record what is shown as an animated GIF file in the snapshot directory.
virtual int snapshot_width()=0
Width of the picture in pixels.
virtual int snapshot_height()=0
Height of the picture in pixels.
bool take_snapshot(const char *filename)
Write the current picture to a BMP file in the snapshot directory.
std::vector< uint8_t > bytes
ESPHOME_LOG_TAG(TAG, "snapshot.display")
uint32_t IRAM_ATTR HOT millis()
Application App
Global storage of Application pointer - only one Application can exist.
struct tm * localtime_r(const time_t *timer, struct tm *result)