Merge pull request #126 from thooge/screenshot

Screenshot feature
This commit is contained in:
Norbert Walter 2024-12-24 00:06:28 +01:00 committed by GitHub
commit df5ff1c91a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 498 additions and 18 deletions

View File

@ -11,6 +11,7 @@
#include "Pagedata.h"
#include "OBP60Hardware.h"
#include "OBP60Extensions.h"
#include "imglib.h"
// Character sets
#include "Ubuntu_Bold8pt7b.h"
@ -404,4 +405,44 @@ void generatorGraphic(uint x, uint y, int pcolor, int bcolor){
getdisplay().print("G");
}
#endif
// Function to handle HTTP image request
void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUMBER], AsyncWebServerRequest *request) {
GwLog *logger = api->getLogger();
String imgformat = api->getConfig()->getConfigItem(api->getConfig()->imageFormat,true)->asString();
imgformat.toLowerCase();
String filename = "Page" + String(*pageno) + "_" + pages[*pageno].description->pageName + "." + imgformat;
logger->logDebug(GwLog::LOG,"handle image request [%s]: %s", imgformat, filename);
uint8_t *fb = getdisplay().getBuffer(); // EPD framebuffer
std::vector<uint8_t> imageBuffer; // image in webserver transferbuffer
String mimetype;
if (imgformat == "gif") {
// GIF is commpressed with LZW, so small
mimetype = "image/gif";
if (!createGIF(fb, &imageBuffer, GxEPD_WIDTH, GxEPD_HEIGHT)) {
logger->logDebug(GwLog::LOG,"GIF creation failed: Hashtable init error!");
return;
}
}
else if (imgformat == "bmp") {
// Microsoft BMP bitmap
mimetype = "image/bmp";
createBMP(fb, &imageBuffer, GxEPD_WIDTH, GxEPD_HEIGHT);
}
else {
// PBM simple portable bitmap
mimetype = "image/x-portable-bitmap";
createPBM(fb, &imageBuffer, GxEPD_WIDTH, GxEPD_HEIGHT);
}
AsyncWebServerResponse *response = request->beginResponse_P(200, mimetype, (const uint8_t*)imageBuffer.data(), imageBuffer.size());
response->addHeader("Content-Disposition", "inline; filename=" + filename);
request->send(response);
imageBuffer.clear();
}
#endif

View File

@ -70,4 +70,6 @@ void solarGraphic(uint x, uint y, int pcolor, int bcolor); // S
void generatorGraphic(uint x, uint y, int pcolor, int bcolor); // Generator graphic with fill level
void startLedTask(GwApi *api);
#endif
void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUMBER], AsyncWebServerRequest *request);
#endif

View File

@ -39,11 +39,6 @@
#define OBP_SPI_DIN 48
#define SHOW_TIME 6000 // Show time in [ms] for logo and WiFi QR code
#define FULL_REFRESH_TIME 600 // Refresh cycle time in [s][600...3600] for full display update (very important healcy function)
#define MAX_PAGE_NUMBER 10 // Max number of pages for show data
#define FONT1 "Ubuntu_Bold8pt7b"
#define FONT2 "Ubuntu_Bold24pt7b"
#define FONT3 "Ubuntu_Bold32pt7b"
#define FONT4 "DSEG7Classic_BoldItalic80pt7b"
// GPS (NEO-6M, NEO-M8N, ATGM336H)
#define OBP_GPS_RX 2

View File

@ -4,6 +4,8 @@
#include <functional>
#include <vector>
#define MAX_PAGE_NUMBER 10 // Max number of pages for show data
typedef std::vector<GwApi::BoatValue *> ValueList;
typedef struct{
String pageName;
@ -114,6 +116,13 @@ class PageDescription{
}
};
class PageStruct{
public:
Page *page=NULL;
PageData parameters;
PageDescription *description=NULL;
};
// Structure for formated boat values
typedef struct{
double value;

View File

@ -898,6 +898,22 @@
"obp60":"true"
}
},
{
"name": "imageFormat",
"label": "Screenshot Format",
"type": "list",
"default":"PBM",
"description": "Graphics file format for screenshots [GIF|PBM|BMP]",
"list": [
{"l":"Compressed image (GIF)","v":"GIF"},
{"l":"Portable bitmap (PBM)","v":"PBM"},
{"l":"Windows bitmap (BMP)","v":"BMP"}
],
"category":"OBP60 Pages",
"capabilities": {
"obp60":"true"
}
},
{
"name": "page1type",
"label": "Type",

347
lib/obp60task/imglib.cpp Normal file
View File

@ -0,0 +1,347 @@
/******************************************************************************
* Image functions:
* - Convert a 1bit framebuffer in RAM to
* - GIF, compressed, based on giflib and gif_hash
* - PBM, portable bitmap, very simple copy
* - BMP, bigger with a little bit fiddling around
*
* SPDX-License-Identifier: MIT
*****************************************************************************/
#include <Arduino.h> // needed for PROGMEM
#include <vector>
#include "GwLog.h" // needed for logger
#include "imglib.h"
GifFilePrivateType gifprivate;
void ClearHashTable(GifHashTableType *HashTable) {
memset(HashTable->HTable, 0xFF, HT_SIZE * sizeof(uint32_t));
}
GifHashTableType *InitHashTable(void) {
GifHashTableType *HashTable;
if ((HashTable = (GifHashTableType *)ps_malloc(sizeof(GifHashTableType))) == NULL) {
return NULL;
}
ClearHashTable(HashTable);
return HashTable;
}
static int KeyItem(uint32_t Item) {
return ((Item >> 12) ^ Item) & HT_KEY_MASK;
}
void InsertHashTable(GifHashTableType *HashTable, uint32_t Key, int Code) {
int HKey = KeyItem(Key);
uint32_t *HTable = HashTable->HTable;
while (HT_GET_KEY(HTable[HKey]) != 0xFFFFFL) {
HKey = (HKey + 1) & HT_KEY_MASK;
}
HTable[HKey] = HT_PUT_KEY(Key) | HT_PUT_CODE(Code);
}
int ExistsHashTable(GifHashTableType *HashTable, uint32_t Key) {
int HKey = KeyItem(Key);
uint32_t *HTable = HashTable->HTable, HTKey;
while ((HTKey = HT_GET_KEY(HTable[HKey])) != 0xFFFFFL) {
if (Key == HTKey) {
return HT_GET_CODE(HTable[HKey]);
}
HKey = (HKey + 1) & HT_KEY_MASK;
}
return -1;
}
/******************************************************************************
Put 2 bytes (a word) into the given file in little-endian order:
******************************************************************************/
void GifPutWord(std::vector<uint8_t>* gifBuffer, uint16_t Word) {
/*cuint8_t c[2];
[0] = LOBYTE(Word);
c[1] = HIBYTE(Word);
gifBuffer->push_back(c[0]);
gifBuffer->push_back(c[1]); */
gifBuffer->push_back(LOBYTE(Word));
gifBuffer->push_back(HIBYTE(Word));
}
/******************************************************************************
This routines buffers the given characters until 255 characters are ready
to be output. If Code is equal to -1 the buffer is flushed (EOF).
The buffer is Dumped with first byte as its size, as GIF format requires.
******************************************************************************/
void GifBufferedOutput(std::vector<uint8_t>* gifBuffer, GifByteType *Buf, int c) {
if (c == FLUSH_OUTPUT) {
// Flush everything out.
for (int i = 0; i < Buf[0] + 1; i++) {
gifBuffer->push_back(Buf[i]);
}
// Mark end of compressed data, by an empty block (see GIF doc):
Buf[0] = 0;
gifBuffer->push_back(0);
} else {
if (Buf[0] == 255) {
// Dump out this buffer - it is full:
for (int i = 0; i < Buf[0] + 1; i++) {
gifBuffer->push_back(Buf[i]);
}
Buf[0] = 0;
}
Buf[++Buf[0]] = c;
}
}
/******************************************************************************
The LZ compression output routine:
This routine is responsible for the compression of the bit stream into
8 bits (bytes) packets.
******************************************************************************/
void GifCompressOutput(std::vector<uint8_t>* gifBuffer, const int Code) {
if (Code == FLUSH_OUTPUT) {
while (gifprivate.CrntShiftState > 0) {
// Get Rid of what is left in DWord, and flush it.
GifBufferedOutput(gifBuffer, gifprivate.Buf, gifprivate.CrntShiftDWord & 0xff);
gifprivate.CrntShiftDWord >>= 8;
gifprivate.CrntShiftState -= 8;
}
gifprivate.CrntShiftState = 0; // For next time.
GifBufferedOutput(gifBuffer, gifprivate.Buf, FLUSH_OUTPUT);
} else {
gifprivate.CrntShiftDWord |= ((long)Code) << gifprivate.CrntShiftState;
gifprivate.CrntShiftState += gifprivate.RunningBits;
while (gifprivate.CrntShiftState >= 8) {
// Dump out full bytes:
GifBufferedOutput(gifBuffer, gifprivate.Buf, gifprivate.CrntShiftDWord & 0xff);
gifprivate.CrntShiftDWord >>= 8;
gifprivate.CrntShiftState -= 8;
}
}
/* If code cannt fit into RunningBits bits, must raise its size. Note */
/* however that codes above LZ_MAX_CODE are used for special signaling. */
if (gifprivate.RunningCode >= gifprivate.MaxCode1 && Code <= LZ_MAX_CODE) {
gifprivate.MaxCode1 = 1 << ++gifprivate.RunningBits;
}
}
/******************************************************************************
Setup the LZ compression for this image:
******************************************************************************/
void GifSetupCompress(std::vector<uint8_t>* gifBuffer) {
gifBuffer->push_back(0x02);// Bits per pixel wit minimum 2
gifprivate.Buf[0] = 0; // Nothing was output yet
gifprivate.BitsPerPixel = 2; // Minimum is 2
gifprivate.ClearCode = (1 << 2);
gifprivate.EOFCode = gifprivate.ClearCode + 1;
gifprivate.RunningCode = gifprivate.EOFCode + 1;
gifprivate.RunningBits = 2 + 1; // Number of bits per code
gifprivate.MaxCode1 = 1 << gifprivate.RunningBits; // Max. code + 1
gifprivate.CrntCode = FIRST_CODE; // Signal that this is first one!
gifprivate.CrntShiftState = 0; // No information in CrntShiftDWord
gifprivate.CrntShiftDWord = 0;
GifCompressOutput(gifBuffer, gifprivate.ClearCode);
}
void createGifHeader(std::vector<uint8_t>* gifBuffer, uint16_t width, uint16_t height) {
// SCREEN DESCRIPTOR
gifBuffer->push_back('G');
gifBuffer->push_back('I');
gifBuffer->push_back('F');
gifBuffer->push_back('8');
gifBuffer->push_back('7');
gifBuffer->push_back('a');
GifPutWord(gifBuffer, width);
GifPutWord(gifBuffer, height);
gifBuffer->push_back(0x80 | (1 << 4));
gifBuffer->push_back(0x00); // Index into the ColorTable for background color
gifBuffer->push_back(0x00); // Pixel Aspect Ratio
// Colormap
gifBuffer->push_back(0xff); // Color 0
gifBuffer->push_back(0xff);
gifBuffer->push_back(0xff);
gifBuffer->push_back(0x00); // Color 1
gifBuffer->push_back(0x00);
gifBuffer->push_back(0x00);
// IMAGE DESCRIPTOR
gifBuffer->push_back(DESCRIPTOR_INTRODUCER);
GifPutWord(gifBuffer, 0);
GifPutWord(gifBuffer, 0);
GifPutWord(gifBuffer, width);
GifPutWord(gifBuffer, height);
gifBuffer->push_back(0x00); // No colormap here , we use the global one
}
/******************************************************************************
The LZ compression routine:
This version compresses the given buffer Line of length LineLen.
This routine can be called a few times (one per scan line, for example), in
order to complete the whole image.
******************************************************************************/
void GifCompressLine(std::vector<uint8_t>* gifBuffer, const GifPixelType *Line, const int LineLen) {
int i = 0, CrntCode;
GifHashTableType *HashTable;
HashTable = gifprivate.HashTable;
if (gifprivate.CrntCode == FIRST_CODE) { // Its first time!
CrntCode = Line[i++];
} else {
CrntCode =
gifprivate.CrntCode; // Get last code in compression
}
while (i < LineLen) { // Decode LineLen items
GifPixelType Pixel = Line[i++]; // Get next pixel from stream.
/* Form a new unique key to search hash table for the code
* combines CrntCode as Prefix string with Pixel as postfix
* char.
*/
int NewCode;
unsigned long NewKey = (((uint32_t)CrntCode) << 8) + Pixel;
if ((NewCode = ExistsHashTable(HashTable, NewKey)) >= 0) {
/* This Key is already there, or the string is old one,
* so simple take new code as our CrntCode:
*/
CrntCode = NewCode;
} else {
/* Put it in hash table, output the prefix code, and
* make our CrntCode equal to Pixel.
*/
GifCompressOutput(gifBuffer, CrntCode);
CrntCode = Pixel;
/* If however the HashTable if full, we send a clear
* first and Clear the hash table.
*/
if (gifprivate.RunningCode >= LZ_MAX_CODE) {
// Time to do some clearance:
GifCompressOutput(gifBuffer, gifprivate.ClearCode);
gifprivate.RunningCode = gifprivate.EOFCode + 1;
gifprivate.RunningBits = gifprivate.BitsPerPixel + 1;
gifprivate.MaxCode1 = 1 << gifprivate.RunningBits;
ClearHashTable(HashTable);
} else {
// Put this unique key with its relative Code in hash table:
InsertHashTable(HashTable, NewKey, gifprivate.RunningCode++);
}
}
}
// Preserve the current state of the compression algorithm:
gifprivate.CrntCode = CrntCode;
if (gifprivate.PixelCount == 0) {
// We are done - output last Code and flush output buffers:
GifCompressOutput(gifBuffer, CrntCode);
GifCompressOutput(gifBuffer, gifprivate.EOFCode);
GifCompressOutput(gifBuffer, FLUSH_OUTPUT);
}
}
bool createBMP(uint8_t *frameBuffer, std::vector<uint8_t>* imageBuffer, uint16_t width, uint16_t height) {
// For BMP the line size has to be a multiple of 4 bytes.
// So padding is needed. Also the lines have to be in reverded
// order compared to plain buffer
// BMP header for black-and-white image (1 bit per pixel)
const uint8_t bmp_header[] PROGMEM = {
// BITMAPFILEHEADER (14 Bytes)
0x42, 0x4D, // bfType: 'BM' signature
0x2e, 0x3d, 0x00, 0x00, // bfSize: file size in bytes
0x00, 0x00, // bfReserved1
0x00, 0x00, // bfReserved2
0x3e, 0x00, 0x00, 0x00, // bfOffBits: offset in bytes to pixeldata
// BITMAPINFOHEADER (40 Bytes)
0x28, 0x00, 0x00, 0x00, // biSize: DIB header size
(uint8_t)LOBYTE(width), (uint8_t)HIBYTE(width), 0x00, 0x00, // biWidth
(uint8_t)LOBYTE(height), (uint8_t)HIBYTE(height), 0x00, 0x00, // biHeight
0x01, 0x00, // biPlanes: Number of color planes (1)
0x01, 0x00, // biBitCount: Color depth (1 bit per pixel)
0x00, 0x00, 0x00, 0x00, // biCompression: Compression (none)
0xf0, 0x3c, 0x00, 0x00, // biSizeImage: Image data size (calculate)
0x13, 0x0b, 0x00, 0x00, // biXPelsPerMeter: Horizontal resolution (2835 pixels/meter)
0x13, 0x0b, 0x00, 0x00, // biYPelsPerMeter: Vertical resolution (2835 pixels/meter)
0x02, 0x00, 0x00, 0x00, // biClrUsed: Colors in color palette (2)
0x00, 0x00, 0x00, 0x00, // biClrImportant: Important colors (all)
// PALETTE: COLORTRIPLES of RGBQUAD (n * 4 Bytes)
0x00, 0x00, 0x00, 0x00, // Color palette: Black
0xff, 0xff, 0xff, 0x00 // Color palette: White
};
size_t bmp_headerSize = sizeof(bmp_header);
size_t lineSize = (width / 8);
size_t paddingSize = 0;
if (lineSize % 4 != 0) {
paddingSize = 4 - lineSize % 4;
}
size_t imageSize = bmp_headerSize + (lineSize + paddingSize) * height;
imageBuffer->resize(imageSize);
memcpy(imageBuffer->data(), bmp_header, bmp_headerSize);
for (int y = 0; y < height; y++) {
uint8_t* srcRow = frameBuffer + (y * lineSize);
uint8_t* destRow = imageBuffer->data() + bmp_headerSize + ((height - 1 - y) * (lineSize + paddingSize));
memcpy(destRow, srcRow, lineSize);
for (int j = 0; j < paddingSize; j++) {
destRow[lineSize + j] = 0x00;
}
}
return true;
}
bool createPBM(uint8_t *frameBuffer, std::vector<uint8_t>* imageBuffer, uint16_t width, uint16_t height) {
// creates binary PBM image inside imagebuffer
// returns bytesize of created image
const char pbm_header[] PROGMEM = "P4\n#Created by OBP60\n400 300\n";
size_t pbm_headerSize = sizeof(pbm_header) - 1; // We don't want trailing zero
size_t imageSize = pbm_headerSize + width / 8 * height;
imageBuffer->resize(imageSize);
memcpy(imageBuffer->data(), pbm_header, pbm_headerSize);
memcpy(imageBuffer->data() + pbm_headerSize, frameBuffer, width / 8 * height);
return true;
}
bool createGIF(uint8_t *framebuffer, std::vector<uint8_t>* gifBuffer, uint16_t width, uint16_t height) {
size_t imageSize = 0;
uint16_t bufOffset = 0; // Offset into imageBuffer for next write access
gifprivate.HashTable = InitHashTable();
if (gifprivate.HashTable == NULL) {
return false;
}
gifprivate.PixelCount = width * height;
createGifHeader(gifBuffer, width, height);
// Reset compress algorithm parameters.
GifSetupCompress(gifBuffer);
gifBuffer->reserve(4096); // to avoid lots of alloactions
GifPixelType line[width];
for (int y = 0; y < height; y++) {
// convert uint8_t pixels to single pixels
for (int x = 0; x < width; x++) {
int byteIndex = (y * width + x) / 8;
uint8_t bitIndex = 7 - ((y * width + x) % 8);
line[x] = (framebuffer[byteIndex] & (uint8_t)(1 << bitIndex)) == 0;
}
gifprivate.PixelCount -= width;
GifCompressLine(gifBuffer, line, width);
}
gifBuffer->push_back(TERMINATOR_INTRODUCER);
free((GifHashTableType *)gifprivate.HashTable);
return true;
}

70
lib/obp60task/imglib.h Normal file
View File

@ -0,0 +1,70 @@
/******************************************************************************
*
* imglib.h
*
* SPDX-License-Identifier: MIT
*****************************************************************************/
#ifndef _IMGLIB_H_
#define _IMGLIB_H_ 1
// extract bytes from an unsigned word
#define LOBYTE(x) ((x)&0xff)
#define HIBYTE(x) (((x) >> 8) & 0xff)
// GIF encoding constants
#define DESCRIPTOR_INTRODUCER 0x2c
#define TERMINATOR_INTRODUCER 0x3b
#define LZ_MAX_CODE 4095 // Biggest code possible in 12 bits
#define LZ_BITS 12
#define FLUSH_OUTPUT 4096 // Impossible code, to signal flush
#define FIRST_CODE 4097 // Impossible code, to signal first
#define NO_SUCH_CODE 4098 // Impossible code, to signal empty
// magfic constants and declarations for GIF LZW
#define HT_SIZE 8192 /* 12bits = 4096 or twice as big! */
#define HT_KEY_MASK 0x1FFF /* 13bits keys */
#define HT_KEY_NUM_BITS 13 /* 13bits keys */
#define HT_MAX_KEY 8191 /* 13bits - 1, maximal code possible */
#define HT_MAX_CODE 4095 /* Biggest code possible in 12 bits. */
/* The 32 bits of the long are divided into two parts for the key & code: */
/* 1. The code is 12 bits as our compression algorithm is limited to 12bits */
/* 2. The key is 12 bits Prefix code + 8 bit new char or 20 bits. */
/* The key is the upper 20 bits. The code is the lower 12. */
#define HT_GET_KEY(l) (l >> 12)
#define HT_GET_CODE(l) (l & 0x0FFF)
#define HT_PUT_KEY(l) (l << 12)
#define HT_PUT_CODE(l) (l & 0x0FFF)
typedef unsigned char GifPixelType;
typedef unsigned char GifByteType;
typedef struct GifHashTableType {
uint32_t HTable[HT_SIZE];
} GifHashTableType;
typedef struct GifFilePrivateType {
uint8_t BitsPerPixel; // Bits per pixel (Codes uses at least this + 1)
uint16_t ClearCode; // The CLEAR LZ code
uint16_t EOFCode; // The EOF LZ code
uint16_t RunningCode; // The next code algorithm can generate
uint16_t RunningBits; // The number of bits required to represent RunningCode
uint16_t MaxCode1; // 1 bigger than max. possible code, in RunningBits bits
uint16_t LastCode; // The code before the current code.
uint16_t CrntCode; // Current algorithm code
uint16_t CrntShiftState; // Number of bits in CrntShiftDWord
uint32_t CrntShiftDWord; // For bytes decomposition into codes
uint32_t PixelCount; // Number of pixels in image
GifByteType Buf[256]; // Compressed input is buffered here
GifHashTableType *HashTable;
} GifFilePrivateType;
bool createGIF(uint8_t *framebuffer, std::vector<uint8_t>* imageBuffer, uint16_t width, uint16_t height);
bool createBMP(uint8_t *framebuffer, std::vector<uint8_t>* imageBuffer, uint16_t width, uint16_t height);
bool createPBM(uint8_t *framebuffer, std::vector<uint8_t>* gifBuffer, uint16_t width, uint16_t height);
#endif /* _IMGLIB_H */

View File

@ -183,13 +183,6 @@ class PageList{
}
};
class PageStruct{
public:
Page *page=NULL;
PageData parameters;
PageDescription *description=NULL;
};
/**
* this function will add all the pages we know to the pagelist
* each page should have defined a registerXXXPage variable of type
@ -353,6 +346,10 @@ void OBP60Task(GwApi *api){
// Init pages
int numPages=1;
PageStruct pages[MAX_PAGE_NUMBER];
// Set start page
int pageNumber = int(api->getConfig()->getConfigItem(api->getConfig()->startPage,true)->asInt()) - 1;
int lastPage=pageNumber;
BoatValueList boatValues; //all the boat values for the api query
//commonData.distanceformat=config->getString(xxx);
//add all necessary data to common data
@ -395,6 +392,11 @@ void OBP60Task(GwApi *api){
pages[i].parameters.values.push_back(value);
}
}
api->registerRequestHandler("screenshot", [api, &pageNumber, pages](AsyncWebServerRequest *request) {
doImageRequest(api, &pageNumber, pages, request);
});
//now we have prepared the page data
//we start a separate task that will fetch our keys...
MyData allParameters;
@ -431,9 +433,6 @@ void OBP60Task(GwApi *api){
GwApi::BoatValue *hdop = boatValues.findValueOrCreate("HDOP"); // Load GpsHDOP
LOG_DEBUG(GwLog::LOG,"obp60task: start mainloop");
// Set start page
int pageNumber = int(api->getConfig()->getConfigItem(api->getConfig()->startPage,true)->asInt()) - 1;
int lastPage=pageNumber;
commonData.time = boatValues.findValueOrCreate("GPST"); // Load GpsTime
commonData.date = boatValues.findValueOrCreate("GPSD"); // Load GpsTime

View File

@ -23,8 +23,9 @@ lib_deps =
blemasle/MCP23017@2.0.0
adafruit/Adafruit BusIO@1.5.0
adafruit/Adafruit GFX Library@1.11.9
zinggjm/GxEPD2@1.5.8
#zinggjm/GxEPD2@1.5.8
#https://github.com/ZinggJM/GxEPD2
https://github.com/thooge/GxEPD2
sstaub/Ticker@4.4.0
adafruit/Adafruit BMP280 Library@2.6.2
adafruit/Adafruit BME280 Library@2.2.2