feat: Take screenshots (#759)
## Summary * **What is the goal of this PR?** Implements a take-screenshot feature * **What changes are included?** - Quick press Power button and Down button at the same time to take a screenshot - Screenshots are saved in `screenshots` folder ## Additional Context - Currently it does not use the device orientation. --- Example screenshots:  [screenshot-6771.bmp](https://github.com/user-attachments/files/25157071/screenshot-6771.bmp)  [screenshot-14158.bmp](https://github.com/user-attachments/files/25157073/screenshot-14158.bmp) ### AI Usage Did you use AI tools to help write this code? _** YES --------- Co-authored-by: Eliz Kilic <elizk@google.com> Co-authored-by: Xuan Son Nguyen <son@huggingface.co> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
This commit is contained in:
117
src/util/ScreenshotUtil.cpp
Normal file
117
src/util/ScreenshotUtil.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
#include "ScreenshotUtil.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <BitmapHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "Bitmap.h" // Required for BmpHeader struct definition
|
||||
|
||||
void ScreenshotUtil::takeScreenshot(GfxRenderer& renderer) {
|
||||
const uint8_t* fb = renderer.getFrameBuffer();
|
||||
if (fb) {
|
||||
String filename_str = "/screenshots/screenshot-" + String(millis()) + ".bmp";
|
||||
if (ScreenshotUtil::saveFramebufferAsBmp(filename_str.c_str(), fb, HalDisplay::DISPLAY_WIDTH,
|
||||
HalDisplay::DISPLAY_HEIGHT)) {
|
||||
LOG_DBG("SCR", "Screenshot saved to %s", filename_str.c_str());
|
||||
} else {
|
||||
LOG_ERR("SCR", "Failed to save screenshot");
|
||||
}
|
||||
} else {
|
||||
LOG_ERR("SCR", "Framebuffer not available");
|
||||
}
|
||||
|
||||
// Display a border around the screen to indicate a screenshot was taken
|
||||
if (renderer.storeBwBuffer()) {
|
||||
renderer.drawRect(6, 6, HalDisplay::DISPLAY_HEIGHT - 12, HalDisplay::DISPLAY_WIDTH - 12, 2, true);
|
||||
renderer.displayBuffer();
|
||||
delay(1000);
|
||||
renderer.restoreBwBuffer();
|
||||
renderer.displayBuffer(HalDisplay::RefreshMode::HALF_REFRESH);
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenshotUtil::saveFramebufferAsBmp(const char* filename, const uint8_t* framebuffer, int width, int height) {
|
||||
if (!framebuffer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Note: the width and height, we rotate the image 90d counter-clockwise to match the default display orientation
|
||||
int phyWidth = height;
|
||||
int phyHeight = width;
|
||||
|
||||
std::string path(filename);
|
||||
size_t last_slash = path.find_last_of('/');
|
||||
if (last_slash != std::string::npos) {
|
||||
std::string dir = path.substr(0, last_slash);
|
||||
if (!Storage.exists(dir.c_str())) {
|
||||
if (!Storage.mkdir(dir.c_str())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FsFile file;
|
||||
if (!Storage.openFileForWrite("SCR", filename, file)) {
|
||||
LOG_ERR("SCR", "Failed to save screenshot");
|
||||
return false;
|
||||
}
|
||||
|
||||
BmpHeader header;
|
||||
|
||||
createBmpHeader(&header, phyWidth, phyHeight);
|
||||
|
||||
bool write_error = false;
|
||||
if (file.write(reinterpret_cast<uint8_t*>(&header), sizeof(header)) != sizeof(header)) {
|
||||
write_error = true;
|
||||
}
|
||||
|
||||
if (write_error) {
|
||||
file.close();
|
||||
Storage.remove(filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t rowSizePadded = (phyWidth + 31) / 32 * 4;
|
||||
// Max row size for 480px width = 60 bytes; use fixed buffer to avoid VLA
|
||||
constexpr size_t kMaxRowSize = 64;
|
||||
if (rowSizePadded > kMaxRowSize) {
|
||||
LOG_ERR("SCR", "Row size %u exceeds buffer capacity", rowSizePadded);
|
||||
file.close();
|
||||
Storage.remove(filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
// rotate the image 90d counter-clockwise on-the-fly while writing to save memory
|
||||
uint8_t rowBuffer[kMaxRowSize];
|
||||
memset(rowBuffer, 0, rowSizePadded);
|
||||
|
||||
for (int outY = 0; outY < phyHeight; outY++) {
|
||||
for (int outX = 0; outX < phyWidth; outX++) {
|
||||
// 90d counter-clockwise: source (srcX, srcY)
|
||||
// BMP rows are bottom-to-top, so outY=0 is the bottom of the displayed image
|
||||
int srcX = width - 1 - outY; // phyHeight == width
|
||||
int srcY = phyWidth - 1 - outX; // phyWidth == height
|
||||
int fbIndex = srcY * (width / 8) + (srcX / 8);
|
||||
uint8_t pixel = (framebuffer[fbIndex] >> (7 - (srcX % 8))) & 0x01;
|
||||
rowBuffer[outX / 8] |= pixel << (7 - (outX % 8));
|
||||
}
|
||||
if (file.write(rowBuffer, rowSizePadded) != rowSizePadded) {
|
||||
write_error = true;
|
||||
break;
|
||||
}
|
||||
memset(rowBuffer, 0, rowSizePadded); // Clear the buffer for the next row
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
if (write_error) {
|
||||
Storage.remove(filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
8
src/util/ScreenshotUtil.h
Normal file
8
src/util/ScreenshotUtil.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
class ScreenshotUtil {
|
||||
public:
|
||||
static void takeScreenshot(GfxRenderer& renderer);
|
||||
static bool saveFramebufferAsBmp(const char* filename, const uint8_t* framebuffer, int width, int height);
|
||||
};
|
||||
Reference in New Issue
Block a user