mirror of
https://github.com/usetrmnl/trmnl-display.git
synced 2026-08-13 14:29:45 -07:00
* Changed to cpp program based on trmnl_lib * Fixed build script for running in trmnl-display repo instead of trmnl_lib * enabled partial updates * enabled partial updates * updated to use default panel def for 7.5 inch * corrected switch position * Added initial support for the Inky Impression 13.3 * small fixes for the 13.3 Inky Impression * Added support for Waveshare's IT8951 HAT using FastEPD
1384 lines
48 KiB
C++
1384 lines
48 KiB
C++
//
|
|
// C++ example for TRMNL library
|
|
// written by Larry Bank (bitbank@pobox.com)
|
|
// Project started 5/19/2026
|
|
// Copyright (c) 2026 BitBank Software, Inc.
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//===========================================================================
|
|
//
|
|
// Enable SHOW_DETAILS for debugging only
|
|
//#define SHOW_DETAILS
|
|
#ifndef __MACH__
|
|
#include <bb_epaper.h>
|
|
#include <FastEPD.h>
|
|
#endif // __MACH__
|
|
#include <PNGdec.h>
|
|
#include <JPEGDEC.h>
|
|
#include "cJSON.h"
|
|
#include <trmnl_lib.h>
|
|
#include <unistd.h>
|
|
#include <termios.h>
|
|
#include <fcntl.h>
|
|
#include <dirent.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdexcept>
|
|
#include <stdlib.h>
|
|
#include <SDL2/SDL.h>
|
|
SDL_Window *win;
|
|
SDL_Surface *canvas, *winSurface;
|
|
#ifndef __MACH__
|
|
BBEPAPER bbep;
|
|
FASTEPD epaper;
|
|
#endif // __MACH__
|
|
volatile bool bQuit = false;
|
|
static int iCount = 0; // number of updates
|
|
static bool bCanDoPartial = false;
|
|
bool bSSH = false; // flag indicating if we're running from an SSH session
|
|
char szKey[64], szURL[128];
|
|
int iAdapter, iMode;
|
|
int iOrientation = 0; // default to non-rotated
|
|
int iPanel1Bit, iPanel2Bit;
|
|
int iInvert = 0; // assume not inverted
|
|
int iBGR = 0; // reversed R/B order
|
|
int iStretch = -1;
|
|
uint8_t u8SpectraPal[512]; // RGB333 mapped to closest Spectra6 color
|
|
int ConvertBpp(uint8_t *pBMP, int w, int h, int iBpp, uint8_t *palette);
|
|
|
|
enum {
|
|
STRETCH_NONE = 0,
|
|
STRETCH_FILL,
|
|
STRETCH_ASPECTFILL
|
|
};
|
|
|
|
enum {
|
|
ADAPTER_FRAMEBUFFER = 0,
|
|
ADAPTER_PIMORONI,
|
|
ADAPTER_WAVESHARE2,
|
|
ADAPTER_WAVESHARE2_RV2,
|
|
ADAPTER_PIMORONI_2, // Inky Impression 13.3
|
|
ADAPTER_WAVESHARE_IT8951, // Waveshare IT8951 HAT
|
|
};
|
|
|
|
typedef struct tagAdapter
|
|
{
|
|
uint8_t u8DC, u8RST, u8BUSY, u8CS, u8PWR, u8SPI, u8CS2;
|
|
} ADAPTER;
|
|
const char *szAdapters[] = {"framebuffer", "pimoroni", "waveshare_2", "waveshare_2_opi_rv2", "pimoroni_2", "waveshare_it8951", NULL};
|
|
const char *szModes[] = {"full", "fast", "partial", NULL};
|
|
const char *szStretch[] = {"none", "fill", "aspectfill", NULL};
|
|
const char *szPanels[] = {
|
|
"EP_PANEL_UNDEFINED","EP42_400x300","EP42B_400x300", // 0-2
|
|
"EP213_122x250", "EP213B_122x250", "EP293_128x296", // 3-5
|
|
"EP294_128x296", "EP295_128x296", "EP295_128x296_4GRAY", // 6-8
|
|
"EP266_152x296", "EP102_80x128", "EP27B_176x264", // 9-11
|
|
"EP29R_128x296", "EP122_192x176", "EP154R_152x152", // 12-14
|
|
"EP42R_400x300", "EP42R2_400x300", "EP37_240x416", // 15-17
|
|
"EP37B_240x416", "EP213_104x212", "EP75_800x480", // 18-20
|
|
"EP75_800x480_GEN2", "EP75_800x480_4GRAY", "EP75_800x480_4GRAY_GEN2", // 21-23
|
|
"EP75_800x480_4GRAY_V2", "EP29_128x296", "EP29_128x296_4GRAY", // 24-26
|
|
"EP213R_122x250", "EP154_200x200", "EP154B_200x200", // 27-29
|
|
"EP266YR_184x360", "EP29YR_128x296", "EP29YR_168x384", // 30-32
|
|
"EP583_648x480", "EP296_128x296", "EP26R_152x296", // 33-35
|
|
"EP73_800x480", "EP73_SPECTRA_800x480", "EP74R_640x384", // 36-38
|
|
"EP583R_600x448", "EP75R_800x480", "EP426_800x480", // 39-41
|
|
"EP426_800x480_4GRAY", "EP29R2_128x296", "EP41_640x400", // 42-44
|
|
"EP81_SPECTRA_1024x576", "EP7_960x640", "EP213R2_122x250", // 45-47
|
|
"EP29Z_128x296", "EP29Z_128x296_4GRAY", "EP213Z_122x250", // 48-50
|
|
"EP213Z_122x250_4GRAY", "EP154Z_152x152", "EP579_792x272", // 51-53
|
|
"EP213YR_122x250", "EP37YR_240x416", "EP35YR_184x384", // 54-56
|
|
"EP397YR_800x480", "EP154YR_200x200", "EP266YR2_184x360", // 57-59
|
|
"EP42YR_400x300", "EP215YR_160x296", "EP1085_1360x480", // 60-62
|
|
"EP31_240x320", "EP75YR_800x480", "EP154_200x200_4GRAY", // 63-65
|
|
"EP42B_400x300_4GRAY", "EP397_800x480", "EP397_800x480_4GRAY", // 66-68
|
|
"EP368_792x528", "EP368_792x528_4GRAY", "EP213ZZ_122x250", // 69-72
|
|
"EP40_SPECTRA_400x600", "EP27_176x264", "EP27_176x264_4GRAY", // 73-75
|
|
"EP426B_800x480", "EP583_648x480_4GRAY", "EP133_SPECTRA_1200x1600", // 76-78
|
|
"IT8951_1872x1440", // FastEPD panels start here
|
|
NULL // must be last entry
|
|
};
|
|
// DC, RST, BUSY, CS, PWR, SPI, CS2
|
|
ADAPTER adapters[] = {{0,0,0,0,0,0,0}, // framebuffer
|
|
{22, 27, 17, 8, 0xff, 0, 0}, // Pimoroni
|
|
{25, 17, 24, 8, 18, 0, 0}, // Waveshare 2.x
|
|
{49, 71, 92, 76, 70, 3, 0}, // Waveshare 2.x on OPi RV2
|
|
{22, 27, 17, 26, 0xff, 0, 16}, // Pimoroni_2
|
|
{0xff, 17, 24, 8, 0xff, 0, 0}, // Waveshare IT8951
|
|
};
|
|
//
|
|
// Find the index value of a string within a list
|
|
// The list must be terminated with a NULL pointer
|
|
// Returns a value 0-N or -1 for not found
|
|
//
|
|
int FindItemName(const char **pList, const char *pName, const char *szLabel)
|
|
{
|
|
int i = 0;
|
|
while (pList[i] != NULL && strcasecmp(pName, pList[i]) != 0) {
|
|
i++;
|
|
}
|
|
if (pList[i] == NULL) {
|
|
printf("Invalid %s; must be one of: ", szLabel);
|
|
// Print the list of valid values
|
|
i = 0;
|
|
while (pList[i] != NULL) {
|
|
printf("%s, ", pList[i]);
|
|
i++;
|
|
}
|
|
printf("\b\b \n"); // erase the last comma
|
|
return -1; // not found
|
|
}
|
|
return i;
|
|
} /* FindItemName() */
|
|
#ifndef __MACH__
|
|
//
|
|
// bb_epaper colors to map to Spectra6 colors
|
|
// The RGB values are not correct for the panel, but for simple mapping
|
|
// these work best. These get mapped from bb_epaper color indices to
|
|
// Spectra6 color indices by the setPixel() method.
|
|
//
|
|
const int iSpectraRGB[] = { // r, g, b
|
|
// 18, 12, 16, // black
|
|
// 165, 165, 165, // white
|
|
// 150, 131, 39, // yellow
|
|
// 82, 24, 10, // red
|
|
// 40, 72, 123, // blue
|
|
// 89, 104, 63, // green
|
|
|
|
0, 0, 0, // black = 0
|
|
192,192,192, // white = 1
|
|
192,192,0, // yellow = 2
|
|
192,0,0, // red = 3
|
|
0,0,192, // blue = 4
|
|
0,192,0, // green = 5
|
|
};
|
|
#endif //!__MACH__
|
|
// Map the Spectra6 palette to the closest RGB333 values
|
|
void CreateSpectra6Pal(const int *pSrc, uint8_t *pDest)
|
|
{
|
|
int i, j;
|
|
int r, g, b, r1, g1, b1;
|
|
int dist, min_dist, min_index;
|
|
|
|
for (i=0; i<512; i++) { // RGB333
|
|
r = (i & 7)*36;
|
|
g = ((i >> 3) & 7)*36;
|
|
b = (i >> 6)*36;
|
|
min_dist = 0x7fffffff;
|
|
min_index = 0;
|
|
for (j=0; j<6; j++) { // match to the closes Spectra6 color
|
|
r1 = pSrc[j*3];
|
|
g1 = pSrc[j*3+1];
|
|
b1 = pSrc[j*3+2];
|
|
dist = (r - r1) * (r - r1); // delta red squared
|
|
dist += (g - g1) * (g - g1); // delta green squared
|
|
dist += (b - b1) * (b - b1); // delta blue squared
|
|
if (dist < min_dist) {
|
|
min_dist = dist;
|
|
min_index = j;
|
|
}
|
|
} // for j
|
|
pDest[i] = min_index; // best match palette index for this RGB333 color
|
|
// if ((i & 15) == 15) {
|
|
// printf("%d\n",pDest[i]);
|
|
// } else {
|
|
// printf("%d,", pDest[i]);
|
|
// }
|
|
} // for i
|
|
} /* CreateSpectra6Pal() */
|
|
//
|
|
// Convert the RGB value into one of 6 Spectra6 colors
|
|
//
|
|
uint8_t GetSpectraPixel(int r, int g, int b)
|
|
{
|
|
uint8_t c;
|
|
uint16_t rgb333;
|
|
|
|
rgb333 = (r>>5) + ((g & 0xe0) >> 2) + ((b & 0xe0) << 1);
|
|
c = u8SpectraPal[rgb333];
|
|
return c;
|
|
} /* GetSpectraPixel() */
|
|
//
|
|
// Display help text if the input parameters are missing or incorrect
|
|
//
|
|
void ShowHelp(void)
|
|
{
|
|
printf("trmnl_display utility - run TRMNL on your RPI monitor or ePaper display\nwritten by Larry Bank (bitbank@pobox.com)\nCopyright(c) 2025-2026 TRMNL LLC\n");
|
|
printf("A JSON file (~/.config/trmnl/show_img.json) can contain the setup\nor the parameters can be passed on the command line (in any order):\n");
|
|
printf("mode=<update mode> can be full, fast or partial\nadapter=<epaper PCB> can be waveshare_2 or pimoroni\npanel_1bit=<bb_epaper panel name>\npanel_2bit=<bb_epaper panel name>\n");
|
|
printf("Color images and bit depths greater than 2-bpp will be\nautomatically converted to 2-bit (4 grays).\n");
|
|
printf("example: ./trmnl_display mode=fast panel_1bit=EP75_800x480 adapter=waveshare_2\n");
|
|
} /* ShowHelp() */
|
|
|
|
// Set this to the size of images you will receive
|
|
#define IMAGE_WIDTH 800
|
|
#define IMAGE_HEIGHT 480
|
|
|
|
PNG png;
|
|
JPEGDEC jpg;
|
|
int iWidth, iHeight, iBpp, iPixelType;
|
|
uint8_t *pBitmap, *pPalette=NULL;
|
|
|
|
const char *szPNGErrors[] = {"Success", "Invalid Parameter", "Decoding", "Out of memory", "No buffer allocated", "Unsupported feature", "Invalid file", "Too big", "Quit early"};
|
|
const char *szJPEGErrors[] = {"Success", "Invalid Parameter", "Decoding", "Unsupported feature", "Invalid file", "Out of memory"};
|
|
|
|
//
|
|
// Decode the BMP file
|
|
//
|
|
int DecodeBMP(uint8_t *pData, int iSize)
|
|
{
|
|
int iOffBits; // offset to bitmap data
|
|
int y, iDestPitch=0, iPitch;
|
|
uint8_t bFlipped = 0;
|
|
uint8_t *s, *d;
|
|
|
|
iWidth = *(int16_t *)&pData[18];
|
|
iHeight = *(int16_t *)&pData[22];
|
|
if (iHeight < 0) {
|
|
iHeight = -iHeight;
|
|
} else {
|
|
bFlipped = 1;
|
|
}
|
|
iBpp = *(int16_t *)&pData[28];
|
|
iOffBits = *(uint16_t *)&pData[10];
|
|
switch (iBpp) {
|
|
case 1:
|
|
iDestPitch = ((iWidth+7)>>3);
|
|
iPixelType = PNG_PIXEL_INDEXED;
|
|
break;
|
|
case 4:
|
|
iDestPitch = ((iWidth+1)>>1);
|
|
iPixelType = PNG_PIXEL_INDEXED;
|
|
break;
|
|
case 8:
|
|
iDestPitch = iWidth;
|
|
iPixelType = PNG_PIXEL_INDEXED;
|
|
break;
|
|
case 24:
|
|
iDestPitch = iWidth*3;
|
|
iPixelType = PNG_PIXEL_TRUECOLOR;
|
|
iBGR = 1; // reversed R/B order
|
|
break;
|
|
case 32:
|
|
iDestPitch = iWidth*4;
|
|
iPixelType = PNG_PIXEL_TRUECOLOR_ALPHA;
|
|
iBGR = 1;
|
|
break;
|
|
} // switch on bpp
|
|
iPitch = (iDestPitch + 3) & 0xfffc; // must be DWORD aligned
|
|
if (bFlipped)
|
|
{
|
|
iOffBits += ((iHeight-1) * iPitch); // start from bottom
|
|
iPitch = -iPitch;
|
|
}
|
|
pBitmap = (uint8_t *)malloc(iHeight * iDestPitch);
|
|
s = &pData[iOffBits];
|
|
d = pBitmap;
|
|
for (y=0; y<iHeight; y++) { // copy the bitmap to the common format
|
|
memcpy(d, s, iDestPitch);
|
|
s += iPitch;
|
|
d += iDestPitch;
|
|
}
|
|
// Adjust the palette for 3-byte entries (if there is one)
|
|
if (iBpp <= 8) {
|
|
int iColors = 1<<iBpp;
|
|
d = pPalette = pData;
|
|
iOffBits = *(uint16_t *)&pData[10];
|
|
s = &pData[iOffBits - (4 * iColors)];
|
|
for (y=0; y<iColors; y++) {
|
|
d[0] = s[0]; d[1] = s[1]; d[2] = s[2];
|
|
s += 4;
|
|
d += 3;
|
|
}
|
|
} else {
|
|
pPalette = NULL;
|
|
}
|
|
return PNG_SUCCESS; // re-use this return code
|
|
} /* DecodeBMP() */
|
|
//
|
|
// Decode the JPEG file into an uncompressed bitmap
|
|
//
|
|
int DecodeJPEG(uint8_t *pData, int iSize)
|
|
{
|
|
int rc, iPitch;
|
|
rc = jpg.openRAM(pData, iSize, NULL);
|
|
if (!rc) {
|
|
rc = jpg.getLastError();
|
|
printf("JPEG open returned error: %s\n", szJPEGErrors[rc]);
|
|
return -1; // only show the error once
|
|
}
|
|
iWidth = jpg.getWidth();
|
|
iHeight = jpg.getHeight();
|
|
iBpp = jpg.getBpp();
|
|
if (iBpp == 8) {
|
|
iPixelType = PNG_PIXEL_GRAYSCALE;
|
|
jpg.setPixelType(EIGHT_BIT_GRAYSCALE);
|
|
iPitch = iWidth;
|
|
} else {
|
|
iPixelType = PNG_PIXEL_TRUECOLOR_ALPHA;
|
|
jpg.setPixelType(RGB8888);
|
|
iPitch = iWidth*4;
|
|
iBpp = 32; // output is 32-bits
|
|
}
|
|
pBitmap = (uint8_t *)malloc(iPitch * (iHeight+15));
|
|
jpg.setFramebuffer(pBitmap);
|
|
jpg.decode(0, 0, 0);
|
|
return jpg.getLastError();
|
|
} /* DecodeJPEG() */
|
|
|
|
//
|
|
// Decode the PNG file into an uncompressed bitmap
|
|
//
|
|
int DecodePNG(uint8_t *pData, int iSize)
|
|
{
|
|
int rc;
|
|
rc = png.openRAM(pData, iSize, NULL);
|
|
if (rc != PNG_SUCCESS) {
|
|
printf("PNG open returned error: %s\n", szPNGErrors[rc]);
|
|
return -1; // only show the error once
|
|
}
|
|
iBGR = 1; // reversed R/B order
|
|
iWidth = png.getWidth();
|
|
iHeight = png.getHeight();
|
|
iBpp = png.getBpp();
|
|
pPalette = png.getPalette();
|
|
iPixelType = png.getPixelType();
|
|
if (iPixelType != PNG_PIXEL_INDEXED) pPalette = NULL; // tell other code that there's no palette present
|
|
pBitmap = (uint8_t *)malloc(png.getBufferSize());
|
|
png.setBuffer(pBitmap);
|
|
rc = png.decode(NULL, 0);
|
|
return rc;
|
|
} /* DecodePNG() */
|
|
#ifndef __MACH__
|
|
//
|
|
// Draw the current image onto the epaper display
|
|
//
|
|
void ShowEPDImage(void)
|
|
{
|
|
int x, y, iPlaneOffset, iSrcPitch=0, iDestPitch=0;
|
|
uint8_t *s, *d, uc=0;
|
|
s = pBitmap;
|
|
if (iAdapter == ADAPTER_WAVESHARE_IT8951) { // use FastEPD
|
|
if (iBpp > 4) {
|
|
iBpp = ConvertBpp(s, iWidth, iHeight, iBpp, pPalette);
|
|
}
|
|
d = (uint8_t *)epaper.currentBuffer();
|
|
switch (iBpp) {
|
|
case 1:
|
|
epaper.setMode(BB_MODE_1BPP);
|
|
iDestPitch = (epaper.width()+7)/8;
|
|
iSrcPitch = (iWidth+7)/8;
|
|
break;
|
|
case 2:
|
|
epaper.setMode(BB_MODE_2BPP);
|
|
iDestPitch = (epaper.width()+3)/4;
|
|
iSrcPitch = (iWidth+3)/4;
|
|
break;
|
|
case 4:
|
|
epaper.setMode(BB_MODE_4BPP);
|
|
iDestPitch = (epaper.width()+1)/2;
|
|
iSrcPitch = (iWidth+1)/2;
|
|
break;
|
|
}
|
|
for (y=0; y<iHeight; y++) {
|
|
memcpy(d, s, iSrcPitch);
|
|
s += iSrcPitch;
|
|
d += iDestPitch;
|
|
}
|
|
epaper.fullUpdate();
|
|
epaper.einkPower(0);
|
|
return; // done
|
|
} // FastEPD path
|
|
|
|
d = (uint8_t *)bbep.getBuffer();
|
|
iDestPitch = (bbep.width()+7)/8;
|
|
// Convert the source bitmap to 1 or 2-bit grayscale
|
|
if (iBpp >= 2 || (bbep.capabilities() & BBEP_7COLOR)) {
|
|
CreateSpectra6Pal(iSpectraRGB, u8SpectraPal);
|
|
iBpp = ConvertBpp(s, iWidth, iHeight, iBpp, pPalette);
|
|
}
|
|
if (!(bbep.capabilities() & (BBEP_7COLOR | BBEP_4COLOR | BBEP_3COLOR))) {
|
|
if (iBpp == 1) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("Selecting 1-bpp panel type\n");
|
|
#endif
|
|
bbep.setPanelType(iPanel1Bit);
|
|
} else {
|
|
#ifdef SHOW_DETAILS
|
|
printf("Selecting 2-bpp panel type\n");
|
|
#endif
|
|
bbep.setPanelType(iPanel2Bit);
|
|
}
|
|
}
|
|
if (iBpp == 1 && !(bbep.capabilities() & BBEP_7COLOR)) {
|
|
iSrcPitch = (iWidth+7)/8;
|
|
for (y=0; y<iHeight; y++) {
|
|
if (bbep.capabilities() & BBEP_4COLOR) {
|
|
// memory layout is different
|
|
for (x=0; x<iWidth; x++) {
|
|
if ((x & 7) == 0) uc = s[x>>3];
|
|
if (!(uc & 0x80)) bbep.drawPixel(x, y, BBEP_BLACK); // background is already white
|
|
uc <<= 1;
|
|
}
|
|
} else {
|
|
memcpy(d, s, iSrcPitch);
|
|
if (iWidth & 7) { // fill partial byte with white
|
|
d[iWidth>>3] |= (0xff >> (iWidth & 7));
|
|
}
|
|
}
|
|
s += iSrcPitch;
|
|
d += iDestPitch;
|
|
}
|
|
} else if (!(bbep.capabilities() & (BBEP_7COLOR | BBEP_3COLOR | BBEP_4COLOR))) { // >=2 bpp
|
|
iPlaneOffset = iDestPitch * iHeight; // offset to 2nd memory plane
|
|
iSrcPitch = (iWidth+3)/4; // every source pixel depth will become 2-bpp
|
|
for (y=0; y<iHeight; y++) {
|
|
// Split the 2-bit packed pixels into 2 bit planes for the EPD
|
|
for (x=0; x<iWidth/4; x+=2) { // work with pairs of bytes
|
|
uint8_t s0 = ~s[x]; // grayscale is inverted on the EPD
|
|
uint8_t s1 = ~s[x+1];
|
|
uint8_t u8Mask = 0x80, d0=0, d1=0;
|
|
for (int bit=0; bit<4; bit++) {
|
|
if (s0 & u8Mask) d1 |= (0x80 >> bit);
|
|
if (s0 & (u8Mask>>1)) d0 |= (0x80 >> bit);
|
|
if (s1 & u8Mask) d1 |= (0x8 >> bit);
|
|
if (s1 & (u8Mask>>1)) d0 |= (0x8 >> bit);
|
|
u8Mask >>= 2;
|
|
} // for each bit
|
|
d[x/2] = d0; // plane 0
|
|
d[(x/2) + iPlaneOffset] = d1; // plane 1
|
|
} // for x
|
|
s += iSrcPitch;
|
|
d += iDestPitch;
|
|
} // for y
|
|
} // >= 2bpp
|
|
free(pBitmap);
|
|
// Push the pixels from our RAM buffer to the e-epaper
|
|
#ifdef SHOW_DETAILS
|
|
printf("Writing data to EPD...\n");
|
|
#endif
|
|
if (bbep.capabilities() & (BBEP_7COLOR | BBEP_3COLOR | BBEP_4COLOR | BBEP_4GRAY)) { // if it's not 1-bit, it only supports full refresh
|
|
iMode = REFRESH_FULL;
|
|
}
|
|
if (iBpp == 1 && !(bbep.capabilities() & (BBEP_3COLOR | BBEP_4COLOR | BBEP_7COLOR | BBEP_4GRAY))) {
|
|
if (bCanDoPartial) {
|
|
if ((iCount & 3) == 3) {
|
|
iMode = REFRESH_FAST; // clean up any ghosting
|
|
} else {
|
|
iMode = REFRESH_PARTIAL;
|
|
}
|
|
bbep.writePlane(PLANE_0, iInvert);
|
|
} else {
|
|
bbep.writePlane(PLANE_DUPLICATE, iInvert);
|
|
}
|
|
bbep.refresh(iMode);
|
|
bCanDoPartial = true; // for the next 1-bit image
|
|
iCount++;
|
|
} else if (bbep.capabilities() & BBEP_7COLOR) { // Spectra6
|
|
bbep.writePlane();
|
|
bbep.refresh(REFRESH_FULL);
|
|
} else { // 3-color, 4-color, or 4 gray mode
|
|
bbep.writePlane(PLANE_BOTH, iInvert);
|
|
bbep.refresh(iMode); // some 4-color panels support fast update
|
|
bCanDoPartial = false;
|
|
iCount = 0;
|
|
}
|
|
#ifdef SHOW_DETAILS
|
|
printf("Refresh complete, sleeping panel.\n");
|
|
#endif
|
|
bbep.sleep(LIGHT_SLEEP); // turn off the epaper power circuit
|
|
} /* ShowEPDImage() */
|
|
#endif // __MACH__
|
|
//
|
|
// Draw the current image onto a SDL window
|
|
//
|
|
void ShowSDLImage(void)
|
|
{
|
|
uint16_t *d, u16, r, g, b;
|
|
uint8_t *s;
|
|
int iSrcPitch;
|
|
int x, y, rOff = 2, bOff = 0;
|
|
uint8_t ucTemp[768]; // temporary palette for grayscale
|
|
|
|
if (iBGR) {
|
|
rOff = 0;
|
|
bOff = 2;
|
|
}
|
|
if (!pPalette && iBpp <= 8) { // create a grayscale palette if needed
|
|
int iDelta, iCount = 1<<iBpp;
|
|
int iGray=0;
|
|
iDelta = 255/(iCount-1);
|
|
for (x=0; x<iCount; x++) {
|
|
ucTemp[x*3] = (uint8_t)iGray;
|
|
ucTemp[x*3+1] = (uint8_t)iGray;
|
|
ucTemp[x*3+2] = (uint8_t)iGray;
|
|
iGray += iDelta;
|
|
}
|
|
pPalette = ucTemp;
|
|
} else {
|
|
}
|
|
if (iPixelType == PNG_PIXEL_TRUECOLOR) {
|
|
iBpp = 24;
|
|
} else if (iPixelType == PNG_PIXEL_TRUECOLOR_ALPHA) {
|
|
iBpp = 32;
|
|
}
|
|
// Create a surface to hold the image canvas
|
|
canvas = SDL_CreateRGBSurfaceWithFormat(0, iWidth, iHeight, 16, SDL_PIXELFORMAT_RGB565);
|
|
if (canvas == nullptr) {
|
|
printf("SDL_CreateSurface error %s\n", SDL_GetError());
|
|
SDL_DestroyWindow(win);
|
|
SDL_Quit();
|
|
return;
|
|
}
|
|
|
|
iSrcPitch = (iWidth * iBpp)/8;
|
|
for (y=0; y<iHeight; y++) {
|
|
s = pBitmap + (y*iSrcPitch);
|
|
d = (uint16_t *)canvas->pixels;
|
|
d += y * iWidth;
|
|
switch(iBpp) {
|
|
case 1:
|
|
{
|
|
uint8_t uc;
|
|
uc = *s++;
|
|
for (x=0; x<iWidth; x++) {
|
|
if (uc & 0x80) {
|
|
r = pPalette[rOff+3];
|
|
g = pPalette[4];
|
|
b = pPalette[bOff+3];
|
|
} else {
|
|
r = pPalette[rOff];
|
|
g = pPalette[1];
|
|
b = pPalette[bOff];
|
|
}
|
|
*d++ = ((r & 0xf8)<<8) | ((g & 0xfc) << 3) | (b >> 3);
|
|
uc <<= 1;
|
|
if ((x & 7) == 7) uc = *s++;
|
|
} // for x
|
|
}
|
|
break;
|
|
case 2:
|
|
{
|
|
uint8_t c, uc;
|
|
uc = *s++;
|
|
for (x=0; x<iWidth; x++) {
|
|
c = uc >> 6;
|
|
r = pPalette[c*3+rOff];
|
|
g = pPalette[c*3+1];
|
|
b = pPalette[c*3+bOff];
|
|
*d++ = ((r & 0xf8)<<8) | ((g & 0xfc) << 3) | (b >> 3);
|
|
uc <<= 2;
|
|
if ((x & 3) == 3) uc = *s++;
|
|
} // for x
|
|
}
|
|
break;
|
|
case 4:
|
|
{
|
|
uint8_t c, uc;
|
|
uc = *s++;
|
|
for (x=0; x<iWidth; x++) {
|
|
c = uc >> 4;
|
|
r = pPalette[c*3+rOff];
|
|
g = pPalette[c*3+1];
|
|
b = pPalette[c*3+bOff];
|
|
*d++ = ((r & 0xf8)<<8) | ((g & 0xfc) << 3) | (b >> 3);
|
|
uc <<= 4;
|
|
if ((x & 1) == 1) uc = *s++;
|
|
} // for x
|
|
}
|
|
break;
|
|
case 8:
|
|
{
|
|
uint8_t uc;
|
|
uc = *s++;
|
|
for (x=0; x<iWidth; x++) {
|
|
r = pPalette[uc*3+rOff];
|
|
g = pPalette[uc*3+1];
|
|
b = pPalette[uc*3+bOff];
|
|
*d++ = ((r & 0xf8)<<8) | ((g & 0xfc) << 3) | (b >> 3);
|
|
uc = *s++;
|
|
} // for x
|
|
}
|
|
break;
|
|
case 24:
|
|
case 32:
|
|
{
|
|
for (x=0; x<iWidth; x++) {
|
|
u16 = (s[rOff] & 0xf8)<<8; // R
|
|
u16 |= (s[1] & 0xfc) << 3; // G
|
|
u16 |= (s[bOff] >> 3); // B
|
|
*d++ = u16;
|
|
s += (iBpp/8);
|
|
} // for x
|
|
}
|
|
break;
|
|
} // switch on bpp
|
|
} // for y
|
|
free(pBitmap); // no longer needed
|
|
// winSurface = SDL_GetWindowSurface(win);
|
|
if (1) {//winSurface) {
|
|
int w, h;
|
|
SDL_GetWindowSize(win, &w, &h);
|
|
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear"); // linear interpolate
|
|
SDL_Renderer *renderer = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);
|
|
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, canvas);
|
|
SDL_Rect dstrect;
|
|
SDL_RenderClear(renderer);
|
|
dstrect.x = dstrect.y = 0;
|
|
if (iOrientation == 0 || iOrientation == 180) {
|
|
dstrect.w = w; dstrect.h = h;
|
|
} else {
|
|
float f = (float)h / (float)w;
|
|
dstrect.w = (int)(f * w);
|
|
dstrect.h = (int)(f * h);
|
|
dstrect.x = (w-dstrect.w)/2;
|
|
dstrect.y = (h-dstrect.h)/2;
|
|
}
|
|
SDL_RenderCopyEx(renderer, texture, NULL, &dstrect, iOrientation, NULL, SDL_FLIP_NONE);
|
|
SDL_RenderPresent(renderer);
|
|
SDL_FreeSurface(canvas);
|
|
SDL_DestroyTexture(texture);
|
|
SDL_DestroyRenderer(renderer);
|
|
} else {
|
|
printf("Error acquiring SDL Window Surface; is the monitor connected?\n");
|
|
}
|
|
} /* ShowSDLImage() */
|
|
//
|
|
// Figure out the image type and decode it
|
|
// returns 1 for success, 0 for failure
|
|
//
|
|
int decodeImage(uint8_t *pData, int iSize) {
|
|
int rc;
|
|
|
|
if (iSize < 64) { // invalid file
|
|
printf("Invalid image file\n");
|
|
return 0;
|
|
}
|
|
if (pData[0] == 'B' && pData[1] == 'M') { // it's a BMP file
|
|
rc = DecodeBMP(pData, iSize);
|
|
} else if (pData[0] == 0xff && pData[1] == 0xd8) { // JPEG
|
|
rc = DecodeJPEG(pData, iSize);
|
|
if (rc != JPEG_SUCCESS) {
|
|
if (rc > 0) {
|
|
printf("JPEG decode returned error: %s\n", szJPEGErrors[rc]);
|
|
}
|
|
return 0;
|
|
}
|
|
} else {
|
|
rc = DecodePNG(pData, iSize);
|
|
if (rc != PNG_SUCCESS) {
|
|
if (rc > 0) {
|
|
printf("PNG decode returned error: %s\n", szPNGErrors[rc]);
|
|
}
|
|
return 0;
|
|
} else {
|
|
#ifdef SHOW_DETAILS
|
|
printf("PNG decode succeeded\n");
|
|
#endif
|
|
}
|
|
}
|
|
return 1;
|
|
} /* decodeImage() */
|
|
//
|
|
// Run the TRMNL viewer as SDL fullscreen
|
|
//
|
|
void TRMNL_SDL(void)
|
|
{
|
|
uint8_t *pImage;
|
|
TRMNL trmnl;
|
|
int rc, iSize;
|
|
time_t now, next_update;
|
|
|
|
time(&next_update); // get the current time
|
|
trmnl.setDisplaySize(IMAGE_WIDTH, IMAGE_HEIGHT); // dynamic display size is not supported yet; for future use
|
|
// Create the SDL window
|
|
#ifdef __MACH__
|
|
// create a windowed version for MacOS
|
|
win = SDL_CreateWindow("TRMNL", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, IMAGE_WIDTH, IMAGE_HEIGHT, SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL);
|
|
#else
|
|
win = SDL_CreateWindow("TRMNL", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, IMAGE_WIDTH, IMAGE_HEIGHT, SDL_WINDOW_SHOWN | SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL);
|
|
#endif
|
|
if (win == nullptr) {
|
|
printf("SDL_CreateWindow Error: %s\n", SDL_GetError());
|
|
SDL_Quit();
|
|
return;
|
|
}
|
|
bool bQuit = false;
|
|
#ifdef SHOW_DETAILS
|
|
printf("Created SDL window, about to enter event loop\n");
|
|
#endif
|
|
while (!bQuit) {
|
|
SDL_Event e;
|
|
if (bSSH) { // capture keys from the SSH session (STDIN)
|
|
fd_set set;
|
|
struct timeval timeout = {0, 1000}; // 1ms timeout to keep SDL responsive
|
|
FD_ZERO(&set);
|
|
FD_SET(STDIN_FILENO, &set);
|
|
if (select(STDIN_FILENO + 1, &set, NULL, NULL, &timeout) > 0) {
|
|
if (FD_ISSET(STDIN_FILENO, &set)) {
|
|
char c = getchar();
|
|
if (c == '\n' || c == '\r') { // Detect Enter key
|
|
printf("Enter key pressed, skipping to next in playlist...\n");
|
|
next_update = now;
|
|
} else if (c == 0x1b) { // ESC key
|
|
bQuit = true;
|
|
}
|
|
}
|
|
}
|
|
} // running from SSH session
|
|
|
|
while (SDL_PollEvent(&e)) { // take care of queued events
|
|
if (e.type == SDL_WINDOWEVENT && e.window.event == SDL_WINDOWEVENT_CLOSE) {
|
|
bQuit = true;
|
|
}
|
|
if (e.type == SDL_QUIT || (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE)) {
|
|
bQuit = true;
|
|
}
|
|
if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RETURN) {
|
|
// skip to next image before time expires
|
|
printf("Enter key pressed, skipping to next in playlist...\n");
|
|
next_update = now;
|
|
}
|
|
} // while SDL events
|
|
SDL_Delay(100);
|
|
time(&now);
|
|
if (now > next_update) {
|
|
rc = trmnl.getAPI(szKey, szURL);
|
|
if (rc == TRMNL_SUCCESS) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("getAPI succeeded\n");
|
|
#endif
|
|
next_update = now + trmnl.getSleepTime();
|
|
rc = trmnl.getImage(&pImage, &iSize);
|
|
if (rc == TRMNL_SUCCESS) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("getImage succeeded, size = %d bytes\n", iSize);
|
|
#endif
|
|
if (decodeImage(pImage, iSize)) {
|
|
ShowSDLImage();
|
|
}
|
|
trmnl.freeImage();
|
|
}
|
|
} else {
|
|
printf("getAPI failed with error: %d, exiting...\n", trmnl.getHTTPCode());
|
|
bQuit = true;
|
|
}
|
|
}
|
|
} // while SDL window displayed
|
|
printf("exiting...\n");
|
|
// Clean up
|
|
SDL_DestroyWindow(win);
|
|
SDL_Quit();
|
|
} /* TRMNL_SDL() */
|
|
#ifndef __MACH__
|
|
//
|
|
// Match the given pixel to black (00), white (01), or red (1x)
|
|
//
|
|
unsigned char GetBWRPixel(int r, int g, int b)
|
|
{
|
|
uint8_t ucOut=BBEP_BLACK;
|
|
int gr;
|
|
|
|
gr = (b + r + g*2)>>2; // gray
|
|
// match the color to closest of black/white/red
|
|
if (r > g && r > b) { // red is dominant
|
|
if (gr < 100 && r < 80) {
|
|
// black
|
|
} else {
|
|
if (r-b > 32 && r-g > 32) {
|
|
// is red really dominant?
|
|
ucOut = BBEP_RED; // red (can be 2 or 3, but 3 is compatible w/BWYR)
|
|
} else { // yellowish should be white
|
|
// no, use white instead of pink/yellow
|
|
ucOut = BBEP_WHITE;
|
|
}
|
|
}
|
|
} else { // check for white/black
|
|
if (gr >= 128) {
|
|
ucOut = BBEP_WHITE; // white
|
|
} else {
|
|
// black
|
|
}
|
|
}
|
|
return ucOut;
|
|
} /* GetBWRPixel() */
|
|
//
|
|
// Match the given pixel to black (00), white (01), yellow (10), or red (11)
|
|
// returns 2 bit value of closest matching color
|
|
//
|
|
unsigned char GetBWYRPixel(int r, int g, int b)
|
|
{
|
|
uint8_t ucOut=BBEP_BLACK;
|
|
int gr;
|
|
|
|
gr = (b + r + g*2)>>2; // gray
|
|
// match the color to closest of black/white/yellow/red
|
|
if (r > b || g > b) { // red or yellow is dominant
|
|
if (gr < 90 && r < 80 && g < 80) {
|
|
// black
|
|
} else {
|
|
if (r-b > 32 && r-g > r/2) {
|
|
// is red really dominant?
|
|
ucOut = BBEP_RED; // red
|
|
} else if (r-b > 32 && g-b > 32) {
|
|
// yes, yellow
|
|
ucOut = BBEP_YELLOW;
|
|
} else {
|
|
ucOut = BBEP_WHITE; // gray/white
|
|
}
|
|
}
|
|
} else { // check for white/black
|
|
if (gr >= 100) {
|
|
ucOut = BBEP_WHITE; // white
|
|
} else {
|
|
// black
|
|
}
|
|
}
|
|
return ucOut;
|
|
} /* GetBWYRPixel() */
|
|
//
|
|
// The user passed a file which has 2 or more bits per pixel
|
|
// convert it to 1 or 2-bpp grayscale
|
|
//
|
|
int ConvertBpp(uint8_t *pBMP, int w, int h, int iBpp, uint8_t *palette)
|
|
{
|
|
int gray, r=0, g=0, b=0, x, y, iDelta, iPitch, iDestPitch, iDestBpp;
|
|
uint8_t *s, *d, *pPal, u8, count;
|
|
|
|
if (iPanel2Bit == -1) { // only 1 or 4 bit panel available
|
|
iDestBpp = (bbep.capabilities() & BBEP_7COLOR) ? 4 : 1;
|
|
} else {
|
|
iDestBpp = 2;
|
|
}
|
|
if (iBpp == 2 && bbep.capabilities() & BBEP_7COLOR) {
|
|
iDestBpp = 1; // Spectra6 can't display 4 gray levels
|
|
}
|
|
if (iDestBpp == 1) {
|
|
iDestPitch = (w+7)/8;
|
|
} else if (iDestBpp == 2) {
|
|
iDestPitch = (w+3)/4;
|
|
} else { // 4
|
|
iDestPitch = (w+1)/2;
|
|
}
|
|
// The bits per pixel info from PNG files is per color channel
|
|
// Convert the value into a true bits per pixel
|
|
switch (iPixelType) {
|
|
case PNG_PIXEL_INDEXED:
|
|
break;
|
|
case PNG_PIXEL_TRUECOLOR:
|
|
if (iBpp <= 8) {
|
|
iBpp *= 3;
|
|
}
|
|
palette = NULL;
|
|
break;
|
|
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
|
if (iBpp <= 8) {
|
|
iBpp *= 4;
|
|
}
|
|
palette = NULL;
|
|
break;
|
|
case PNG_PIXEL_GRAYSCALE:
|
|
palette = NULL;
|
|
break;
|
|
} // switch on pixel type
|
|
|
|
// Loop through the source image and convert each pixel to 2-bit grayscale
|
|
// Overwrite the source image with the converted image since it will be smaller or
|
|
// equal in size to the original. This is needed even for 2-bit images which may
|
|
// use a palette with random color entries.
|
|
iPitch = (w * iBpp)/8;
|
|
iDelta = iBpp/8;
|
|
for (y=0; y<h; y++) {
|
|
s = &pBMP[iPitch * y];
|
|
d = &pBMP[iDestPitch * y]; // overwrite the original data as we change it
|
|
count = 8; // bits in a byte
|
|
u8 = 0; // start with all black
|
|
for (x=0; x<w; x++) { // slower code, but less code :)
|
|
u8 <<= iDestBpp;
|
|
switch (iBpp) {
|
|
case 24:
|
|
case 32:
|
|
r = s[0];
|
|
g = s[1];
|
|
b = s[2];
|
|
s += iDelta;
|
|
break;
|
|
case 16:
|
|
r = s[1] & 0xf8; // red
|
|
g = ((s[0] | s[1] << 8) >> 3) & 0xfc; // green
|
|
b = s[0] << 3;
|
|
s += 2;
|
|
break;
|
|
case 8:
|
|
if (palette) {
|
|
pPal = &palette[s[0] * 3];
|
|
r = pPal[0];
|
|
g = pPal[1];
|
|
b = pPal[2];
|
|
} else {
|
|
r = g = b = s[0];
|
|
}
|
|
s++;
|
|
break;
|
|
case 4:
|
|
if (palette) {
|
|
if (x & 1) {
|
|
pPal = &palette[(s[0] & 0xf) * 3];
|
|
s++;
|
|
} else {
|
|
pPal = &palette[(s[0]>>4) * 3];
|
|
}
|
|
r = pPal[0];
|
|
g = pPal[1];
|
|
b = pPal[2];
|
|
} else {
|
|
if (x & 1) {
|
|
r = g = b = (s[0] & 0xf) | (s[0] << 4);
|
|
s++;
|
|
} else {
|
|
r = g = b = (s[0] >> 4) | (s[0] & 0xf0);
|
|
}
|
|
}
|
|
break;
|
|
case 2:
|
|
if (palette) {
|
|
pPal = &palette[((s[0] >> ((3-(x&3))*2)) & 3)*3];
|
|
r = pPal[0]; g = pPal[1]; b = pPal[2];
|
|
} else {
|
|
r = g = b = (s[0] << ((x&3)*2)) & 0xc0;
|
|
}
|
|
if ((x & 3) == 3) s++;
|
|
break;
|
|
case 1:
|
|
if (palette) {
|
|
pPal = &palette[((s[0] >> (7-(x&7))) & 1)*3];
|
|
r = pPal[0]; g = pPal[1]; b = pPal[2];
|
|
} else {
|
|
r = g = b = ((s[0] << (x&7)) & 0x80);
|
|
}
|
|
if ((x & 7) == 7) s++;
|
|
break;
|
|
} // switch on bpp
|
|
// 3, 4 and 7-color epaper need the colors translated
|
|
// through custom color tables because different panels use
|
|
// different bit patterns to mean different things.
|
|
if (bbep.capabilities() & BBEP_7COLOR) { // Spectra6
|
|
bbep.drawPixel(x, y, GetSpectraPixel(r, g, b));
|
|
} else if (bbep.capabilities() & BBEP_3COLOR) { // B/W/R
|
|
bbep.drawPixel(x, y, GetBWRPixel(r, g, b));
|
|
} else if (bbep.capabilities() & BBEP_4COLOR) { // B/W/Y/R
|
|
bbep.drawPixel(x, y, GetBWYRPixel(r, g, b));
|
|
} else { // Assume 1 or 2 bit grayscale
|
|
// Convert the source rgb into gray with a simple formula which favors green
|
|
gray = (r + g*2 + b)/4;
|
|
u8 |= gray >> (8-iDestBpp); // pack 1 or 2 bit gray pixels into a destination byte
|
|
count -= iDestBpp;
|
|
if (count == 0) { // byte is full, store it and prepare the next
|
|
*d++ = u8;
|
|
u8 = 0;
|
|
count = 8;
|
|
}
|
|
}
|
|
} // for x
|
|
if (count != 8 && iDestBpp == 1) {
|
|
*d++ = (u8 << count); // store last partial byte
|
|
}
|
|
} // for y
|
|
return iDestBpp;
|
|
} /* ConvertBpp() */
|
|
//
|
|
// Run TRMNL on an epaper panel
|
|
//
|
|
void TRMNL_EPAPER(void)
|
|
{
|
|
TRMNL trmnl;
|
|
time_t now, next_update;
|
|
uint8_t *pImage;
|
|
int rc, iSize;
|
|
|
|
time(&next_update); // get the current time
|
|
if (adapters[iAdapter].u8PWR != 0xff) {
|
|
pinMode(adapters[iAdapter].u8PWR, OUTPUT);
|
|
digitalWrite(adapters[iAdapter].u8PWR, 1); // enable power to EPD
|
|
}
|
|
// Make sure SPI is enabled; if not, we can enable it from here
|
|
// (at least on Raspberry Pi SBCs)
|
|
{
|
|
DIR *pDir;
|
|
struct dirent *pDE;
|
|
int bFound = 0;
|
|
|
|
pDir = opendir("/dev");
|
|
if (!pDir) {
|
|
printf("Error searching /dev directory; try running as sudo. Aborting...\n");
|
|
return;
|
|
}
|
|
// Search all names for "spidev"
|
|
while ((pDE = readdir(pDir)) != NULL) {
|
|
if (memcmp(pDE->d_name, "spidev", 6) == 0) { // found one!
|
|
bFound = 1;
|
|
break;
|
|
}
|
|
} // while searching
|
|
if (!bFound) { // SPI is disabled, enable it
|
|
printf("Enabling the SPI bus...\n");
|
|
if (system("sudo dtparam spi=on") == -1) { // problem
|
|
printf("Error trying to enable SPI!\n");
|
|
return;
|
|
} else {
|
|
printf("SPI enabled\n");
|
|
}
|
|
usleep(1000000); // allow time for it to start
|
|
}
|
|
}
|
|
if (iAdapter == ADAPTER_WAVESHARE_IT8951) { // use FastEPD
|
|
rc = epaper.initIT8951(adapters[iAdapter].u8SPI, 0, 0, adapters[iAdapter].u8CS, adapters[iAdapter].u8BUSY, adapters[iAdapter].u8RST, -1, -1);
|
|
if (rc != BBEP_SUCCESS) {
|
|
printf("initIT8951 returned error: %d\n", rc);
|
|
return;
|
|
}
|
|
rc = epaper.setPanelSize(BBEP_DISPLAY_ED078KC2);
|
|
if (rc != BBEP_SUCCESS) {
|
|
printf("setPanelSize returned %d\n", rc);
|
|
return;
|
|
}
|
|
epaper.fillScreen(BBEP_WHITE);
|
|
trmnl.setDisplaySize(epaper.width(), epaper.height());
|
|
#ifdef SHOW_DETAILS
|
|
printf("Setting display size to %d x %d\n", epaper.width(), epaper.height());
|
|
#endif
|
|
} else { // use bb_epaper
|
|
// This MUST be set before initializing the I/O so that the initial
|
|
// command sequence is sent to properly prepare the EPD for receiving data
|
|
rc = bbep.setPanelType((iPanel1Bit == -1) ? iPanel2Bit : iPanel1Bit);
|
|
#ifdef SHOW_DETAILS
|
|
printf("setPanelType returned %d\n", rc);
|
|
#endif
|
|
if (adapters[iAdapter].u8CS2 != 0) {
|
|
bbep.setCS2(adapters[iAdapter].u8CS2);
|
|
}
|
|
bbep.initIO(adapters[iAdapter].u8DC, adapters[iAdapter].u8RST, adapters[iAdapter].u8BUSY, adapters[iAdapter].u8CS, adapters[iAdapter].u8SPI, 0, 8000000);
|
|
bbep.allocBuffer(true); // always allocate 2 memory planes
|
|
if (bbep.width() < bbep.height() && bbep.width() < 800) {
|
|
bbep.setRotation(270);
|
|
}
|
|
trmnl.setDisplaySize(bbep.width(), bbep.height());
|
|
#ifdef SHOW_DETAILS
|
|
printf("Setting display size to %d x %d\n", bbep.width(), bbep.height());
|
|
#endif
|
|
} // bb_epaper
|
|
while (!bQuit) {
|
|
fd_set set;
|
|
struct timeval timeout = {0, 1000}; // 1ms timeout to keep SDL responsive
|
|
FD_ZERO(&set);
|
|
FD_SET(STDIN_FILENO, &set);
|
|
if (select(STDIN_FILENO + 1, &set, NULL, NULL, &timeout) > 0) {
|
|
if (FD_ISSET(STDIN_FILENO, &set)) {
|
|
char c = getchar();
|
|
if (c == '\n' || c == '\r') { // Detect Enter key
|
|
printf("Enter key pressed, skipping to next in playlist...\n");
|
|
next_update = now;
|
|
} else if (c == 0x1b) { // ESC key
|
|
bQuit = true;
|
|
}
|
|
}
|
|
}
|
|
time(&now);
|
|
if (now > next_update) {
|
|
rc = trmnl.getAPI(szKey, szURL);
|
|
if (rc == TRMNL_SUCCESS) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("getAPI succeeded\n");
|
|
#endif
|
|
next_update = now + trmnl.getSleepTime();
|
|
rc = trmnl.getImage(&pImage, &iSize);
|
|
if (rc == TRMNL_SUCCESS) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("getImage succeed, size = %d bytes\n", iSize);
|
|
#endif
|
|
if (decodeImage(pImage, iSize)) {
|
|
ShowEPDImage();
|
|
}
|
|
trmnl.freeImage();
|
|
}
|
|
} else {
|
|
printf("getAPI failed with error: %d, exiting...\n", trmnl.getHTTPCode());
|
|
bQuit = true;
|
|
}
|
|
}
|
|
usleep(100000); // don't use 100% of the CPU
|
|
} // while (!bQuit)
|
|
if (adapters[iAdapter].u8PWR != 0xff) {
|
|
digitalWrite(adapters[iAdapter].u8PWR, 0); // disable power to EPD
|
|
}
|
|
if (iAdapter == ADAPTER_WAVESHARE_IT8951) {
|
|
epaper.deInit(); // shut down the board and I/O
|
|
}
|
|
} /* TRMNL_EPAPER() */
|
|
#endif // __MACH__
|
|
//
|
|
// Parse the command line arguments to substitute or override the JSON settings
|
|
//
|
|
void ParseArgs(int argc, const char *argv[])
|
|
{
|
|
char szFile[256];
|
|
|
|
if (argc < 2) return; // nothing to do
|
|
printf("cli parameters overriding JSON...\n");
|
|
for (int i=1; i<argc; i++) {
|
|
char *pName, *pValue, *saveptr;
|
|
pName = strtok_r((char *)argv[i], "=", &saveptr);
|
|
pValue = strtok_r(NULL, "=", &saveptr);
|
|
printf("%d: %s %s\n", i, pName, pValue);
|
|
if (strcmp(pName, "mode") == 0) {
|
|
iMode = FindItemName(szModes, pValue, "update mode");
|
|
} else if (strcmp(pName, "stretch") == 0) {
|
|
iStretch = FindItemName(szStretch, pValue, "stretch");
|
|
} else if (strcmp(pName, "file") == 0) {
|
|
strcpy(szFile, pValue);
|
|
} else if (strcmp(pName, "panel_1bit") == 0) {
|
|
iPanel1Bit = FindItemName(szPanels, pValue, "1-bit panel");
|
|
} else if (strcmp(pName, "panel_2bit") == 0) {
|
|
iPanel2Bit = FindItemName(szPanels, pValue, "2-bit panel");
|
|
} else if (strcmp(pName, "adapter") == 0) {
|
|
iAdapter = FindItemName(szAdapters, pValue, "adapter");
|
|
} else if (strcmp(pName, "invert") == 0) {
|
|
iInvert = !strcmp(pValue, "true");
|
|
}
|
|
}
|
|
} /* ParseArgs() */
|
|
//
|
|
// Read and parse the JSON files defining the TRMNL setup parameters
|
|
//
|
|
void ParseJSON(void)
|
|
{
|
|
int rc, iSize;
|
|
FILE *ihandle;
|
|
uint8_t *pData;
|
|
char szJSON[256]; // current dir
|
|
cJSON *pJSON, *pItem;
|
|
char szFile[256];
|
|
|
|
szKey[0] = szURL[0] = 0; // assume failure
|
|
strcpy(szJSON, getenv("HOME")); // get the home directory
|
|
strcat(szJSON, "/.config/trmnl/config.json"); // name of local config file
|
|
ihandle = fopen(szJSON, "r+b");
|
|
if (ihandle) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("config.json found!\n");
|
|
#endif
|
|
fseek(ihandle, 0, SEEK_END);
|
|
iSize = (int)ftell(ihandle);
|
|
fseek(ihandle, 0, SEEK_SET);
|
|
pData = (uint8_t *)malloc(iSize);
|
|
rc = fread(pData, 1, iSize, ihandle);
|
|
if (rc != iSize) {
|
|
printf("Error reading %s!\n", szJSON);
|
|
fclose(ihandle);
|
|
free(pData);
|
|
return;
|
|
}
|
|
fclose(ihandle);
|
|
pJSON = cJSON_ParseWithLength((const char *)pData, iSize);
|
|
if (pJSON) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("config.json parsed successfully!\n");
|
|
#endif
|
|
if (cJSON_HasObjectItem(pJSON, "api_key")) {
|
|
pItem = cJSON_GetObjectItem(pJSON, "api_key");
|
|
strcpy(szKey, pItem->valuestring);
|
|
}
|
|
if (cJSON_HasObjectItem(pJSON, "base_url")) {
|
|
pItem = cJSON_GetObjectItem(pJSON, "base_url");
|
|
strcpy(szURL, pItem->valuestring);
|
|
strcat(szURL, "/api/display");
|
|
}
|
|
} // if pJSON
|
|
free(pData);
|
|
} // if iHandle
|
|
strcpy(szJSON, getenv("HOME")); // get the home directory
|
|
strcat(szJSON, "/.config/trmnl/show_img.json"); // name of local config file
|
|
//printf("config name: %s\n", szJSON);
|
|
ihandle = fopen(szJSON, "r+b");
|
|
if (ihandle) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("show_img.json found!\n");
|
|
#endif
|
|
fseek(ihandle, 0, SEEK_END);
|
|
iSize = (int)ftell(ihandle);
|
|
fseek(ihandle, 0, SEEK_SET);
|
|
pData = (uint8_t *)malloc(iSize);
|
|
rc = fread(pData, 1, iSize, ihandle);
|
|
if (rc != iSize) {
|
|
printf("Error reading file!\n");
|
|
fclose(ihandle);
|
|
free(pData);
|
|
return;
|
|
}
|
|
fclose(ihandle);
|
|
pJSON = cJSON_ParseWithLength((const char *)pData, iSize);
|
|
if (pJSON) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("show_img.json parsed successfully!\n");
|
|
#endif
|
|
if (cJSON_HasObjectItem(pJSON, "orientation")) {
|
|
int i;
|
|
pItem = cJSON_GetObjectItem(pJSON, "orientation");
|
|
i = pItem->valueint;
|
|
if (i == 90 || i == 180 || i == 270) {
|
|
iOrientation = i;
|
|
#ifdef SHOW_DETAILS
|
|
printf("orientation = %d\n", iOrientation);
|
|
#endif
|
|
}
|
|
}
|
|
if (cJSON_HasObjectItem(pJSON, "stretch")) {
|
|
pItem = cJSON_GetObjectItem(pJSON, "stretch");
|
|
iStretch = FindItemName(szStretch, pItem->valuestring, "stretch");
|
|
if (iStretch >= 0) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("stretch = %s\n", szStretch[iStretch]);
|
|
#endif
|
|
}
|
|
}
|
|
if (cJSON_HasObjectItem(pJSON, "invert")) {
|
|
pItem = cJSON_GetObjectItem(pJSON, "invert");
|
|
iInvert = !strcmp(pItem->valuestring, "true");
|
|
#ifdef SHOW_DETAILS
|
|
printf("invert = %s\n", (iInvert) ? "true" : "false");
|
|
#endif
|
|
}
|
|
if (cJSON_HasObjectItem(pJSON, "adapter")) {
|
|
pItem = cJSON_GetObjectItem(pJSON, "adapter");
|
|
iAdapter = FindItemName(szAdapters, pItem->valuestring, "adapter");
|
|
if (iAdapter >= 0) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("Adapter = %s\n", szAdapters[iAdapter]);
|
|
#endif
|
|
}
|
|
}
|
|
if (cJSON_HasObjectItem(pJSON, "panel_1bit")) {
|
|
pItem = cJSON_GetObjectItem(pJSON, "panel_1bit");
|
|
iPanel1Bit = FindItemName(szPanels, pItem->valuestring, "1-bit panel");
|
|
if (iPanel1Bit >= 0) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("panel1bit = %d (%s)\n", iPanel1Bit, szPanels[iPanel1Bit]);
|
|
#endif
|
|
}
|
|
}
|
|
if (cJSON_HasObjectItem(pJSON, "panel_2bit")) {
|
|
pItem = cJSON_GetObjectItem(pJSON, "panel_2bit");
|
|
iPanel2Bit = FindItemName(szPanels, pItem->valuestring, "2-bit panel");
|
|
if (iPanel2Bit >= 0) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("panel2bit = %d (%s)\n", iPanel2Bit, szPanels[iPanel2Bit]);
|
|
#endif
|
|
}
|
|
}
|
|
if (cJSON_HasObjectItem(pJSON, "mode")) {
|
|
pItem = cJSON_GetObjectItem(pJSON, "mode");
|
|
iMode = FindItemName(szModes, pItem->valuestring, "update mode");
|
|
if (iMode >= 0) {
|
|
#ifdef SHOW_DETAILS
|
|
printf("mode = %s\n", szModes[iMode]);
|
|
#endif
|
|
}
|
|
}
|
|
if (cJSON_HasObjectItem(pJSON, "file")) {
|
|
pItem = cJSON_GetObjectItem(pJSON, "file");
|
|
strcpy(szFile, pItem->valuestring);
|
|
}
|
|
cJSON_Delete(pJSON);
|
|
} else {
|
|
printf("Error parsing JSON!\n");
|
|
}
|
|
free(pData);
|
|
} // if show_img.json file exists
|
|
#ifdef SHOW_DETAILS
|
|
printf("key: %s, url: %s\n", szKey, szURL);
|
|
#endif
|
|
} /* ParseJSON() */
|
|
void signal_handler(int signum)
|
|
{
|
|
printf("Ctrl-C hit; exiting...\n");
|
|
bQuit = true;
|
|
} /* signal_handler() */
|
|
//
|
|
// Set STDIN to raw or cooked mode
|
|
//
|
|
void setRawMode(bool enable) {
|
|
static struct termios oldt, newt;
|
|
if (enable) {
|
|
tcgetattr(STDIN_FILENO, &oldt);
|
|
newt = oldt;
|
|
newt.c_lflag &= ~(ICANON | ECHO); // Disable buffering and echoing
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
|
|
} else {
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
|
|
}
|
|
}
|
|
//
|
|
// Main program entry point
|
|
//
|
|
int main(int argc, const char * argv[]) {
|
|
|
|
if (argc == 2 && strcmp(argv[1], "-d") == 0) {
|
|
iInvert = 1; // dark mode
|
|
}
|
|
printf("TRMNL Display\nPress ENTER to skip, ESC to exit\n");
|
|
iAdapter = iPanel1Bit = iPanel2Bit = -1;
|
|
#ifndef __MACH__
|
|
iMode = REFRESH_FULL; // default to full refresh
|
|
#endif
|
|
signal(SIGINT, signal_handler); // catch Ctrl-C
|
|
bSSH = (getenv("SSH_CLIENT") != nullptr);
|
|
#ifdef SHOW_DETAILS
|
|
printf("Running from SSH = %s\n", (bSSH) ? "Yes" : "No");
|
|
#endif
|
|
ParseJSON();
|
|
#ifdef __MACH__
|
|
iAdapter = ADAPTER_FRAMEBUFFER;
|
|
#endif
|
|
ParseArgs(argc, argv);
|
|
if (!szKey[0]) {
|
|
printf("API key not found, exiting...\n");
|
|
return -1;
|
|
}
|
|
if (iAdapter == ADAPTER_FRAMEBUFFER) { // for framebuffer, some parameters don't matter
|
|
iMode = iPanel1Bit = iPanel2Bit = 0;
|
|
}
|
|
if (iAdapter == -1 || (iPanel1Bit == -1 && iPanel2Bit == -1)) { // print instructions
|
|
ShowHelp();
|
|
return -1;
|
|
}
|
|
if (iStretch < 0) iStretch = STRETCH_ASPECTFILL; // default
|
|
iMode = 0; // DEBUG - safer to use full refresh all the time
|
|
if (bSSH) {
|
|
setRawMode(true);
|
|
}
|
|
if (iAdapter == ADAPTER_FRAMEBUFFER) { // framebuffer
|
|
TRMNL_SDL();
|
|
} else {
|
|
#ifdef __MACH__
|
|
printf("trmnl_display cannot control SPI ePaper display on MacOS!\n");
|
|
#else
|
|
TRMNL_EPAPER();
|
|
#endif
|
|
}
|
|
if (bSSH) {
|
|
setRawMode(false);
|
|
}
|
|
return 0;
|
|
} /* main() */
|
|
|
|
|