DEV Community

Cover image for Simple Arduino Framework Photo Frame Implementation with Photos Downloaded from the Internet via DumbDisplay
Trevor Lee
Trevor Lee

Posted on

Simple Arduino Framework Photo Frame Implementation with Photos Downloaded from the Internet via DumbDisplay

Simple Arduino Framework Raspberry Pi Pico / ESP32 TFT LCD Photo Frame Implementation with Photos Downloaded from the Internet via DumbDisplay

The target of this project is to implement, mostly the software part, a simple Arduino framework photos / images showing "photo frame" using Raspberry Pi Pico or ESP32 with photos / images downloaded from the Internet via DumbDisplay -- an Android app running on your Android phone.

The microcontroller program here is developed in Arduino framework using VS Code and PlatformIO, in the similar fashion as described by the post -- A Way to Run Arduino Sketch With VSCode PlatformIO Directly

The simple remote UI for downloading photos / images from the Internet is realized with the help of the DumbDisplay Android app. For a brief description of DumbDisplay, you may want to refer to the post -- Blink Test With Virtual Display, DumbDisplay

Please note that the UI is driven by the microcontroller program; i.e., the control flow of the UI is programmed in the sketch. Additionally, please note that the downloaded image, be it in Jpeg or PNG format, will be transferred to the microcontroller board in Jpeg format, scaled to site inside the TFT LCD screen.

For Raspberry Pi Pico board (WiFi), a ST7789 2.8 inch 240x320 SPI TFT LCD screen is attached to a Raspberry Pi Pico board.
The TFT LCD module library used is the Adafruit-ST7735-Library Arduino library.

For ESP32, LiLyGo TDisplay / TCamera Plus board is used.
The TFT LCD module library use is the bodmer/TFT_eSPI Arduino library.

In all cases, the Jpeg library used is the bodmer/TJpg_Decoder Arduino library.

A simple flash-based LittleFS file-system is allocated for storing the saved Jpeg images.

The microcontroller board has two running modes:

1) When connected to the DumbDisplay Android app (using WiFi), a simple UI is provided for downloading images from some predefined sites,
as well as for transferring the downloaded image in Jpeg format to the microcontroller board.
Note that the predefined sites is hardcoded in the sketch that you can conveniently change as desired by changing the sketch.
2) When not connected to the DumbDisplay Android app, the microcontroller cycles through the saved Jpeg images displaying them to the TFT LCD
screen one by one like a simple "photo frame". Note that since the images are stored in LittleFS, they will survive even after reboot of the
microcontroller board.

Connect for the UI; disconnect to enjoy "photo frame" slide show.

The UI

The first time connected, an image download will be initiated automatically.
After downloading an image the image will be transferred to the microcontroller board in Jpeg format and be displayed to the TFT LCD screen.

You can choose to save the transferred image by clicking the đź’ľSave button.
Notice that the [7-segment] number displayed next to the Saved🗂️ label will be bumped up after saving.
The number indicates the number of images saved to the microcontroller's LittleFS storage.

If you so desired, you can turn on auto-save by clicking the Auto save button. (You turn off auto-save by clicking the button again.)

If you want to initiate another download of image, click on the canvas that shows the downloaded image.

If you want to delete all the saved images, double-click on the [7-segment] number.

After you are done with downloading and saving images, disconnect the microcontroller.
After disconnection, the "photo frame" slide show begins on the microcontroller side.

Anytime you want to change the saved images, reconnect to DumbDisplay Android app.

Connect for the UI; disconnect to enjoy "photo frame" slide show.

Wiring TFT LCD Module

For LiLyGo TDisplay / TCamera Plus board, the TFT LCD screen is in-built the microcontroller board; hence, no need for additional wiring.

For Raspberry Pi Pico board, as mentioned previously, a ST7789 2.8 inch 240x320 SPI TFT LCD module is used; hence, some wiring is necessary

Raspberry Pi Pico SPI TFT LCD
3V3 VCC
GND GND
GP21 BL
GP17 CS
GP16 RS / DC
GP18 CLK / SCLK
GP19 SDA / MOSI
GP20 RST

Developing and Building

As mentioned previously, the sketch will be developed using VS Code and PlatformIO.
Please clone the PlatformIO project TFTImageShow GitHub repository.

The configurations for developing and building of the sketch in basically captured in the platformio.ini file

[env] monitor_speed = 115200 [env:PICOW] ; ensure long file name support ... git config --system core.longpaths true platform = https://github.com/maxgerhardt/platform-raspberrypi.git board = rpipicow framework = arduino board_build.core = earlephilhower board_build.filesystem = littlefs board_build.filesystem_size = 1m lib_deps = https://github.com/trevorwslee/Arduino-DumbDisplay https://github.com/adafruit/Adafruit-ST7735-Library.git https://github.com/adafruit/Adafruit-GFX-Library https://github.com/Bodmer/TJpg_Decoder.git Wire SPI https://github.com/adafruit/Adafruit_BusIO build_flags = -D FOR_PICOW [env:TDISPLAY] platform = espressif32 board = esp32dev framework = arduino board_build.filesystem = littlefs lib_deps = https://github.com/trevorwslee/Arduino-DumbDisplay bodmer/TFT_eSPI ; Setup25_TTGO_T_Display bodmer/TJpg_Decoder LittleFS build_flags = -D FOR_TDISPLAY [env:TCAMERAPLUS] platform = espressif32 board = esp32dev framework = arduino board_build.filesystem = littlefs lib_deps = https://github.com/trevorwslee/Arduino-DumbDisplay bodmer/TFT_eSPI ; modify User_Setup_Select.h ... Setup44_TTGO_CameraPlus bodmer/TJpg_Decoder LittleFS Wire SPI SPIFFS build_flags = -D FOR_TCAMERAPLUS 
Enter fullscreen mode Exit fullscreen mode

Please make sure you select the correct PlatformIO project environment -- PICOW / TDISPLAY / TCAMERAPLUS

For PICOW, the platform core is download from https://github.com/maxgerhardt/platform-raspberrypi.git.
(As far as I know, this is the only PlatformIO platform core that supports the use of Raspberry Pi PicoW WiFi capability.)

It might take a long time for PlatformIO to download and install it.
If PlatformIO fails to download and install the platform core, it might be that your system doesn't have long "file name" enabled, in such a case, try

git config --system core.longpaths true 
Enter fullscreen mode Exit fullscreen mode

For TDISPLAY that uses bodmer/TFT_eSPI, you will need to modify the installed .pio/libdeps/TDISPLAY/TFT_eSPI/User_Set_Select.h
to use User_Setups/Setup25_TTGO_T_Display.h rather than the default User_Setup.h like

... //#include <User_Setup.h> // Default setup is root library folder ... #include <User_Setups/Setup25_TTGO_T_Display.h> // Setup file for ESP32 and TTGO T-Display ST7789V SPI bus TFT ... 
Enter fullscreen mode Exit fullscreen mode

For TCAMERAPLUS which also uses bodmer/TFT_eSPI, modify User_Set_Select.h similarly

... //#include <User_Setup.h> // Default setup is root library folder ... #include <User_Setups/Setup44_TTGO_CameraPlus.h> // Setup file for ESP32 and TTGO T-CameraPlus ST7789 SPI bus TFT 240x240 ... 
Enter fullscreen mode Exit fullscreen mode

The program entry point is src/main.cpp

// *** // the below _secret.h just define macros like: // #define WIFI_SSID "your-wifi-ssid" // #define WIFI_PASSWORD "your-wifi-password" // *** #include "_secret.h" #include "tft_image_show/tft_image_show.ino" 
Enter fullscreen mode Exit fullscreen mode

Notice there are two included files -- _secret.h and tft_image_show/tft_image_show.ino -- in the src directory

You will need to create the _secret.h with content like

#define WIFI_SSID "your-wifi-ssid" #define WIFI_PASSWORD "your-wifi-password" 
Enter fullscreen mode Exit fullscreen mode

With these macros for accessing your WiFi, the microcontroller board will connect to DumbDisplay Android app using WiFi.
If you do not want to use WiFi, simply don't provide them.
In such a case, connection to DumbDisplay Android app is assumed to be using serial UART (slower) via an OTG adapter.
Please refer to the above mentioned post -- Blink Test With Virtual Display, DumbDisplay

The Sketch

The sketch of the project is tft_image_show/tft_image_show.ino. You can [easily] customize some aspects of the sketch

... // NEXT_S defines the delay (in seconds) to show next saved image #define NEXT_S 5 ... // MAX_IMAGE_COUNT define that maximum number of images that can be saved // set MAX_IMAGE_COUNT to 0 to force reformat the storage #define MAX_IMAGE_COUNT 10 ... // getDownloadImageURL() returns a URL to download an image; add / remove sites as needed // download image bigger than needed (on purpose) const String urls[] = { String("https://loremflickr.com/") + String(2 * TFT_WIDTH) + String("/") + String(2 * TFT_HEIGHT), String("https://picsum.photos/") + String(2 * TFT_WIDTH) + String("/") + String(2 * TFT_HEIGHT), }; const char* getDownloadImageURL() { int idx = random(2); return urls[idx].c_str(); } ... 
Enter fullscreen mode Exit fullscreen mode
  • The slide show delay is defined by the macro NEXT_S, which default to 5 seconds
  • The maximum number of saved images is defined by the macro MAX_IMAGE_COUNT, which default to 10. Note that if you set MAX_IMAGE_COUNT to 0, flash and run the sketch, the LittleFS storage will be reformatted. For normal running, MAX_IMAGE_COUNT should be at lease 1.
  • You can modify urls / getDownloadImageURL() to add / remove Internet sites for downloading images.

Sketch Highlight -- TFT LCD Library Adafruit-ST7735-Library

Here is how Adafruit-ST7735-Library used in the sketch.

First a global tft object is defined like

 #define A_TFT_BL 21 #define A_TFT_CS 17 #define A_TFT_DC 16 #define A_TFT_SCLK 18 #define A_TFT_MOSI 19 #define A_TFT_RST 20 #define TFT_WIDTH 320 #define TFT_HEIGHT 240 #include <Adafruit_ST7789.h> Adafruit_ST7789 tft(A_TFT_CS, A_TFT_DC, A_TFT_RST); 
Enter fullscreen mode Exit fullscreen mode

Notice the pin assignments exactly match the wiring described previously.

Then in setup()

 pinMode(A_TFT_BL, OUTPUT); digitalWrite(A_TFT_BL, 1); // light it up tft.init(240, 320, SPI_MODE0); tft.invertDisplay(false); tft.setRotation(1); tft.setSPISpeed(40000000); 
Enter fullscreen mode Exit fullscreen mode
  • The back-light part is obvious.
  • The TFT LCD screen size is 240x320.
  • Why SPI_MODE0 and other settings? Simply, they work for me.

Sketch Highlight -- TFT LCD Library bodmer/TFT_eSPI

Here is how bodmer/TFT_eSPI used in the sketch.

First, a global tft object is defined like

 #include <TFT_eSPI.h> TFT_eSPI tft = TFT_eSPI(); 
Enter fullscreen mode Exit fullscreen mode

Then in setup()

 tft.init(); tft.setRotation(0); 
Enter fullscreen mode Exit fullscreen mode

Sketch Highlight -- Jpeg Library bodmer/TJpg_Decoder

You might be wondering why use Jpeg but not RGB565 directly. Simply because of the very high data compression ratio of Jpeg.

Anyway, here is how bodmer/TJpg_Decoder used in the sketch.

Include the needed headers

#include <TJpg_Decoder.h> 
Enter fullscreen mode Exit fullscreen mode

In setup()

#if defined(TFT_ESPI_VERSION) TJpgDec.setSwapBytes(true); #endif TJpgDec.setCallback(tft_output); 
Enter fullscreen mode Exit fullscreen mode

Why setSwapBytes(true)? Since it seems to work that way.

And here is the callback tft_output, which is mostly copied from an example of bodmer/TJpg_Decoder

bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) { // Stop further decoding as image is running off bottom of screen if ( y >= tft.height() ) return 0; // This function will clip the image block rendering automatically at the TFT boundaries #if defined(TFT_ESPI_VERSION) tft.pushRect(x, y, w, h, bitmap); #else tft.drawRGBBitmap(x, y, bitmap, w, h); #endif // Return 1 to decode next block return 1; } 
Enter fullscreen mode Exit fullscreen mode

Notice that in case of using bodmer/TFT_eSPI, the way to draw the decoded Jpeg chunk is

 tft.pushRect(x, y, w, h, bitmap); 
Enter fullscreen mode Exit fullscreen mode

If Adafruit-ST7735-Library is use, the way to draw the decoded Jpeg chunk is

 tft.drawRGBBitmap(x, y, bitmap, w, h); 
Enter fullscreen mode Exit fullscreen mode

After setting TJpgDec up, a Jpeg image can be drawn like

 TJpgDec.drawJpg(x, y, jpegImage.bytes, jpegImage.byteCount); 
Enter fullscreen mode Exit fullscreen mode

Sketch Highlight -- LittleFS

Include the needed headers

#include <FS.h> #include <LittleFS.h> 
Enter fullscreen mode Exit fullscreen mode

In setup()

 LittleFS.begin(); 
Enter fullscreen mode Exit fullscreen mode

If you want to format the LittleFS, call format() like

 LittleFS.format() 
Enter fullscreen mode Exit fullscreen mode

Jpeg image can be saved like

 File f = LittleFS.open(fileName, "w"); if (f) { f.println(currentJpegImage.width); f.println(currentJpegImage.height); f.println(currentJpegImage.byteCount); f.write(currentJpegImage.bytes, currentJpegImage.byteCount); f.close(); } 
Enter fullscreen mode Exit fullscreen mode

Notice that not only the Jpeg image bytes got written to the file, the various metadata are saved first. (I.e. the file is not a normal JPG file, but a customized one.)

And Jpeg image can be read back like

 File f = LittleFS.open(fileName, "r"); if (f) { int width = f.readStringUntil('\n').toInt(); int height = f.readStringUntil('\n').toInt(); int byteCount = f.readStringUntil('\n').toInt(); uint8_t* bytes = new uint8_t[byteCount]; f.readBytes((char*) bytes, byteCount); f.close(); tempImage.width = width; tempImage.height = height; tempImage.byteCount = byteCount; tempImage.bytes = bytes; } 
Enter fullscreen mode Exit fullscreen mode

Sketch Highlight -- DumbDisplay

Like all other use cases of using DumbDisplay, you first declare a global DumbDisplay object dumbdisplay

#if defined(WIFI_SSID) #include "wifidumbdisplay.h" DumbDisplay dumbdisplay(new DDWiFiServerIO(WIFI_SSID, WIFI_PASSWORD)); #else #include "dumbdisplay.h" DumbDisplay dumbdisplay(new DDInputOutput()); #endif 
Enter fullscreen mode Exit fullscreen mode

There, the new configured DDWiFiServerIO is one of the few ways to establish connection with DumbDisplay Android app.
Like in "no WiFi" case, Serial UART DDInputOutput is used.

Then, several global helper objects / pointers are declared

DDMasterResetPassiveConnectionHelper pdd(dumbdisplay); GraphicalDDLayer* imageLayer; LcdDDLayer* saveButtonLayer; LcdDDLayer* autoSaveOptionLayer; LcdDDLayer* savedCountLabelLayer; SevenSegmentRowDDLayer* savedCountLayer; SimpleToolDDTunnel* webImageTunnel; ImageRetrieverDDTunnel* imageRetrieverTunnel = NULL; 
Enter fullscreen mode Exit fullscreen mode
  • The DDMasterResetPassiveConnectionHelper global object pdd is a helper for managing connect and reconnection with DumbDisplay app.
  • The GraphicalDDLayer pointer imageLayer is the canvas to which downloaded image is drawn to. Like other layers, they the layer is created later, in this case, when DumbDisplay app is created.
  • The LcdDDLayer pointers saveButtonLayer, autoSaveOptionLayer and savedCountLabelLayer are for save button, auto-save button, and saved-count label respectively.
  • The SevenSegmentRowDDLayer pointer savedCountLayer is for show the saved image count.
  • The SimpleToolDDTunnel pointer webImageTunnel is for download image to DumbDisplay Android app purpose. It will be created together with other layers and tunnels.
  • The ImageRetrieverDDTunnel pointer imageRetrieverTunnel is for retrieving the data of the downloaded image, in Jpeg format.

The life-cycle of the above DumbDisplay layers and "tunnels" are managed by the global pdd object, which will monitor connection and disconnection of
DumbDisplay app, calling appropriate user-defined functions as well as DumbDisplay functions in the appropriate time. It is cooperatively given "time slices" in the loop() block like

 void loop() { ... pdd.loop(initializeDD, updateDD); ... } 
Enter fullscreen mode Exit fullscreen mode

The initializeDD is the function defined in the sketch that is supposed to create the various layer and tunnel objects.

void initializeDD() { tft.fillScreen(COLOR_BG); // create a graphical layer for drawing the downloaded web image to imageLayer = dumbdisplay.createGraphicalLayer(2 * TFT_WIDTH, 2 * TFT_HEIGHT); ... // create a LCD layer for the save button saveButtonLayer = dumbdisplay.createLcdLayer(6, 2); ... // create a LCD layer for the auto save option autoSaveOptionLayer = dumbdisplay.createLcdLayer(6, 1); ... // create a LCD layer as the label for the number of saved images savedCountLabelLayer = dumbdisplay.createLcdLayer(8, 1); ... // create a 7-segment layer for showing the number of saved images savedCountLayer = dumbdisplay.create7SegmentRowLayer(2); ... // create a tunnel for downloading web image ... initially, no URL yet ... downloaded.png is the name of the image to save webImageTunnel = dumbdisplay.createImageDownloadTunnel("", "downloaded.png"); ... // create a tunnel for retrieving JPEG image data from DumbDisplay app storage imageRetrieverTunnel = dumbdisplay.createImageRetrieverTunnel(); // auto pin the layers dumbdisplay.configAutoPin(DDAutoPinConfig('V') .addLayer(imageLayer) .beginGroup('H') ... .endGroup() .build()); } 
Enter fullscreen mode Exit fullscreen mode

The updateDD is the function define in the sketch that is supposed to receive "time slices" to update / act-on the layer and tunnel objects.

 bool isFirstUpdate = !pdd.firstUpdated(); bool updateUI = isFirstUpdate; if (autoSaveOptionLayer->getFeedback() != NULL) { // toggle auto save autoSave = !autoSave; updateUI = true; } if (updateUI) { if (autoSave) { autoSaveOptionLayer->writeLine("Auto✅️"); } else { autoSaveOptionLayer->writeLine("Auto⛔"); } } ... if (isFirstUpdate || state == NOTHING) { if (isFirstUpdate || imageLayer->getFeedback() != NULL) { // trigger download image saveButtonLayer->disabled(true); imageLayer->noBackgroundColor(); state = DOWNLOADING_FOR_IMAGE; } return; } if (state == DOWNLOADING_FOR_IMAGE) { // set the URL to download web image currentJpegImage.release(); String url = getDownloadImageURL(); webImageTunnel->reconnectTo(url); imageLayer->clear(); imageLayer->write("downloading image ..."); state = WAITING_FOR_IMAGE_DOWNLOADED; return; } if (state == WAITING_FOR_IMAGE_DOWNLOADED) { int result = webImageTunnel->checkResult(); if (result == 1) { // web image downloaded ... retrieve JPEG data of the image imageRetrieverTunnel->reconnectForJpegImage("downloaded.png", TFT_WIDTH, TFT_HEIGHT); imageLayer->clear(); imageLayer->drawImageFileFit("downloaded.png"); state = RETRIEVING_IMAGE; retrieveStartMillis = millis(); } else if (result == -1) { // failed to download the image imageLayer->clear(); imageLayer->write("!!! failed to download image !!!"); dumbdisplay.writeComment("XXX failed to download XXX"); state = NOTHING; } return; } if (state == RETRIEVING_IMAGE) { // read the retrieve image (if it is available) DDJpegImage jpegImage; bool retrievedImage = imageRetrieverTunnel->readJpegImage(jpegImage); if (retrievedImage) { unsigned long retrieveTakenMillis = millis() - retrieveStartMillis; dumbdisplay.writeComment(String("* ") + jpegImage.width + "x" + jpegImage.height + " (" + String(jpegImage.byteCount / 1024.0) + " KB) in " + String(retrieveTakenMillis / 1000.0) + "s"); if (jpegImage.isValid()) { ... } else { ... } ... state = NOTHING; } return } 
Enter fullscreen mode Exit fullscreen mode

Notice

  • how layer "feedback" (e.g. clicking) is received using getFeedback()
  • how the webImageTunnel "tunnel" is used to download image with call to reconnectTo()
  • how the imageRetrieverTunnel "tunnel" is used to initiate retrieving of download image data with call to reconnectForJpegImage()
  • how the image data (DDJpegImage) is received via the tunnel imageRetrieverTunnel with call to readJpegImage()

The whole updateDD basically is a "state-machine" that handles the different states (state) of the UI processing:

  • NOTHING -- just started, or finished download / saving of image; will wait for imageLayer being clicked to initiate an image
  • DOWNLOADING_FOR_IMAGE -- this state could have been merged with previous stage; anyway, it reconnects webImageTunnel to activate an image download
  • WAITING_FOR_IMAGE_DOWNLOADED -- waiting for download image to complete; then will retrieve the download image to be displayed to the TFT LCD screen
  • RETRIEVING_IMAGE -- retrieving the download image data to be transferred to the microcontroller; once retrieved, display the image to the TFT LCD screen

The slide show is carried out when "idle" (not connected to DumbDisplay app)

void loop() { pdd.loop(initializeDD, updateDD); if (pdd.isIdle()) { if (pdd.justBecameIdle()) { // re-start slide show ... } unsigned long now = millis(); if (now >= nextMillis) { if (MAX_IMAGE_COUNT > 0 && savedImageCount > 0) { ... } else { ... } ... } } } 
Enter fullscreen mode Exit fullscreen mode

Build and Upload

Build, upload the sketch and try it out!

For WiFi connectivity, you will need to find out the IP address of the microcontroller board. Simply connect the microcontroller board with a Serial monitor (set to use baud rate 115200), you should see lines like

binded WIFI TrevorWireless listening on 192.168.0.218:10201 ... listening on 192.168.0.218:10201 ... 
Enter fullscreen mode Exit fullscreen mode

See that the IP address of the microcontroller board is printed out.

On DumbDisplay Android app side

You will need the microcontroller's IP address to configure DumbDisplay Android app to connect it with your microcontroller
  • Start the DumbDisplay app.
  • Click on the Establish Connection icon.
  • In the "establish connection" dialog, you should see the "add WIFI device" icon at the bottom right of the dialog. Click on it.
  • A popup for you to enter WIFI IP will be shown. Enter the IP address of your ESP board as Network Host. Click OK when done.
  • Back to the "establish connection" dialog, a new entry will be added, click on it to establish WIFI connection.

Have fun with it!

Enjoy!

Peace be with you!
May God bless you!
Jesus loves you!
Amazing Grace!

Top comments (0)