// // 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 #include #endif // __MACH__ #include #include #include "cJSON.h" #include #include #include #include #include #include #include #include #include #include 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= can be full, fast or partial\nadapter= can be waveshare_2 or pimoroni\npanel_1bit=\npanel_2bit=\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 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= 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>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> 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<pixels; d += y * iWidth; switch(iBpp) { case 1: { uint8_t uc; uc = *s++; for (x=0; x> 3); uc <<= 1; if ((x & 7) == 7) uc = *s++; } // for x } break; case 2: { uint8_t c, uc; uc = *s++; for (x=0; x> 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> 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> 3); uc = *s++; } // for x } break; case 24: case 32: { for (x=0; x> 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> 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; ivaluestring); } 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() */