I’ve been following the documentation as I want to take a snapshot on device and save it to an SD card.
So far I’ve tried to use lv_snapshot_take but it returns NULL all the time as there is not enough RAM in the region it is running in on the Teensy (RAM1)
So I tried to use lv_snapshot_take_to_buf but the watchdog on the Teensy is resetting the device due to a null pointer.
Below is a function I wrote to take a snapshot and save it to SD.
I call it where I want to take a snapshot and pass the lv_scr_act() object to it to take a full screen snapshot.
Can anyone provide an example of how they used lv_snapshot_take_to_buf to make sure i am using it correctly?
What MCU/Processor/Board and compiler are you using?
Teensy MicroMod (IMXRT1062)
What LVGL version are you using?
v8.1
Code to reproduce
#include "../lib/lvgl/lvgl.h"
#include "SdFat.h"
#include "Arduino.h"
SdFs sd;
FsFile file;
// Use Teensy SDIO
#define SD_CONFIG SdioConfig(FIFO_SDIO)
// Size to log 10 byte lines at 25 kHz for more than ten minutes.
#define LOG_FILE_SIZE (480*320*3) // 480px * 320px * 3bytes per pixle
#define LOG_FILENAME "snapshot.c"
DMAMEM uint16_t buff[LOG_FILE_SIZE/2]; //Buffer in RAM2 to place the snapshot
lv_img_dsc_t * img;
void takeSnapshot(lv_obj_t * screenshot){
arm_dcache_flush((uint16_t*)buff, sizeof(buff)); // always flush cache after writing to DMAMEM variable that will be accessed by DMA
lv_res_t snap = lv_snapshot_take_to_buf(screenshot, LV_IMG_CF_TRUE_COLOR_ALPHA, img, &buff, sizeof(buff));
if (snap == LV_RES_INV){
Serial.println("Snapshot failed");
}
else{
// Initialize the SD.
if (!sd.begin(SD_CONFIG)) {
sd.initErrorHalt(&Serial);
}
// Open or create file - truncate existing file.
if (!file.open(LOG_FILENAME, O_RDWR | O_CREAT | O_TRUNC)) {
Serial.println("open failed\n");
return;
}
// File must be pre-allocated to avoid huge
// delays searching for free clusters.
if (!file.preAllocate(LOG_FILE_SIZE)) {
Serial.println("preAllocate failed\n");
file.close();
return;
}
file.write((uint16_t*)buff);
file.close();
Serial.println("Snapshot saved");
}
}
I am not familiar with the Teensy or the lv_snapshot feature but looking at your code I think I can see an error, the image descriptor lv_img_dsc_t * img; shouldn’t be a pointer so this will cause your NULL pointer crash. Taking your code it might be worth trying it like this…
#include "../lib/lvgl/lvgl.h"
#include "SdFat.h"
#include "Arduino.h"
SdFs sd;
FsFile file;
// Use Teensy SDIO
#define SD_CONFIG SdioConfig(FIFO_SDIO)
// Size to log 10 byte lines at 25 kHz for more than ten minutes.
#define LOG_FILE_SIZE (480*320*3) // 480px * 320px * 3bytes per pixle
#define LOG_FILENAME "snapshot.c"
DMAMEM uint16_t buff[LOG_FILE_SIZE/2]; //Buffer in RAM2 to place the snapshot
lv_img_dsc_t img; // I believe this shouldn't be a pointer!!
void takeSnapshot(lv_obj_t * screenshot){
arm_dcache_flush((uint16_t*)buff, sizeof(buff)); // always flush cache after writing to DMAMEM variable that will be accessed by DMA
// In this line we add address of operator '&' to 'img'
lv_res_t snap = lv_snapshot_take_to_buf(screenshot, LV_IMG_CF_TRUE_COLOR_ALPHA, &img, &buff, sizeof(buff));
if (snap == LV_RES_INV){
Serial.println("Snapshot failed");
}
else{
// Initialize the SD.
if (!sd.begin(SD_CONFIG)) {
sd.initErrorHalt(&Serial);
}
// Open or create file - truncate existing file.
if (!file.open(LOG_FILENAME, O_RDWR | O_CREAT | O_TRUNC)) {
Serial.println("open failed\n");
return;
}
// File must be pre-allocated to avoid huge
// delays searching for free clusters.
if (!file.preAllocate(LOG_FILE_SIZE)) {
Serial.println("preAllocate failed\n");
file.close();
return;
}
file.write((uint16_t*)buff);
file.close();
Serial.println("Snapshot saved");
}
}
I would also check the buffer size carefully as it looks like it might be incorrect, if it is lv_snapshot_take_to_buf() should return LV_RES_INV so if that happens I would adjust the size of the buffer accordingly. If you have a debugger you can step into the lv_snapshot_take_to_buf() and see what value it is expecting for length if you are unsure about the size of the buffer.
@pete-pjb thank you for your response and suggestions.
I checked the size of the object that should be returned by calling lv_snapshot_buf_size_needed and it returned 460.8Kb. Being that the buffer value is a 2 byte unsigned integer, it needs to be half of that, no? Or should I just save an an uint8_t?
It’s not clear what kind of object is retuned to the buffer.
Thanks @pete-pjb that worked!
Now I need to tinker with getting it converted to a bitmap
I have been able to write it as a bitmap to the SD card but the image is flipped and mirrored, as well as mostly blue.
I know the bitmap config is wrong as it’s set up for RGB555 and not 565, and the pixels are definitely being read in the incorrect order
You can set the bitmap height to the negative value to make the client render it mirrored. The BMP spec orders the pixel rows bottom to top by default, but you can also put them top to bottom if you specify a negative height.
I’m using the BITMAPV2INFOHEADER BMP format while setting BI_BITFIELDS to 3 for RGB bitmasks like so:
If you write such header before dumping the LVGL pixels to the file, it should render fine on the client.
I’m using it to take local screenshots on flash and also send the screenshot to a browser via HTTP.
Thanks for the tip!
How do you suggest I dump the snapshot code to the card?
Just write the full array as is? Or loop through it with something like this?
uint16_t readPixA(int x, int y) { // get pixel color code in rgb565 format
static long pos = 0;
uint16_t r = (buff[pos+1] << 8) | buff[pos];
pos+=3;
return r;
}
void writeImageToFile(){
byte VH, VL;
int w = 480;
int h = 320;
int i, j = 0;
for (i = h; i > 0; i--) {
for (j = 0; j < w; j++) {
uint16_t rgb = readPixA(j,i); // get pix color in rgb565 format
VH = ((uint8_t)rgb & 0xFF00) >> 8; // High Byte
VL = (uint8_t)rgb & 0x00FF; // Low Byte
//RGB565 to RGB555 conversion... 555 is default for uncompressed BMP
//this conversion is from ...topic=177361.0 and has not been verified
VL = (VH << 7) | ((VL & 0xC0) >> 1) | (VL & 0x1f);
VH = VH >> 1;
//Write image data to file, low byte first
file.write(VL);
file.write(VH);
}
}
......
}
Here is my full snapshot code with the function+struct provided by @fvanroie
void takeSnapshot(lv_obj_t * screenshot){
//arm_dcache_flush((uint8_t*)buff, sizeof(buff)); // always flush cache after writing to DMAMEM variable that will be accessed by DMA
// In this line we add address of operator '&' to 'img'
Serial.printf("Buffer size: %d, Snapshot size required: %u \n", LOG_FILE_SIZE, lv_snapshot_buf_size_needed(lv_scr_act(), LV_IMG_CF_TRUE_COLOR_ALPHA));
lv_res_t snap = lv_snapshot_take_to_buf(screenshot, LV_IMG_CF_TRUE_COLOR_ALPHA, &snapshot_img, &buff, sizeof(buff));
if (snap == LV_RES_INV){
Serial.println("Snapshot failed");
}
else{
Serial.println("Snapshot Success");
uint8_t header[128];
Serial.println("Starting to build header");
gui_get_bitmap_header(header, sizeof(header));
Serial.println("Finished building header");
// Initialize the SD
if (!sd.begin(SD_CONFIG)) {
sd.initErrorHalt(&Serial);
}
// Open or create file - truncate existing file.
if (!file.open(LOG_FILENAME, O_RDWR | O_CREAT | O_TRUNC)) {
Serial.println("open failed\n");
return;
}
// File must be pre-allocated to avoid huge
// delays searching for free clusters.
if (!file.preAllocate(LOG_FILE_SIZE)) {
Serial.println("preAllocate failed\n");
file.close();
return;
}
Serial.println("Start write headers to SD");
file.write(header, 122);
Serial.println("Finish write headers to SD, and start write image to SD");
for(uint32_t i=0; i<LOG_FILE_SIZE;i++){
file.write(buff[i]);
}
Serial.println("Finished writing image data to SD");
file.close();
Serial.println("Snapshot saved to SD");
}
The idea is to craft the BMP header so that you can write the same VDB bitmap array as is (RGB565 left-to-right top-to-bottom) without conversion. This is the least taxing on the MCU. Using the information in the header any imaging application should be able to figure out how to display the image correctly. For me, all that was needed was a negative height to mirror it and the appropriate bit masks for each color channel.
Can you share the actual BMP file or the first 66 header bytes? I see you are writing 122 bytes to the header, but in my code the header only uses 66 bytes ( “BM” + sizeof(bmp_header_t)).
File pFileOut;
/** Take Screenshot.
*
* Flush buffer into a binary file.
*
* @note: data pixel should be formated to uint16_t RGB. Set by Bitmap header.
*
* @param[in] pFileName Output binary file name.
*
**/
void guiTakeScreenshot(const char* pFileName)
{
uint8_t buffer[sizeof(bmp_header_t) + 2]; // "BM" + struct size
gui_get_bitmap_header(buffer, sizeof(buffer)); // get header data
pFileOut = HASP_FS.open(pFileName, "w");
if(pFileOut) {
size_t len = pFileOut.write(buffer, sizeof(buffer));
if(len == sizeof(buffer)) {
LOG_VERBOSE(TAG_GUI, F("Bitmap header written"));
/* Refresh screen using a screenshot callback */
lv_disp_t* disp = lv_disp_get_default();
void (*flush_cb)(struct _disp_drv_t * disp_drv, const lv_area_t* area, lv_color_t* color_p);
flush_cb = disp->driver.flush_cb; // store original callback first
disp->driver.flush_cb = gui_screenshot_to_file; // replace the callback function
lv_obj_invalidate(lv_scr_act()); // invalidate whole screen area
lv_refr_now(NULL); // Force a call our screenshot callback
disp->driver.flush_cb = flush_cb; // restore the original callback
LOG_VERBOSE(TAG_GUI, F("Bitmap data flushed to %s"), pFileName);
} else {
LOG_ERROR(TAG_GUI, F("Data written does not match header size"));
}
pFileOut.close();
} else {
LOG_WARNING(TAG_GUI, F(D_FILE_SAVE_FAILED), pFileName);
}
}
/* Flush VDB bytes to both a file and screen */
/* This function temporarily replaces the flush_cb while writing the screenshot */
static void gui_screenshot_to_file(lv_disp_drv_t* disp, const lv_area_t* area, lv_color_t* color_p)
{
size_t len = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1); /* Number of pixels */
len *= sizeof(lv_color_t); /* Number of bytes */
size_t res = pFileOut.write((uint8_t*)color_p, len);
if(res != len) gui_flush_not_complete();
// indirect callback to flush screenshot data to the screen too, so both are in sync
Tft.flush_pixels(disp, area, color_p);
}
I don’t use lv_snapshot_take_to_buf but instead temporarily replace the flush_cb which uses the regular VDB to flush the bytes instead. It doesn’t need a separate buffer to snapshot the screen.
Attached is the generated Bitmap file.
I believe there are two issues:
the size of the file is too big. Image data is calculated at 480 * 320 * 3 where is should actually be
480 * 320 * 2 as we don’t need the alpha byte (correct me if i am wrong)
Only some of the pixel data is being pushed into the file, and not all of it.
The header is OK, except for bmp->bfOffBits must be 68 instead of 126 and bmp->biHeight should be -320.
The image data in the file is not the RGB565 pixel-array, so that’s why you see the stripes.
Technically that’s the image that is in the file, so the pixel array isn’t exported properly…
You don’t need to add an alpha byte, just plain RGB565 (as uint16 or byte array).
Opening the file in a binary like 010 Editor helps to verify the data. Now it is only a matter of writing the VDB buffer after the BMP header and it should work.
It was a buffer size issue, I explicitly put the buffer size value on the last parameter and it worked. Now to the next problem, I get a black screen when I try to capture lv_scr_act(). Going to debug a bit and see if i can finally get this working.
Ok I got it sorted out, now I have a proper image, however the colors are kinda messed up. @fvanroie do you know how to swap the two bytes colors from the BMP header? It seems that I have to swap the order from RGB to BGR.
Thanks, I already tried that, didn’t change anything. I think that field is ignored if you are working with >8bit, in my project I use 16 bits.
I upgraded to 8.3 same issue with the snapshot function, there is a color noise around the edges of objects, for example around this circle you can see the noise: