Files
Larry BankandGitHub 87b9430782 Changed to cpp program based on trmnl_lib (#29)
* 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
2026-08-12 13:05:18 -04:00

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() */