ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
snapshot.cpp
Go to the documentation of this file.
1#ifdef USE_HOST
2#include "snapshot.h"
3#include "esphome/core/log.h"
4
5#include <fcntl.h>
6#include <strings.h>
7#include <unistd.h>
8#include <cctype>
9#include <cerrno>
10#include <cstdio>
11#include <cstring>
12#include <ctime>
13#include <filesystem>
14#include <memory>
15
16namespace esphome::snapshot {
17
18namespace {
19
20constexpr const char *const TAG = "snapshot";
21
22// Longest name we will build a path from. NAME_MAX is 255 and we may append a collision suffix.
23constexpr size_t MAX_NAME_LENGTH = 200;
24// Give up rather than spin forever if every candidate name is taken.
25constexpr unsigned MAX_NAME_ATTEMPTS = 1000;
26// A BMP file header followed by a BITMAPINFOHEADER, which is where the pixels start.
27constexpr size_t BMP_HEADER_SIZE = 54;
28constexpr size_t BMP_INFO_HEADER_SIZE = 40;
29constexpr int BMP_BITS_PER_PIXEL = 24;
30
33bool has_bmp_suffix(const std::string &name) {
34 return name.size() >= 4 && strcasecmp(name.c_str() + name.size() - 4, ".bmp") == 0;
35}
36
40std::string sanitise_filename(const char *const name, bool *name_changed) {
41 std::string result;
42 bool all_dots = true;
43 bool changed = false;
44 for (const char *p = name; *p != '\0'; p++) {
45 if (result.size() >= MAX_NAME_LENGTH) {
46 changed = true;
47 break;
48 }
49 char c = *p;
50 if (!(std::isalnum(static_cast<unsigned char>(c)) || c == '.' || c == '_' || c == '-')) {
51 c = '_';
52 changed = true;
53 }
54 if (c != '.')
55 all_dots = false;
56 result.push_back(c);
57 }
58 if (all_dots) {
59 *name_changed = true;
60 return "";
61 }
62 if (!has_bmp_suffix(result))
63 result += ".bmp";
64 *name_changed = changed;
65 return result;
66}
67
69std::string add_suffix(const std::string &name, unsigned attempt) {
70 char suffix[12];
71 snprintf(suffix, sizeof(suffix), "-%u", attempt);
72 auto dot = name.rfind('.');
73 if (dot == std::string::npos)
74 return name + suffix;
75 return name.substr(0, dot) + suffix + name.substr(dot);
76}
77
80const char *snapshot_dir() {
81 const char *dir = getenv("ESPHOME_SNAPSHOT_DIR"); // NOLINT(concurrency-mt-unsafe)
82 return dir != nullptr && dir[0] != '\0' ? dir : ESPHOME_SNAPSHOT_DIR;
83}
84
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));
90}
91
94size_t bmp_row_size(int width) { return (static_cast<size_t>(width) * 3 + 3) & ~size_t{3}; }
95
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;
102
103 uint8_t header[BMP_HEADER_SIZE];
104 uint8_t *pos = header;
105 *pos++ = 'B';
106 *pos++ = 'M';
107 put_le(pos, static_cast<uint32_t>(BMP_HEADER_SIZE + pixel_bytes), 4);
108 put_le(pos, 0, 4); // reserved
109 put_le(pos, BMP_HEADER_SIZE, 4);
110 put_le(pos, BMP_INFO_HEADER_SIZE, 4);
111 put_le(pos, static_cast<uint32_t>(width), 4);
112 put_le(pos, static_cast<uint32_t>(height), 4);
113 put_le(pos, 1, 2); // one plane
114 put_le(pos, BMP_BITS_PER_PIXEL, 2);
115 put_le(pos, 0, 4); // not compressed
116 put_le(pos, static_cast<uint32_t>(pixel_bytes), 4);
117 put_le(pos, 0, 4); // pixels per metre across, unspecified
118 put_le(pos, 0, 4); // pixels per metre down, unspecified
119 put_le(pos, 0, 4); // no palette
120 put_le(pos, 0, 4); // so no palette entry matters more than another
121
122 if (fwrite(header, 1, sizeof(header), file) != sizeof(header))
123 return false;
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)
126 return false;
127 }
128 return true;
129}
130
134bool write_snapshot_file(const uint8_t *pixels, int width, int height, size_t row_stride, const std::string &name,
135 bool exact) {
136 const std::string dir = snapshot_dir();
137 std::error_code ec;
138 std::filesystem::create_directories(dir, ec);
139 if (ec) {
140 ESP_LOGE(TAG, "Could not create snapshot directory %s: %s", dir.c_str(), ec.message().c_str());
141 return false;
142 }
143
144 // O_EXCL guarantees we never write over a file that is already there.
145 std::string path;
146 int fd = -1;
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);
150 if (fd >= 0)
151 break;
152 if (errno != EEXIST) {
153 ESP_LOGE(TAG, "Could not create %s: %s", path.c_str(), strerror(errno));
154 return false;
155 }
156 if (exact) {
157 // The caller asked for this exact name, so silently writing somewhere else would be worse
158 // than failing - a test asserting on the path would pick up a stale file.
159 ESP_LOGE(TAG, "Snapshot %s already exists, not overwriting", path.c_str());
160 return false;
161 }
162 }
163 if (fd < 0) {
164 ESP_LOGE(TAG, "Could not find an unused name for %s in %s", name.c_str(), dir.c_str());
165 return false;
166 }
167
168 FILE *file = fdopen(fd, "wb");
169 if (file == nullptr) {
170 ESP_LOGE(TAG, "Could not open %s: %s", path.c_str(), strerror(errno));
171 ::close(fd);
172 ::unlink(path.c_str());
173 return false;
174 }
175 bool ok = write_bmp(file, pixels, width, height, row_stride);
176 int saved_errno = ok ? 0 : errno;
177 // Closing can fail in its own right - the last of the data is still on its way out.
178 if (fclose(file) != 0) {
179 if (ok)
180 saved_errno = errno;
181 ok = false;
182 }
183 if (!ok) {
184 ESP_LOGE(TAG, "Could not write %s: %s", path.c_str(), strerror(saved_errno));
185 // Leave no truncated file behind - it would block a retry under the same name.
186 ::unlink(path.c_str());
187 return false;
188 }
189 ESP_LOGI(TAG, "Snapshot written to %s", path.c_str());
190 return true;
191}
192
193} // namespace
194
195// helper function since ESP_LOGW is disallowed in a header file
196void Snapshot::log_action_failed() { ESP_LOGW(TAG, "snapshot.take did not write a file"); }
197
198bool Snapshot::take_snapshot(const char *filename) {
199 const int width = this->snapshot_width();
200 const int height = this->snapshot_height();
201 if (width <= 0 || height <= 0) {
202 ESP_LOGE(TAG, "Snapshot requested but the display is %dx%d", width, height);
203 return false;
204 }
205
206 std::string name;
207 bool exact = false;
208 if (filename != nullptr) {
209 bool name_changed = false;
210 name = sanitise_filename(filename, &name_changed);
211 exact = !name.empty();
212 if (name_changed) {
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());
215 }
216 }
217 if (name.empty()) {
218 struct timespec now {};
219 if (clock_gettime(CLOCK_REALTIME, &now) != 0)
220 now = {};
221 struct tm tm_buf {};
222 if (localtime_r(&now.tv_sec, &tm_buf) == nullptr)
223 tm_buf = {};
224 char stamp[32]{};
225 // ::strftime to be sure of the one from <ctime>; display has an unrelated member of that name
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];
229 int written =
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");
234 }
235 name = buffer;
236 }
237
238 // Rows are padded out to a multiple of four bytes, as the file wants them, so each one can be
239 // written straight from the buffer. Zeroed on allocation, which is what the padding must be.
240 const size_t row_stride = bmp_row_size(width);
241 auto pixels = std::make_unique<uint8_t[]>(row_stride * height);
242 if (!this->capture_bgr(pixels.get(), row_stride))
243 return false;
244 return write_snapshot_file(pixels.get(), width, height, row_stride, name, exact);
245}
246
247} // namespace esphome::snapshot
248#endif
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_
Definition snapshot.h:50
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.
Definition snapshot.cpp:196
bool take_snapshot(const char *filename)
Write the current picture to a BMP file in the snapshot directory.
Definition snapshot.cpp:198
const char *const name
Definition lsm6ds.cpp:11
std::vector< uint8_t > bytes
Definition sml_parser.h:12
const char *const TAG
Definition spi.cpp:7
size_t size_t pos
Definition helpers.h:1092
int written
Definition helpers.h:1099
struct tm * localtime_r(const time_t *timer, struct tm *result)
Definition posix_tz.cpp:295
static void uint32_t
uint16_t y
Definition tt21100.cpp:6